Vue响应式原理

本文只分析了data中数据变为响应式对象,及修改触发dom更新的过程,computed和watch暂不讨论

一、Vue如何实现数据响应式

  1、new Vue() 实例时,调用原型上_init() 方法

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

  2、_init方法中主要代码如下,其中调用initState方法

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

  3、initState方法主要初始化了props、data、methods、computed、watch等

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

  4、首先分析initData方法,方法最后调用 observe 方法

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  /** 省略代码 
   *  1、提示data方法应该返回一个对象 
   *  2、对比data和props、methods中的属性,如果重复则抛出警告
   *  3、代理 使通过this.x 访问data中的x属性值时,实际访问的是this._data.x
    */
  // observe data
  observe(data, true /* asRootData */)
}
observe 方法实现
function
observe (value, asRootData) { // 如果观测的数据不是一个对象或者是一个VNode,则直接返回 if (!isObject(value) || value instanceof VNode) { return } var ob; // 如果观测的对象已经包含了__ob__,说明该对象已经处于观测中,__ob__实际上是一个Observer实例,后面会介绍如何定义 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 否则满足上述条件下,new Observer ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }

  5、Observer对象源码如下

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value     
    this.dep = new Dep()
    this.vmCount = 0
  // 给被观测的数据定义__ob__ 属性,属性值为当前Observer实例对象
  // def方法是对Object.defineProperty方法的封装,第四个参数如果不传递或者传入false,则表示不可枚举
 
    def(value, '__ob__', this)
   // 被观测对象是数组
if (Array.isArray(value)) {
    // 绑定一些数组方法,包括push、pop、shift、unshift、splice、reverse、sort方法,可以触发视图更新操作等
if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) }
    // 调用observeArray 方法,该方法主要遍历数组每项,并对每项执行observe方法,进行观测
this.observeArray(value) } else {
    // 被观测数据是对象,调用walk方法,该方法主要遍历对象所有属性,并将对应属性值全部变成响应式对象
this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) {
    // 将对象变成响应式对象的方法调用 defineReactive(obj, keys[i]) } }
observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }

  6、defineReactive方法源码如下

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
 // 省略。。。,最终结果可以认为val = obj[key]  val = obj[key]
 // 对对象属性递归执行observe方法,将每个对象属性值设置get、set方法,变成响应式对象
let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () {
    // 这个方法在render方法执行时,会访问数据,则调用get方法,在这个方法中会进行依赖收集 const value
= getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }

  通过上述流程,则将data中的数据全部变成响应式对象,用一张图表示

 

 二、依赖收集

  上面提到,当访问某项数据时,会触发该数据的访问器get属性,那么什么时候会访问数据呢,其实是在render方法执行,生成vdom的过程中会进行数据的访问

  从上面_init方法中看到,最后执行了vm.$mount ()方法

  $mount()方法的前半部分主要是生成render函数,最后调用runtime-only和runtime-compiler版本共同的mount方法,可以参考 https://www.cnblogs.com/happy-dog/p/13697646.html

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
mountComponent 方法实现
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el /** 省略代码,提示使用了runtime-only版本下使用了template方式,应该换成render方式或者切换到runtime-compiler版本 */ callHook(vm, 'beforeMount') // 调用生命周期函数 let updateComponent /** 省略updateComponent的一些判断赋值,主要是增加了性能统计的一些代码 */ updateComponent = () => { vm._update(vm._render(), hydrating) // 1、render方法在updateComponent方法执行时执行,那么updateComponent方法什么时候执行呢 } new Watcher(vm, updateComponent, noop, { // 2、updateComponent方法作为实例化Watcher对象的参数, 这个Watcher对象是一个渲染watcher before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
watcher 构造函数和get方法实现
constructor ( vm: Component, expOrFn: string
| Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) /** 省略一些实例属性的赋值,目前不那么重要 */ this.cb = cb this.id = ++uid // uid for batching this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' this.getter = expOrFn this.value = this.get() // 1、实例化Watcher时,最后调用了get方法 } get () { pushTarget(this) // 2、首先执行pushTarget方法,该方法位于在源码dep.js文件中,给Dep订阅器设置唯一的watcher,并把当前watcher推入targetStack中 let value const vm = this.vm try { // 实际调用updateComponent方法 value = this.getter.call(vm, vm) // 3、执行getter方法,实际上就是执行updateComponent方法,即会执行vm_render()方法,该方法返回vnode,会访问数据,从而触发访问器属性get } catch (e) { /** updateComponent函数调用错误处理, */ } finally { if (this.deep) { traverse(value) } popTarget() // 4、此时依赖收集完成,从targetStack中弹出更新完的watcher,并把Dep.target置为targetStack中上一个watcher this.cleanupDeps() // 5、做一个newDepIds —— depIds,newDeps —— deps;并把newDepIds 和 newDeps清空 } return value }

 回到Vue把数据变成响应式对象时,定义的get属性

get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
        dep.depend()             // Dep对象的depend方法判断Dep.target存在,执行Dep.target.addDep(dep),即调用Watcher对象的addDep方法
        if (childOb) {
          childOb.dep.depend()             
          if (Array.isArray(value)) {
            dependArray(value)
          }
       }
    }
    return value
},
addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {     // 判断
        dep.addSub(this)
      }
    }
  }
// 把订阅器dep放在当前watcher的newDeps和newDepIds中,并且如果把当前watcher放在dep的subs中,即watcher中保存了dep,dep中也保存了watcher

 通过上述过程,即完成了依赖收集

三、派发更新

从前面的分析中,知道vdom生成过程中,进行了数据的访问,完成了收集依赖,接下来分析修改数据,更新dom的过程

修改数据时,触发访问器属性,set方法中,最后调用了dep.notify()

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    /** 省略判断非异步更新的情况 */
    // 遍历subs中的watcher,触发update方法
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()            // 如果是同步更新,不用放在下一个tick,直接执行run方法
    } else {
      queueWatcher(this)     // 一般都会走到这里,调用queueWatcher方法
    }
  }
queueWatcher 主要代码实现
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true queue.push(watcher) // 用队列收集需要更新的watcher nextTick(flushSchedulerQueue) // 执行nextTick方法 } }
// nextTick 主要代码实现
const callbacks = []
export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { cb.call(ctx) }) // 回调函数统一收集传入的cb函数,即flushSchedulerQueue if (!pending) { pending = true timerFunc() // 执行timerFunc() } }
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
主要是上面把callbacks中存放的flushSchedulerQueue方法放在异步中调用,如果浏览器支持原生Promise或者MutationObserver,则表示使用的微任务,否则表示回调放在宏任务中执行,
这样只有当执行栈中所有同步代码执行完成,才会执行nextTick方法中传入的
flushSchedulerQueue

 

flushSchedulerQueue 主要代码实现
function
flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id /** 排序原因 * 对watcher进行排序,因为组件更新是从父组件到子组件的,一个组件的自定义watcher在渲染watcher之前, * 自定义组件先于渲染watcher被创建(initState方法创建了自定义watcher,new Watcher创建渲染watcher), * 如果执行父组件的watcher时,子组件被销毁,则子组件的watcher可以被跳过,不执行 */ queue.sort((a, b) => a.id - b.id) // 遍历queue队列中watcher,进行更新 for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() // 执行beforeUpdate钩子函数 } id = watcher.id has[id] = null watcher.run() // 执行watcher.run, 触发vdom的重新生成渲染 /** 省略非生产环境下,判断是否触发了死循环,watcher一个对象变化,在里面又触发了这个对象变化,导致的死循环 */ } const activatedQueue = activatedChildren.slice() // keep-alive组件相关,暂时不管 const updatedQueue = queue.slice() // 暂存watcher队列,因为下面一行代码会做一些变量重置,queue会被重置为空数组 resetSchedulerState() // 重置queue,has,wait,flush callActivatedHooks(activatedQueue) // keep-alive组件相关,暂时不管 callUpdatedHooks(updatedQueue) // 所有watcher执行完成之后触发updated钩子函数 }
watcher run主要代码实现
run () {
if (this.active) {
    // 触发get方法实现,前面分析过get方法主要操作
    // 1、pushTarget
    // 2、调用this.getter() ,即updateComponent方法,该方法主要是先生成vdom,然后执行vm._update来更新dom
    // 3、popTarget
    // 4、cleanupDeps,清空newDeps
   const value
= this.get()
    // 省略了不影响主逻辑的代码,涉及到自定义watcher的一些实现
} }
cleanupDeps 主要代码实现
cleanupDeps () {
   // 不是第一次收集依赖时,deps不为空 let i
= this.deps.length while (i--) { const dep = this.deps[i]
    // 遍历deps,移除旧的订阅,防止某些组件被删除了,但是订阅却依然存在,导致不必要的浪费
    // 依赖收集是每次更新数据时都会做的事情,所以虽然旧的订阅被移除了,但是如果下次访问对应数据,还是会进行依赖的收集
if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } }
   // 将newDeps和newDepIds分别赋值给deps和depIds,并移除newDeps和newDepIds let tmp
= this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }

 整个派发更新的过程到此就结束了,中间其实跳过了很多自己不理解的代码,但是对主要流程不影响

整个过程是看完 黄佚老师的视频之后(https://coding.imooc.com/learn/list/228.html),自己又梳理了一遍,后面如果有更深刻的认识,会在补充

 

beforeMount执行时机:new Watcher之前,创建vdom之前

beforeUpdate执行时机:调用watcher.run之前,即重新生成vdom之前

updated执行时机:页面更新完成之后

 

posted @ 2020-09-25 23:49  爱吃橘子哇咔咔  阅读(411)  评论(0)    收藏  举报