Vue 的响应式原理及源码分析

回顾Vue的生命周期详解,在调用_init方法初始化Vue实例时,其中一步是调用initState(vm)初始化一些状态(props、data、computed、watch、methods),其中initData(vm)用于初始化data

function initData(vm) {  //初始化data
  ...
  observe(data, true /* asRootData */);
}

observe(data) 会将用户定义的data变成响应式的数据,它的创建过程:

export function observe (value: any, asRootData: ?boolean): Observer | void {
    //不是对象类型(对象包括数组),直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)    //为`data`创建一个`Observer`实例
  }
  if (asRootData && ob) {    //如果是根 data,ob.vmCount自增
    ob.vmCount++
  }
  return ob
}

1. 三个类

Vue实现响应式数据主要用到了三个类,首先就是Observer观察者类:

1.1 Observer 类

以用户提供的选项data(vm.$data)为例,假如当前正在调用new Observer(data)来创建一个Observer实例:

export class Observer {
  value: any;    //用户提供的`data`
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value    //保存 data
    this.dep = new Dep()    //dep 实例
    this.vmCount = 0
    def(value, '__ob__', this)    //用 vm.$data.__ob__ 存储当前`observer`实例
    if (Array.isArray(value)) {
            //如果 value 是数组,添加修改后的 mutators 方法到原型或者数组本身上面
      if (hasProto) {
        protoAugment(value, arrayMethods)    //添加到原型上
      } else {
        copyAugment(value, arrayMethods, arrayKeys)    //添加到数组本身
      }
            //遍历数组每一项,observe 每一项
      this.observeArray(value)
    } else {
            //如果 value 是对象,遍历对象
      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) {
        //遍历数组的每一项,observe 每一项。
        //并没有将任何一项变为响应式属性,因为数组是用 mutators 方法来通知更新的。
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

观察者在观察一个数据时,会分为对象类型和数组类型这两种情况,进行不同的操作,下面分情况讨论。

1.1.1 observe 对象类型

假如当前观察的是一个对象类型的数据obj:

const obj = {
    num: 1,
    str: 'a'
}

如第1.1节所示,在实例化objObserver时,会执行obj.__ob__.walk(data)来遍历data对象,将data对象的每一个属性变成响应式属性。
假如当前正在执行defineReactive(obj, 'num')obj.num变为响应式属性:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,            //obj.num 属性对应的值
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()    //obj.num 属性对应的 Dep 实例

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 保存初始 getter/setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
    //继续观察 obj.num 的值,形成了 observe 方法的递归调用,实现了深度遍历 data 对象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val    //obj.num 的值
      if (Dep.target) {    //如果当前存在目标
        dep.depend()    //给 Dep.target 添加依赖 —— obj.num 属性对应的 dep(闭包中的 dep)
                //如果 obj.num 的值 value 是对象或者数组类型,那么它就有观察者 value.__ob__
        if (childOb) {
          childOb.dep.depend()    //给 Dep.target 添加依赖 —— value.__ob__.dep
                    //如果 value 是数组类型,就给 Dep.target 添加依赖 —— value 的每一项的 __ob__.dep 
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
            ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
            //观察 obj.num 的新值
      childOb = !shallow && observe(newVal)
            //通知更新
      dep.notify()
    }
  })
}

上面提到的依赖主要分为两种:

  1. 属性对应的依赖 dep(在闭包中);
  2. 对象的观察者的 dep 属性。

需要注意的是,对象和数组最开始都是作为父对象的属性,执行过defineReactive方法的,所以它们也有闭包中的dep。对于对象类型和基本类型来说,只需用到闭包中的依赖。

而数组类型则需要用到两种依赖,让我们来看看为什么会这样。

1.1.2 observe 数组类型

回顾 Observer构造函数:

if (Array.isArray(value)) {
    //如果 value 是数组,添加修改后的突变方法到原型或者数组本身上面
  if (hasProto) {
        value.__proto__ = arrayMethods;    //将突变方法添加到原型上
  } else {
    copyAugment(value, arrayMethods, arrayKeys)    //将突变方法添加到数组本身
  }
  this.observeArray(value)
}

我们来看arrayMethods是一个什么样的原型:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)    //相当于 arrayMethods = {}; arrayMethods.__proto__ = arrayProto;
//突变方法,也就是会改变数组本身的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  //缓存初始方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    //调用 array.__ob__.dep.notify() 方法来通知更新
    ob.dep.notify()
    return result
  })
})

数组类型的响应式属性需要用到两种依赖:直接给数组赋一个新值,需要用闭包中的依赖来通知更新;
如果调用 mutators 方法,则需要用array.__ob__.dep依赖来通知更新。

总的来说,Vue是使用数据劫持来收集依赖和通知更新的,数据劫持的两种方法:

  1. Vue3.0 之前使用 ES5 就支持的 Object.defineProperty 来实现数据劫持,优点是无须 polyfill,缺点是无法监听对象添加、删除属性的动作,对数组基于下标的修改、对于 .length 修改等;
  2. Vue3.0 版本将使用 es6proxy 来代替 Object.defineProperty,优点是 proxy 的强大功能可以监听几乎所有的动作,还能支持监听 MapSetWeakMapWeakSet 等类型的数据(简直完美),

缺点是浏览器的支持程度不及 Object.definePropertyie 浏览器完全不支持 proxy)。

1.2 Dep 类

上面提到的依赖都是 Dep 类的实例,Dep类的定义如下:

let uid = 0

//Dep 实例是一个可被订阅的对象
export default class Dep {
  static target: ?Watcher;    //静态属性 Dep.target 表示当前目标,它是一个 Watcher 实例
  id: number;
  subs: Array;    //订阅者数组,订阅者为 Watcher 实例
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
    
    //为目标 Dep.target 收集依赖
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
    
    //通知 subs 中的订阅者们更新
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Vueconfig.asyncfalse时,会先对subs进行排序,再执行watcher.update,这是为了保证watchers订阅者们run方法的执行顺序;而如果config.asynctrue的话,订阅者们的run方法是在下一次事件循环执行的,而且在执行前会排一次序,所以就没必要在notify方法中排序了。

需要注意的是,Vue2.0+已经移除了Vue.config.async配置项,也就是说虽然代码中任有余留,但用户已经无法对它进行配置了,所以我们在阅读Vue源码时可以选择性忽略!config.async

Vue中,一个时间点只能存在一个目标Dep.target,其相关操作也是全局的:

Dep.target = null
const targetStack = []    //target 保存在栈中

//target 入栈,Dep.target 始终为栈顶 target
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

//target 出栈,Dep.target 始终为栈顶 target
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

1.3 Watcher 类

Watcher实例大致可分为三种类型:render Watcheruser Watchercomputed Watcher
不同类型的Watcher实例,具有不同的作用,但是所有的Watcher实例都可以收集依赖。
Watcher类的基本定义:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  user: boolean;    //user Watcher 的 flag
  lazy: boolean;    //computed Watcher 的 flag
  active: boolean;
  deps: Array;    //Dep 数组,收集的依赖
  getter: Function;    //收集依赖、求值的方法
  value: any;
    ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean        //render Watcher 的 flag
  ) {
        //一系列初始化操作
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this    //render Watcher 保存于 vm._watcher 属性
    }
        //vm._watchers 数组用于存储组件实例 vm 的所有 watchers
    vm._watchers.push(this)
    // 用户和 vue 提供的选项
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      ...
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.deps = []
    ...
    if (typeof expOrFn === 'function') {
            //render watcher 的 getter 通常为 updateComponent 方法,详见 Vue 的生命周期详解
      this.getter = expOrFn
    } else {
      //初始化 user watcher 的 getter
            ...
    }
        //在构造函数的最后,执行 this.get(),首次收集依赖。所以说在 new Watcher 时,马上就会收集一次依赖。
    this.value = this.lazy ? undefined : this.get()
  }
    
    //添加依赖 dep 到 newDeps 中(newDeps 用于过渡,之后会添加到 deps 中)
    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)
        }
      }
    }
}

Watcher实例主要是用原型方法get来收集依赖的:

Watcher.prototype.get = function () {
    //第一步,确定目标
    //pushTarget 的定义见第 1.2 节末尾,此时 Dep.target = this
  pushTarget(this)
  let value
  const vm = this.vm
  try {
        //第二步,为目标收集依赖
        //忽视参数 vm,执行了 this.getter() 收集依赖。对于不同类型的 watcher,它们的 getter 不尽相同
    value = this.getter.call(vm, vm)
  } catch (e) {
    ...
  } finally {
    ...
        //如第 1.2 节所示,this 出栈,此时 Dep.target = Previous Target
    popTarget()
        //先清空 deps,然后 deps = newDeps
    this.cleanupDeps()
  }
  return value
}

Dep实例dep执行dep.notify方法通知订阅者更新时,每个订阅者都会执行update方法进行更新:

Watcher.prototype.update = function () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
        //将当前 Watcher 实例添加到队列中
    queueWatcher(this)
  }
}

如果不是lazy或者sync类型,update方法会执行了queueWatcher(this)

var queue = [];    //存储所有组件的所有需要更新的 watcher

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
            //如果任未执行 flushSchedulerQueue,就直接添加该 watcher
      queue.push(watcher);
    } else {
            //如果正在执行 flushSchedulerQueue,就根据 watcher.id 将其插入 queue 中;
            //如果目前已经执行到它后面的 watcher,虽然任会被插入 queue 中,但是不会在此次事件循环执行了
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // queue the flush
    if (!waiting) {
      waiting = true
            //之前就说过了,Vue2.0+ 可以忽略 !config.async,所以 waiting 同样意义不大
      if (!config.async) {
        flushSchedulerQueue();
        return
      }
            //在下一事件循环执行 flushSchedulerQueue
      nextTick(flushSchedulerQueue);
    }
  }
}

flushSchedulerQueue方法会根据id排序queue中的所有watcher,然后执行它们的run方法,详见Vue源码或者Vue的生命周期详解。
watcher.run方法的定义:

Watcher.prototype.run = function () {
  if (this.active) {
        //在 this.get 中执行 this.getter 收集依赖,并返回新的结果
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may have mutated.
      isObject(value) ||
      this.deep
    ) {
      //缓存旧值,保存新值
      const oldValue = this.value
      this.value = value
      if (this.user) {
                //如果是 user watcher
        try {
                    //调用 this.cb 回调
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
                //如果不是 user watcher,this.cb 通常为 ()=>{}
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

2 流程

接下来我们分别分析三种Watcher实例(render Watcheruser Watchercomputed Watcher)实现响应式的原理。

2.1 render Watcher

每个组件实例有且仅有一个render Watcher,保存于vm._watcher属性中。它的主要作用是在依赖改变时重新生成组件的虚拟Dom,并更新到HTML中。

我们首先假设有如下两个文件:

main.js:
new Vue({  // 根组件
  render: h => h(App)
})

---------------------------------------------------

app.vue:

export default {  // app组件
    name: 'app',
  data() {
    return {
      obj: {
        num: 1,
        str: 'a'  // 即使是响应式数据,没被使用就不会进行依赖收集
      }
    }
  }
}

回顾之前的内容,在初始化app实例时,忽视无关的步骤,会依次执行 app._init() => initState(app) => initData(app) => observe(data, true) => new Observer(data) => app.$data.__ob__.walk(data)
其中dataapp.$data都是用户提供的data选项。

回顾第1.1节,在app.$data.__ob__.walk(data)方法中会执行defineReactive方法将data对象的所有属性变成响应式属性,在这个例子中,响应式属性包括objobj.numobj.str
这样,在访问这些响应式属性时就会触发其getter方法从而收集依赖,在修改这些响应式属性的值时就会触发其setter方法从而通知更新。接下来我们看render Watcher是在什么时候收集依赖和更新的。

2.1.1 收集依赖

回顾Vue的生命周期详解,在执行mountComponent方法挂载组件时,在执行beforeMount钩子和mounted钩子之间,会实例化render Watcher

new Watcher(vm, updateComponent, noop, {
    before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate');
        }
    }
}, true /* isRenderWatcher */);

从上可以看出render Watchergetter就是updateComponent方法。回顾第1.3节中Watcher类的构造方法,render Watcher会被存储于app._watcher中,
在构造函数的最后会执行app._watcher.get方法,此方法令 Dep.target = app._watcher并执行app._watcher.getter方法。

在执行getter方法,即updateComponent方法的过程中,会执行app._render方法:

const updateComponent = function () {
    app._update(app._render(), hydrating);
};

在执行app._render()时会执行app组件的render函数,在这个例子中,app 组件的 render 函数如下:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", { attrs: { id: "app" } }, [
    _c("div", [_vm._v(_vm._s(_vm.obj.num))])    //此处访问了 app.obj 以及 app.obj.num 属性
  ])
}

值得注意的是,在这个过程中我们访问了app.obj以及app.obj.num属性,而且一个重要的前提是当前的目标 Dep.target = app._watcher(你在其他什么乱七八糟的地方也有可能访问app.obj.num属性,但是你没有目标的话你就无法给目标添加依赖)。

现在,我们访问了app.obj以及app.obj.num属性,那么就会调用它们的getter方法。

  1. 回顾第1.1.1节,在响应式属性的getter方法中会调用dep.depend()方法。
  2. 回顾第1.2节中的depend方法的定义,dep.depend()方法会为目标Dep.target添加app.objapp.obj.num属性的依赖,也就是调用Dep.target.addDep(objDep)Dep.target.addDep(numDep)
  3. 回顾第1.3节中的addDep方法的定义,Dep.target.addDep(dep)方法不仅会将依赖objDepnumDep添加到依赖数组Dep.target.deps中,为了保持一致,还会调用dep.addSub(Dep.target)方法。
  4. 回顾第1.2节中的addSub方法的定义,dep.addSub(Dep.target)方法会将Dep.target添加到依赖objDepnumDep的订阅者数组dep.subs中。

至此,app._watcher收集依赖objDepnumDep,以及objDepnumDep收集订阅者app._watcher的过程结束。

接下来,只要app._watcher.deps中的任意依赖对应的响应式属性发生改变,就会通知app._watcher进行更新。

2.1.2 通知更新

我们假设有如下created钩子和mounted钩子:

app.vue:
export default {  // app组件
  data() {
    ...
  },
    created() {
        this.obj.num = 2;
    },
    mounted() {
        this.obj.num = 3;
    }
}

created钩子是用于混淆的,上文已经说过,render Watcher是在beforeMount钩子和mounted钩子之间实例化的,实例化时会马上收集一次依赖。所以在执行created钩子时,虽然也会执行app.obj.num属性的setter方法,但是此时它的依赖的订阅者数组subs为空,所以没有订阅者更新。

我们在mounted钩子中修改了app.obj.num属性的值,所以会调用它的setter方法。

  1. 回顾第1.1.1节中的setter方法的定义,它会执行app.obj.num属性对应的依赖numDepnotify方法来通知更新。
  2. 回顾第1.2节中的notify方法的定义,numDep.notify方法会依次执行numDep的订阅者数组subs中的所有watcherupdate方法。在本例中,numDep.subs中只有app._watcher
  3. 回顾第1.3节中的update方法的定义,app._watcher.update方法会将app._watcher添加到队列queue中,等到下一次事件循环再执行app._watcher.run方法。
  4. 回顾第1.3节中的run方法的定义,执行app._watcher.run时,会执行app._watcher.get方法。
  5. 回顾第1.3节中的get方法的定义,执行app._watcher.get时,首先会确定目标,然后会执行app._watcher.getter方法(即updateComponent方法)更新组件。
  6. 没错,接下来又进入了收集依赖的过程。

2.2 user Watcher

user Watcher是指用户自己定义的watcher。假如用户定义了如下所示的app组件:

export default {
    name: 'app',  // app组件
  data() {
    return {
      obj: {
        num: 1,
        str: 'a'
      }
    }
  },
    watch: {
        'obj.num': {
            handler(val) {
                console.log(val)
            }
        }
    },
    created() {
      this.$watch('obj.str', val => {...})
    }
}

使用watch选项或者$watch方法监听一个属性时,最终都会为该属性生成一个对应的user Watcher

从源码来看,在调用initState(app)初始化状态时,如果有watch选项,就会调用initWatch(app, app.$options.watch)初始化watch状态,而在其中又会调用app.$watch(expOrFn, handler, options)生成一个userWatcher实例。
也就是说,watch选项中的属性其实最终也是通过$watch方法来实现监听的。而其中的过程我就不细说了,具体请看

参考链接: Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间

最终生成user Watcher的语句是:

const watcher = new Watcher(vm, expOrFn, cb, options)

我们以obj.num属性的user Watcher为例,上述语句中的参数vm = app实例expOrFn = 'obj.num'cb = val => {console.log(val)}options = {user: true}。回顾Watcher类的构造函数:

export default class Watcher {
  user: boolean;    //user Watcher 的 flag
  value: any;
    ...

  constructor (
    vm: Component,    // app
    expOrFn: string | Function,    // 'obj.num'
    cb: Function,        // val => {console.log(val)}
    options?: ?Object,    // {user: true}
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.user = !!options.user    //true,user Watcher 的 flag
      ...
    }
    this.cb = cb    //val => {console.log(val)},即用户定义的回调
    ...
    if (typeof expOrFn === 'function') {
            ...
    } else {
      //初始化 user watcher 的 getter
            this.getter = parsePath(expOrFn)
            if (!this.getter) {
              this.getter = noop
              ...
            }
    }
        //在构造函数的最后,执行 this.get() 收集依赖。
    this.value = this.lazy ? undefined : this.get()
  }
}

执行parsePath('obj.num'),来获取user Watchergetter

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
    //如果包含不正确的字符,直接返回
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
    //返回的方法就是 getter 方法
  return function (obj) {
        //循环赋值 obj,深度遍历路径,返回最终的结果
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

回顾第1.3节中的get方法的定义,在之后调用getter方法时会传入app实例作为实参,所以在getter方法中,刚开始obj = app
第一次循环,obj = app.obj;第二次循环,obj = app.obj.num,最终返回 app.obj.num

2.2.1 收集依赖

user Watcherrender Watcher收集依赖过程的区别在于watcher.getter方法不同:

  1. user Watcherrender Watcher一样,会在new Watcher()的最后执行userWatcher.get方法;
  2. 执行userWatcher.get时,首先会确定目标Dep.target = userWatcher,然后会执行userWatcher.getter方法,传入app实例作为实参;
  3. 观察上述userWatcher.getter方法,可知先访问了属性app.obj,再访问了属性app.obj.num,所以会依次调用这两个属性的getter方法;
  4. 接下来与render Watcher收集依赖的过程类似,就不细说了。

接下来,只要userWatcher.deps中的任意依赖对应的响应式属性发生改变,就会通知userWatcher进行更新。

2.2.2 通知更新

user Watcher通知更新的过程和render Watcher略有不同,区别在于user Watcher最后会执行用户定义的回调函数。例子不变:

app.vue:
export default {  // app组件
  data() {
    ...
  },
    mounted() {
        this.obj.num = 3;
    }
}

我们在mounted钩子中修改了app.obj.num属性的值,所以会调用它的setter方法。

  1. setter方法中会调用app.obj.num属性对应的依赖numDepnotify方法来通知更新。
  2. numDep.notify方法会依次执行numDep的订阅者数组subs中的所有watcherupdate方法。结合上一节,目前,numDep.subs中包含userWatcherrenderWatcher这两个订阅者,其中userWatcher先生成。
  3. userWatcher.update方法会将userWatcher添加到队列queue中,等到下一次事件循环再执行userWatcher.run方法。
  4. 执行userWatcher.run时,首先会执行userWatcher.get方法;执行userWatcher.get时,首先会确定目标,然后会执行userWatcher.getter方法收集依赖,并且获得app.obj.num属性的新值,新值将从getter方法一直返回到run方法中。
  5. 回顾第1.3节中的run方法的定义,回到run方法中后,接下来会缓存旧值,保存新值,将新值和旧值作为实参传入userWatcher.cb方法中,并执行userWatcher.cb方法,即用户定义的回调函数。

回顾Vue的生命周期详解,之所以说userWatcherrenderWatcher先生成,是因为initState(app)是在beforeCreatecreated钩子之间执行的,
所以userWatcher也是在这之间实例化的;而前面已经说过,renderWatcher是在beforeMount钩子和mounted钩子之间实例化的,所以显而易见userWatcherrenderWatcher先生成。

2.3 computed Watcher(lazy Watcher)

computed Watcher是指用户定义的计算属性所对应的watcher。假如用户定义了如下所示的app组件:

export default {
    name: 'app',  // app组件
  data() {
    return {
      obj: {
        num: 1,
        str: 'a'
      }
    }
  },
    ...
    computed: {
        doubleNum() {
            return this.obj.num * 2;
        }
    }
}

使用computed选项定义一个计算属性时,会为该计算属性生成一个对应的computed Watcher

从源码来看,在调用initState(app)初始化状态时,如果有computed选项,就会调用initComputed(app, app.$options.computed)初始化computed状态:

function initState (vm) {
  vm._watchers = [];    //app 实例的所有 watcher 的集合
  var opts = vm.$options;    //app 实例的选项
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
    //初始化 data
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
    //初始化 computed(先于 initWatch)
  if (opts.computed) { initComputed(vm, opts.computed); }
    //初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

结合上一节和initState方法的定义,相信聪明的你已经发现,在不考虑$watch方法的情况下,同一组件中三种类型的watcher实例的生成顺序为:

computed Watcher => user Watcher => render Watcher

watcher实例的生成顺序也就是它们的watcher.id的大小顺序(从小到大)。回顾Vue的生命周期详解,在下一事件循环执行flushSchedulerQueue方法时,
会根据watcher.idqueue中的watcher进行排序,然后再执行这些watcherrun方法。
所以queue中同一组件watcherrun方法的执行顺序为(不考虑$watch生成的user Watcher和其它组件的watcher):

user Watcher => render Watcher

因为computed Watcher不会被加入queue中,也不是通过watcher.run方法来更新的,所以我们忽略它。而user Watcher先于render Watcher执行显然是符合我们期望的。

回归正题,在initComputed方法中:

function initComputed(vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象存储 computedWatchers
    
  for(const key in computed) {
    const userDef = computed[key];    //用户的定义
    const getter = typeof userDef === 'function' ? userDef : userDef.get;  // 计算属性 key 对应的回调函数
        
    watchers[key] = new Watcher(vm, getter, noop, {lazy: true})  // 实例化 computed-watcher
    ...
  }
}

我们以计算属性doubleNum为例,回顾Watcher类的构造函数:

export default class Watcher {
  constructor (
    vm: Component,    //app
    expOrFn: string | Function,    //用户定义的回调函数,function() { return this.obj.num * 2; }
    cb: Function,        //_ => {}
    options?: ?Object,    //{lazy: true}
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.lazy = !!options.lazy    // true,computed Watcher 的 flag
      ...
    }
        this.dirty = this.lazy    // 表示是否需要对 computed 属性进行计算
    ...
    if (typeof expOrFn === 'function') {
            this.getter = expOrFn
    }
        // 不执行 this.get()
    this.value = this.lazy ? undefined : this.get()
  }
}

实例化computed Watcher后,回到initComputed方法中:

function initComputed(vm, computed) {
  ...
  for(const key in computed) {
    const userDef = computed[key];    //用户的定义
    ...
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    } else {
      if (key in vm.$data) {    // key 不能和 data 里面的属性重名
        warn(("The computed property \"" + key + "\" is already defined in data."), vm);
      } else if (vm.$options.props && key in vm.$options.props) {    // key 不能和 props 里面的属性重名
        warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
      }
    }
  }
}

接下来调用defineComputed方法定义计算属性:

function defineComputed (target, key, userDef) {
    //在 SSR 中,计算属性不缓存
  var shouldCache = !isServerRendering();
  if (typeof userDef === 'function') {
        //创建计算属性的 getter,存于属性描述符中
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  }
    //创建响应式的计算属性 target.key
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

同样以计算属性app.doubleNum为例,在defineComputed方法中,首先设置了doubleNum的属性描述符(Property Descriptor)对象sharedPropertyDefinitiongetset属性,然后用Object.defineProperty方法在app实例上定义了一个属性doubleNum
这样就为app.doubleNum属性定义了gettersetter方法,实现了数据劫持,详见MDN文档Object.defineProperty()。

2.3.1 收集依赖

SSR中,计算属性不缓存,每次访问属性都会直接调用用户定义的回调函数,但是其他情况下会进行缓存。计算属性的缓存是通过createComputedGetter方法实现的:

function createComputedGetter (key) {
    //返回计算属性的 getter
  return function computedGetter () {
        //首先获取刚才生成的 computedWatcher 实例
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {    //如果需要计算
        watcher.evaluate();    //进行计算
      }
            if (Dep.target) {  // 当前的 watcher,如果是页面渲染时触发的这个方法,Dep.target = render watcher
              watcher.depend()  // 将 computedWatcher 的依赖全部复制给 Dep.target
            }
      return watcher.value
    }
  }
}

---------------------------------------------------------------------------------

Watcher.prototype.evaluate = function () {
    this.value = this.get()    // 计算属性求值
    this.dirty = false    // 表示计算属性已经计算,不需要再计算
}

Watcher.prototype.depend () {
  let i = this.deps.length  // computedWatcher.deps 的长度
  while (i--) {
    this.deps[i].depend()  // 收集依赖
  }
}

createComputedGetter方法中对用户定义的回调函数(存于computedWatcher.getter)进行了封装,最终返回了计算属性的getter方法。

以计算属性app.doubleNum为例,在定义了它的getter方法后,当用户首次访问doubleNum属性,调用它的getter方法时,会进行第一次依赖收集:

  1. doubleNum属性的getter方法中,首先获取相应的computedWatcher实例。回顾第2.3节中computedWatcher的实例化过程,可知computedWatcher.getter = function() { return this.obj.num * 2; } ,即用户定义的回调;
  2. 如果属性需要进行计算,就执行computedWatcher.evaluate方法,在其中执行computedWatcher.get方法收集依赖,并且设置计算标识dirtyfalse,表示已经计算过了;
  3. 在执行computedWatcher.get方法时,首先会确定目标Dep.target = computedWatcher,然后会执行computedWatcher.getter方法(即用户定义的回调函数)来获取计算属性的当前值;
  4. 观察用户定义的回调函数,可知它执行时先访问了属性app.obj,再访问了属性app.obj.num,所以会依次调用这两个响应式属性的getter方法添加依赖,添加依赖的具体步骤见第2.1.1节;
  5. 然后回到computedWatcher.get方法中,执行popTarget()方法,令 Dep.target = Previous TargetPrevious Target就是之前的watcher
    简单来说,如果用户用$watch方法监听了一个计算属性,那么在访问这个计算属性时(即调用属性的getter方法时),当前target栈为[..., userWatcher, computedWatcher]。在收集完computedWatcher的依赖之后,
    就会执行popTarget()方法将computedWatcher出栈,所以当前的栈顶元素为userWatcherDep.target = userWatcher
  6. 回到doubleNum属性的getter方法中,如果Dep.target(在computedWatcher前面的watcher)不为空,就执行computedWatcher.depend方法,该方法依次执行computedWatcher收集的依赖的depend方法;
  7. 调用Dep.prototype.depend方法为当前的Dep.target添加依赖,具体步骤见第2.1.1节;
  8. 最终回到doubleNum属性的getter方法中,返回计算属性的当前值给用户。

接下来,只要computedWatcher.deps中的任意依赖对应的响应式属性发生改变,就会通知computedWatcher进行更新。

2.3.2 通知更新

例子如下:

app.vue:
export default {  // app组件
  data() {
    ...
  },
    mounted() {
        this.doubleNum;    //先访问一次添加依赖
        this.obj.num = 3;
    }
}

我们在mounted钩子中修改了app.obj.num属性的值,所以会调用它的setter方法。

  1. setter方法中会调用app.obj.num属性对应的依赖numDepnotify方法来通知更新。
  2. numDep.notify方法会依次执行numDep的订阅者数组subs中的所有watcherupdate方法。目前,numDep.subs中包含userWatcherrenderWatchercomputedWatcher
  3. 回顾第1.3节中的update方法的定义,computedWatcher.update方法会将计算标识computedWatcher.dirty设为true,表示下一次访问该属性时需要重新进行计算。

总的来说,computed属性是一种惰性求值(延迟求值)属性,这种属性不是在被赋予表达式时求值,而是在被访问时求值。另外计算属性的值会被缓存,极大地提升了性能。
简单来说,计算属性不访问就不会计算,访问了就会计算并缓存,等依赖更新后就会将计算标识赋值为true,等下一次访问时会重新计算并缓存。

3 总结:

  1. 收集依赖总是从调用watcher.get方法开始,在watcher.get方法中调用watcher.getter方法时会访问响应式属性,调用响应式属性的getter方法,从而给Dep.target添加依赖(同时给依赖添加订阅者),而调用watcher.get方法收集依赖的时机有:

    1. for render Watcher & user Watcher: watcher实例化时会在构造函数的最后调用watcher.get方法;watcher更新后,下一次事件循环watcher执行run方法时会调用watcher.get方法;
    2. for computed Watcher: 首次访问计算属性,会调用计算属性的getter方法,从而调用watcher.get方法;watcher更新后,下一次访问计算属性时会调用watcher.get方法;
  2. 通知更新:

    1. 从给任意类型的响应式属性设置新值开始,调用响应式属性的setter方法,在setter方法中调用属性所对应的依赖的notify方法,通知该依赖的所有订阅者进行更新,然后在下一次事件循环,执行所有订阅者的watcher.run方法;
    2. 从调用数组类型的响应式属性的突变方法开始,在突变方法中调用array.__ob__.dep依赖的notify方法,通知该依赖的所有订阅者进行更新,然后在下一次事件循环,执行所有订阅者的watcher.run方法。

对于render Watcheruser Watcher,再次收集依赖都是调用watcher.run方法来实现的,而watcher.run方法除了收集依赖这个副作用外,它的主要作用还有:

  1. for render Watcher: 在watcher.get方法内部调用watcher.getter方法实现组件的更新(首先生成组件的VNode,然后直接进行首次渲染或者diff后进行再次渲染);
  2. for user Watcher: 首先在watcher.get方法内部调用watcher.getter方法获取所监听属性的新值,然后回到watcher.run方法中,将属性的新值与旧值作为参数一起传入watcher.cb(即用户定义的回调)中并执行。

你可能感兴趣的:(javascript,vue.js)