Vue2源码解析☞ 3 ☞ 响应式机制

780 阅读9分钟

雨露同行风雨同舟.jpg

活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。

引言

Vue源码解析·初始化章节中,在初始化的过程中曾多次提到对state属性的响应式处理。

Vue是如何收集依赖的?

Vue是如何通知更新的?

对象和数组分别是如何进行深层响应式处理的?

嵌套对象是如何进行浅层响应式处理的?

上面这些问题,都将在本章通过源码一一解释。

响应式机制概述

data.png

上图来自vue.js官方文档

几个重要概念

Observer

在初始化的过程中,Observer会对对象和数组分别执行不同的响应式处理。

对象通过ES5 API为对象的每一个属性定义数据劫持。

数组通过原型链劫持或定义隐藏属性来实现数据劫持。

Dep

在观察者模式中,Dep是发布者,负责将数据变化的消息通知给所有的Watcher

Watcher

在观察者模式中,Watcher是订阅者,收到Dep的通知后,执行update,重新渲染。

简述响应式流程

  • 在Vue初始化过程中,为每一个属性key定义setter和getter。

  • 当页面初次渲染时,执行渲染函数,访问属性时,会触发setter,从而收集依赖,将Dep和Watcher关联起来。

  • 当属性key对应的值变化时,会触发setter,从而通过Dep通知Watcher执行update。

  • 之后,重新渲染页面,

源码解析

以data中的属性为例,来看看data中的属性是如何一步步实现响应式处理的,数据变化后又是如何更新的。

测试案例

编写一个reactive.html文件,内容如下:

<!DOCTYPE html>
<html>
<head>
    <title>Vue源码剖析</title>
    <script src="../../dist/vue.js"></script>
</head>
<body>
    <div id="demo">
        <h1>数据响应化</h1>
        <p>{{face.description}}</p>
        <button @click="care">关心一下</button>
    </div>
    <script>
        const app = new Vue({
            el: '#demo',
            data: {
                face: {
                    description: '面无表情'
                }
            },
            methods: {
              care(){
                this.face.description = '你笑起来真好看'
              }
            }
        });
    </script>
</body>
</html>

在浏览器中打开reactive.html,打开调试器,让调试器获得焦点,使用快捷键Ctrl + P,打开搜索框,键入reactive.html即可看到我们编写的代码。

然后在new Vue行打上断点,刷新页面即可调试Vue源码。

Observe阶段:定义属性的数据劫持

首先通过快捷键Ctrl + P打开调试器搜索框,键入src/core/instance/init.js打开文件,定位到initState(vm)行,打上断点,让代码运行到此处。

然后,步入方法initState内,代码运行到initData(vm)处,开始初始化data选项,执行的操作如下:

  • 获取用户编写的data选项,并挂载到vm的_data属性上
  • 检查data选项是否能得到一个普通的对象,即其对象的toString结果是'[object Object]'
  • 依次检查data中的每个属性key是否在methods、props中使用过,如果使用过则报错
  • 检查data的每一个key是否以_或$为前缀,如果不是,则在vm上设置对于_data中key的代理(这也是我们可以通过this[key]的原因,本质上访问的还是this[_data][key]
  • 此时,对data进行observe处理

observe方法中,最核心的代码就是ob = new Observer(value)

if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__         //如果value中包含__ob__属性,表明已经observe过
  } else if (
    shouldObserve &&          //用于控制特定的数据是否对其进行Observe(将在props,provide,inject,computed等选项的初始化详细说明)
    !isServerRendering() &&   //非服务端渲染
    (Array.isArray(value) || isPlainObject(value)) &&   //Vue只对数组和普通对象进行observe
    Object.isExtensible(value) &&              // ES5 的API,判断一个对象是否可扩展
    !value._isVue  //_isVue属性是在_init方法中挂载到vm上的,说明不对vue实例进行observe
  ) {
    ob = new Observer(value)
  }

下面我们来看,Observer的实例化,源码如下:

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
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  //被observe的对象或数组
    this.dep = new Dep()  //用于收集依赖
    this.vmCount = 0 //该对象或数组被多少vue实例挂载为$data
    def(value, '__ob__', this) //当当前Observer实例作为value的__ob__属性
    if (Array.isArray(value)) {
      if (hasProto) {
        // 通过__proto__属性拦截value的原型链,概念比较抽象,详细操作可看具体源码
        protoAugment(value, arrayMethods) 
      } else {
        // 给value定义隐藏属性
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) //对value的每一个元素进行observe,代码很简单
    } else {
      this.walk(value) //遍历value的每一个属性,并执行响应式处理
    }
  }

在此方法中,做了如下事情:

  • 给Observer实例定义了value属性(也就是被observe的对象或数组)
  • 给Observer实例定义了dep属性,用于收集依赖
  • 给Observer实例定义了vmCount,类似于windows变成中进程的引用计数
  • 针对数组,拦截原型链或定义隐藏属性,对每一个元素进行observe
  • 针对对象,执行了walk方法

walk方法的源码,逻辑很简单,将对象的所有属性执行defineReactive

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) //最核心的代码:实现响应式的基础
    }
  }

到此,进入了关键代码段,执行defineReactive方法:

  • 首先创建了一个Dep实例:const dep = new Dep()
  • 如果被处理的key的configurable是false,则不执行处理
  • 对key对应的val进行预处理,如果不是浅层响应式处理,则对val进行递归式observe
  • 定义getter和setter(数据拦截),源码如下:
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () { //定义对应key的取值器
      //略
    },
    set: function reactiveSetter (newVal) { //定义对应key的存值器
      //略
    }
  })

注意事项:此处只是进行了定义,代码并没有执行,此时还处于initState阶段,处于beforeCreate和created之间,详细代码不做介绍,代码执行时结合实例详细说明

初始化render watcher阶段:触发首次挂载

响应式.png

结合上图的调用堆栈说明:

  • (匿名):html中的脚本调用
  • VueVue构造函数执行
  • Vue._init_init方法执行,created钩子及其之前的初始化,包含provide,inject,data,props,computed,watch等用户代码的初始化。
  • Vue.$mount$mount扩展部分的执行,包含对render、template和el的处理。
  • Vue.$mount$mount初始定义函数的执行,核心就是调用mountComponent方法
  • mountComponent:在此方法内,创建了当前Vue组件实例的render watcher

下面我们看render watcher的实例化过程:

render watcher的构造参数

  // src/core/instance/lifecycle.js
  
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  • 参数1:vm —— Vue组件实例
  • 参数2:expOrFn 表达式或函数, 此处传入的是updateComponent —— 用于执行实例的渲染和挂载
  • 参数3:cb 回调函数,此处的noop表示什么都不执行,no operation
  • 参数4:options 包含deepuserlazysyncbefore,在Vue中共有三种watcher,分别为render watcher, computed watcher,watch watcher(将在专门的章节中详细说明)
  • 参数5:isRenderWatcher,此处传入true,表明是一个render watcher

watcher实例化细节

  • 绑定组件实例:this.vm = vm,意味着通过watcher可以访问其所属的vue实例
  • 如果是render watcher,则将当前watcher实例挂载到vm的_watcher属性上
    if (isRenderWatcher) {
      vm._watcher = this
    }
  • 将当前watcher实例保存在vm_watchers属性中,_watchers属性中保存了当前组件实例中所有的watcher(包含三种watcher
  • 根据options初始化相应的属性
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
  • 部分watcher实例属性的初始化,此处不做详解
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
  • 处理expOrFn,render watcher的实参是updateComponent,此处将updateComponent赋值给watchergetter属性(很重要
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn //updateComponent是在创建render watcher之前定义的函数
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
  • watchervalue属性赋值,对于render watcheroptions中只传了before,也就是说lazy属性是false,所以会执行watcher实例的get方法(很重要)
this.value = this.lazy
      ? undefined
      : this.get()

很难理解的一步:如何触发了组建的渲染和更新

在上面watcher实例的最后一行代码,调用了get方法,下面看get的源码:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm //当前watcher实例所属的vue实例
    try {
      value = this.getter.call(vm, vm) //关键代码
    } catch (e) {
      //略
    } finally {
     //略
    }
    return value
  }

第一行代码:pushTarget(this):本质上是将当前watcher实例赋值给Dep.target,在收集依赖的时候,Dep.target必须有值

上面提到过,render watcher实例的getter属性本质上是updateComponent,所以value = this.getter.call(vm, vm),实际上就是执行vue实例的updateComponent

相关源码:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  • vm._render():执行vue实例的渲染,返回vue实例的vnode(即虚拟DOM)
  • vm._update():执行挂载

其调用堆栈如下:

watcher.png

执行顺序依次为:

  • Watcher: watcher的实例化
  • get: 对于render watcher来说,给value属性赋值本质上就是调用了get实例方法
  • updateComponent:在上面的get方法中执行了getter函数,即updateComponent函数(对于render watcher来说)

依赖收集阶段

dep.png

通过调用堆栈图,看从updateComponent到开始收集依赖是怎么运行的:

  • updateComponent: 在updateComponent函数内部调用了vm._update方法,该方法的第一个参数是vm._render函数的返回结果(其实就是vm对应的虚拟DOM)
  • Vue._render:执行了渲染函数,并返回虚拟DOM
  • (匿名):组件实例对应的渲染函数的执行,渲染函数代码举例:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("数据响应化")]),
_v(" "),_c('p',[_v(_s(face.description))]),_v(" "),
_c('button',{on:{"click":care}},[_v("关心一下")])])}
})

解释: _cvm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

(插曲)Vue内置的渲染函数声明(有兴趣的可以自行研究一下)

  // flow/component.js

  // _c is internal that accepts `normalizationType` optimization hint
  _c: (
    vnode?: VNode,
    data?: VNodeData,
    children?: VNodeChildren,
    normalizationType?: number
  ) => VNode | void;

  // renderStatic
  _m: (index: number, isInFor?: boolean) => VNode | VNodeChildren;
  // markOnce
  _o: (vnode: VNode | Array<VNode>, index: number, key: string) => VNode | VNodeChildren;
  // toString
  _s: (value: mixed) => string;
  // text to VNode
  _v: (value: string | number) => VNode;
  // toNumber
  _n: (value: string) => number | string;
  // empty vnode
  _e: () => VNode;
  // loose equal
  _q: (a: mixed, b: mixed) => boolean;
  // loose indexOf
  _i: (arr: Array<mixed>, val: mixed) => number;
  // resolveFilter
  _f: (id: string) => Function;
  // renderList
  _l: (val: mixed, render: Function) => ?Array<VNode>;
  // renderSlot
  _t: (name: string, fallback: ?Array<VNode>, props: ?Object) => ?Array<VNode>;
  // apply v-bind object
  _b: (data: any, tag: string, value: any, asProp: boolean, isSync?: boolean) => VNodeData;
  // apply v-on object
  _g: (data: any, value: any) => VNodeData;
  // check custom keyCode
  _k: (eventKeyCode: number, key: string, builtInAlias?: number | Array<number>, eventKeyName?: string) => ?boolean;
  // resolve scoped slots
  _u: (scopedSlots: ScopedSlotsData, res?: Object) => { [key: string]: Function };
  • proxyGetter: 我们在模板上使用的都是代理后的属性(即可以直接通过this访问的),然后再访问vm._data中的属性(即用户编写的data选项)
  • reactiveGetter:也就是在defineReactive方法中,给key定义的get函数,在渲染函数中通过访问代理属性,间接触发

reactiveGetter:对指定属性的访问进行劫持,将watcher、dep、vm、state属性(此处是data属性)关联起来

注意事项:暂且不考虑数组,嵌套对象的依赖收集,保持一个低开高走的姿态

  • Dep.target:在render watcher实例化过程中,调用get方法的第一步就是pushTarget方法,本质上就是将render watcher的实例赋值给Dep.target,紧接着依次执行vm._render, 匿名渲染函数 ,proxyGetter, reactiveGetter
  • dep:是在定义数据劫持阶段创建的,此处形成了闭包。此处的dep和key是一对一的关系,dep实例上有一个subs属性,可以保存多个watcher实例(即访问key的vm对应的render watcher),前提:仅考虑单层属性的对象,而不考虑嵌套属性。
    get: function reactiveGetter () {
      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
    },

dep.depend:其实执行的是Dep.target.addDep(this)

Dep.target是包含挂载当前key属性的vm对应的render watcher

Dep实例有一个id属性,该属性是在定义数据劫持阶段赋值的,先遍历到的属性,对应的id值越大,跟data选项中的顺序有关。但是收集依赖的顺序,跟渲染函数的访问顺序有关,也就是我们经常编写的template中的先后编写顺序有关。

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

下面看watcher又是如何处理传入的Dep实例的

Watcher实例的addDep方法

注意事项:此方法是通过watcher实例来调用,一定要明确this指的是什么

  • newDepIds: 用来判断一个dep是否处于一个render watchernewDeps数组中。一个render watcher对应一个vue组件实例,一个dep姑且代表一个data属性key,一个组件实例对应多个data选项中的属性,因此一个watcher对应多个dep
  • depIds:用来判断一个watcher是否存在于dep实例的subs数组中。一个vue组件可以被多个页面使用,即有多个实例,多个render watcher,因此一个dep对应多个watcher。

综合以上两点,watcherdep是多对多的关系。

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

到此,依赖收集简单介绍完了,对于深层嵌套属性的依赖收集,会在响应式流程走完后再做补充。

通知更新

当vm实例的data选项属性发生变化,是如何通知渲染的,先来看一个堆栈调用图

update.png

当我们点击一个按钮,触发一个事件,修改一个属性的值,然后触发了该属性的修改器,也就是reactiveSetter函数。

reactiveSetter

修改器中的两个关键点:

  • shalow:在初始化阶段形成闭包,其值为undefined,如果对应key的值修改后是一个对象,则对其进行深层响应式处理。
  • depkey是一对一的关系,所以通知更新应该始于dep
    set: function reactiveSetter (newVal) {
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

dep.notify:dep的通知方法做了什么

当设置key对应的值时,与key关联的dep发出通知,dep的subs数组中保存着使用当前属性的所有render watcher(render watcher和vm是一对一的关系)。通过代码可知,值变化时,会通知该属性对应的dep收集的所有render watcher执行update方法。(???此处有疑问)

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

watcher.update():执行更新

update方法会涉及到lazydirtysync等属性,将会在watcher专题章节做详细说明;queueWatcher将当前watcher实例压入队列,至于后续操作,将会在异步更新机制章节中做详细说明。

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

数组的响应式

测试案例

<!DOCTYPE html>
<html>
<head>
    <script src="../../dist/vue.js"></script>
</head>
<body>
    <div id="demo">
        <h1>面部特征</h1>
        <button @click="showEyes()">抬头</button>
        <li v-for="a in arr" :key="a">
            {{ a }}
        </li>
    </div>
    <script>
        // 创建实例
        const app = new Vue({
            el: '#demo',
            data: { arr: ['双眼皮', '偶尔皱眉'] },
            methods:{
              showEyes(){
                this.arr.push('清澈的眼神')
              }
            }
        });
    </script>
</body>
</html>

调用堆栈

企业数组属性.png

  • Vue:执行构造函数
  • Vue._init:执行初始化方法
  • initState:初始化state属性,包括dataprops
  • initData:初始化data选项
  • observe:此处observevaluedata对象
  • Observer:给data对象创建Observer实例,并遍历内部属性
  • walk:此方法一定会执行,因为data返回的是一个普通对象
  • defineReactive$$1:定义响应式
  • observe:由于shallowundefined,所以无论key对应的值是否是对象或数组,都会对其执行observe,由于此处的arr属性的值是一个数组,所以会创建Observer实例,对执行observeArray方法

observeArray方法的代码如下:

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

在此示例中,由于数组的元素都是基本数据类型,所以会执行observe,但不会针对索引定义响应式,那么数组元素的响应式是如何实现的呢?

为数组创建Observer实例时,会执行protoAugment(value, arrayMethods)copyAugment(value, arrayMethods, arrayKeys),对应的源码如下:

if (Array.isArray(value)) {
      if (hasProto) {
        // 通过__proto__属性拦截value的原型链,概念比较抽象,详细操作可看具体源码
        protoAugment(value, arrayMethods) 
      } else {
        // 给value定义隐藏属性
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) //对value的每一个元素进行observe,代码很简单
    }

arrayMethods的值如下图:

methods.png

arrayKeys的值如下图:

methodsName.png

也就是说Vue对数组的这7个方法进行了数据劫持,添加了自己的逻辑,具体逻辑,继续往下看

数组原型链劫持

保存Array的原型:

const arrayProto = Array.prototype

以数组的原型为原型创建一个新对象:

export const arrayMethods = Object.create(arrayProto)

arrayMethods的值如下图:

initial.png

定义打补丁的几个方法:

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

为补丁方法定义劫持逻辑:

请看源码注释

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method] //以push方法为例,就是Array.prototype.push
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args) //执行Array的原型方法,this指向被observe的数组对象
    const ob = this.__ob__ //被定义在数组上的observer实例
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args //向数组中添加的元素
        break
      case 'splice':
        inserted = args.slice(2) //splice有第三个参数时,表示插入元素
        break
    }
    if (inserted) ob.observeArray(inserted) //对添加的数组对象进行observe
    // notify change
    ob.dep.notify() //通知更新
    return result
  })
})

执行完上述代码后,结果如下:

methodsToPatch.png

  • arrayMethods本身就是一个数组,原型自然指向Array.prototype
  • 在为数组创建Observer实例的时候,会判断value是否拥有_proto属性,并进行不同的处理,这意味着Vue并非仅仅处理严格意义上的数组,也可以处理类数组对象。

arrayMethods.png

上图中的arrayMethods就是被observe的数组对象的新原型(定义了7个原型方法)。

  • arrayMethodsmethodsToPatch是在执行Vue构造函数前被定义的
  • 在创建Observer实例的时候劫持原型链(数组)或者定义隐藏属性(类数组对象)
  • 执行特定的7个方法时,执行mutator方法

下面看一下被劫持的数组对象的原型链:

原型链1.png

原型链2.png

被劫持的执行逻辑:

首先建立一组类比:

  • 数组对象(我)
  • 插入的原型(父亲)
  • Array.prototype(祖父)

正常数组对象的执行逻辑(除7个特定方法外):

  • 我想吃糖,我没有,跟父亲要
  • 父亲有就给你,但是也没有,我去问问你爷爷
  • 爷爷有就给你了,没有就说你吃的啥啊?

被Vue劫持的数组对象的执行逻辑(7个特定的方法):

  • 我想吃糖,我没有,跟父亲要
  • 父亲有糖,但是你先去爷爷那儿拿些糖,父亲给你做个冰糖葫芦吧

数组响应式机制总结:

  • 定义数据劫持:对象是通过ES5 API Object.prototype.defineProperty来完成的,数组是通过原型链劫持或者定义隐藏属性来完成的。
  • 依赖收集:Vue会为对象的每个key创建一个dep,而只会为数组对象对应的key创建一个dep(对于数组元素是引用数据类型,则需额外的考虑)。
  • 触发更新:对象是通过setter修改器来触发更新的,数组是通过7个原型方法mutator的执行来触发更新。

嵌套对象响应式的探索

测试案例

<!DOCTYPE html>
<html>
<head>
    <script src="../../dist/vue.js"></script>
</head>
<body>
    <div id="demo">
        <h1>picture</h1>
        <button @click="changeTrousers">换裤子</button>
        <div>皮肤:{{description.head.skin}}</div>
        <div>眼神:{{description.head.eyes}}</div>
        <div>辫子:{{description.head.braid}}</div>
        <div>裤子:{{description.clothes.trousers.names}}</div>
        <div>裤子颜色:{{description.clothes.trousers.color}}</div>
    </div>
    <script>
        // 创建实例
        const app = new Vue({
            el: '#demo',
            data: { 
              description: {
                head: {
                  skin: 'smooth',
                  eyes: 'clean',
                  braid: 'ponytail'
                },
                clothes: {
                  trousers: {
                    name: 'jeans',
                    color: 'blue'
                  }
                }
              }
            },
            methods:{
              changeTrousers(){
                let trousers = this.description.clothes.trousers;
                trousers.name = 'casual';
                trousers.color = 'black';
              }
            }
        });
    </script>
</body>
</html>

src/core/observer/index.jsdefineReactive方法中打印key的值。

先来看data选项中定义数据劫持的顺序:

定义数据劫持的顺序.png

我们会发现,定义数据劫持的顺序与data选项中编写的顺序一致。

defineReactive方法中创建dep的时候,将key作为参数传进去,然后在Dep的构造函数中接受这个key值,并打印出来。请看下图:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep(key)
  console.log('定义数据劫持的key:', key)
}
  constructor (key) {
    this.id = uid++
    this.subs = []
    console.log('key的值:', key)
  }

dep构造函数.png

从图中可以看到,并非所有的dep都是在defineReactive方法中创建的,在Dep的构造函数打印日志的地方打一个条件断点key==undefined,然后在调用堆栈中找到无参的new Dep是在哪儿创建的。

observer.png

从图中可知,在创建Observer实例的时候,在Observer实例上挂载了一个Dep实例,也就是说data选项中,如果一个key对应的值如果是对象或者数组,则会创建两个Dep实例。在初始化data的过程中,创建了两种Dep实例,那么这两种Dep实例的功能有什么不同呢?

首先,在Dep构造函数中为Dep实例添加一个属性,改造代码如下:

  constructor (key) {
    this.id = uid++
    this.subs = []
    this.key = key; //新添加的属性,如果是Observer构造函数中调用,不传key,因此值是undefined
    this.isObserverCreated = !key //新添加的属性
  }

在watcher的addDep方法中打印日志,改造代码如下:

  addDep (dep: Dep) {
    //略
    if (!this.newDepIds.has(id)) {
      //略
      console.log('addDep——isObserverCreated:', dep.isObserverCreated)
      console.log('addDep——key:', dep.key)
      //略
    }
  }

之所以在addDep方法中打印日志,是因为Dep.addDepend方法中没有进行过滤,一个属性在一个vm实例中可能被访问多次,为了避免重复打印一个key的依赖收集过程,将打印日志代码编写在Watcher的addDep方法中(因为有过滤,避免对一个Dep实例重复收集)

保存代码后,会重新生成vue.js(前提是已经执行npm run dev),刷新页面后可以看到如下日志:

依赖收集阶段.png

addDep方法中addDep——key: undefined处打一个条件断点:dep.key==undefined,然后刷新页面,看调用堆栈,如下图:

childOb.png

上图中,通过调用堆栈找到收集依赖的dep,恰好是创建的Observer实例中挂载的dep。

验证我们的猜想:在childOb.dep.depend()代码传参'childOb',透传到addDep方法中,并在addDep方法中将其打印出来,打印日志如下:

yanzheng.png

由图可知,在收集依赖时,凡是在defineReactive方法中创建的Dep实例,调用的都是dep.addDepend方法,凡是在Observer实例化时创建的Dep实例,调用的都是childOb.dep.addDepend方法。

在初始化阶段,Vue只是为data选项中的属性定义了数据劫持,但并没有覆盖对象或数组动态添加或删除属性的场景。为此,Vue提供了全局方法$set$delete,下面看一下$set的源码

function set (target, key, val) {
  //错误传参的拦截代码(自行看源码)
  
  //对于数组的处理
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  //target是data选项中的对象或数组,进行observe的时候,会为value添加`__ob__`属性,值就是Observer实例
  //而Observer实例化过程中,挂载了一个Dep实例,用途就在于此
  var ob = (target).__ob__;
  defineReactive$$1(ob.value, key, val);//定义数据劫持
  ob.dep.notify();//立即通知更新
  return val
}

关于Vue的响应式机制到此告一段落,关于异步更新机制和Diff算法将在专门的章节中介绍,在解读异步更新机制前,先剖析一下watchwatchercomputed的区别。

结束语

山水.png

千山万水何惧怕,拨开云雾见红霞

—— 祝君好梦 ——