Vue2.0源码阅读笔记(二):响应式原理

  Vue是数据驱动的框架,在修改数据时,视图会进行更新。数据响应式系统使得状态管理变的简单直接,在开发过程当中减小与DOM元素的接触。而深刻学习其中的原理十分有必要,可以回避一些常见的问题,使开发变的更为高效。
react

1、实现简单的数据响应式系统

  Vue使用观察者模式(又称发布-订阅模式)加数据劫持的方式实现数据响应式系统,劫持数据时使用 Object.defineProperty 方法将数据属性变成访问器属性。Object.defineProperty 是 ES5 中一个没法 shim 的特性,所以Vue 不支持 IE8 以及更低版本浏览器。
  Vue源码中对数据响应式系统的实现比较复杂,在深刻学习这部分源码以前,先实现一个较为简单的版本更有助于后续的理解。代码以下所示:
express

let uid = 0 

// 容器构造函数
function Dep() {
    // 收集观察者的容器
    this.subs = []
    this.id = uid++
}

Dep.prototype = {
    // 将当前观察者收集到容器中
    addSub: function(sub) {
        this.subs.push(sub)
    },

    // 收集依赖,调用观察者的addDep方法
    depend: function() {
        if(Dep.target){
            Dep.target.addDep(this)
        }
    },

    // 遍历执行容器中各观察者的run方法,以执行回调
    notify: function() {
        this.subs.forEach(sub => {
            sub.run()
        })
    }
}

// 初始化当前观察者对象为空
Dep.target = null

// 数据劫持函数
function observe(data) {
    // 防止重复对数据作劫持处理
    if(data.__ob__) return
    let keys = Object.keys(data)
    keys.forEach(key => {
        let val = data[key]
        let dep = new Dep()

        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                dep.depend()
                return val
            },
            set: function(newValue) {
                if((newValue !== newValue) || (newValue === val)){
                    return
                } else {
                    val = newValue
                    dep.notify()
                }
            }
        })
    });
    // 在被劫持的数据上定义一个不可遍历的内部属性
    Object.defineProperty(data, '__ob__',{
        configurable: true,
        enumerable: false,
        value: true,
        writable: true
    })
}

// 观察者构造函数
function Watcher(data, exp, callback) {
    this.cb = callback
    this.deps = {}
    this.exp = exp
    // 获取获得数据的函数
    this.getter = this.parseExp(exp.trim())
    this.data = data
    this.value = this.get()
}

Watcher.prototype = {
    run: function() {
        let value = this.get()
        let oldValue = this.value

        if(value !== oldValue){
            this.value = value
            this.cb.call(null, value, oldValue)
        }
    },

    addDep: function(dep) {
        // 防止收集重复数据
        if(!this.deps.hasOwnProperty(dep.id)){
            dep.addSub(this)
            this.deps[dep.id] = dep
        }
    },

    get: function() {
        // 将实例对象变为当前观察者对象
        Dep.target = this
        // 读取数据,从而触发数据get方法
        let value = this.getter.call(this.data, this.data)
        // 依赖收集完毕,当前观察者对象置为空
        Dep.target = null

        return value
    },

    // 经过形如‘a.b’的字符串形式获取数据值
    parseExp: function(exp) {
        if(/[^\w.$]/.test(exp)) return

        let exps = exp.split('.')

        return function(obj) {
            return obj[exps[1]]
        }
    }
}

// 监测函数
function $watch(data, exp, cb) {
    observe(data)
    new Watcher(data, exp, cb)
}
复制代码

  首先使用监测函数 $watch 来测试一下,代码以下:
数组

let a = {
    b: 100,
    c: 200
}

const callback = function(newValue, oldValue) {
    console.log(`新值为:${newValue},旧值为:${oldValue}`)
}

$watch(a, 'a.b', callback)
$watch(a, 'a.c', callback)

a.b = 101 
a.c = 201
复制代码

  输出结果:
浏览器

新值为:101,旧值为:100
新值为:201,旧值为:200
复制代码

  上述代码的逻辑结构图以下所示:
服务器

响应式原理简单实现
  响应式系统能够分为三个阶段:数据劫持(图中蓝线表示)、收集依赖(图中红线表示)、触发依赖(图中绿线表示)。

一、数据劫持

  在数据劫持函数 observe 中,首先检测对象中是否存在不可遍历的属性 __ob__ 。若是存在,则表示该对象已经转化为响应式的;若是不存在,在数据转化以后添加上 __ob__ 属性。
  而后循环遍历对象的属性,将数据属性变成访问器属性,每一个访问器属性经过闭包引用一个 Dep 实例 dep 。在读取属性时,会触发 get 方法,而后调用 dep.depend() ,收集依赖。在为属性设置新值时,会触发 set 方法,而后调用 dep.notify() ,触发依赖。经过 observe 方法仅仅是改造对象属性,对象属性的 get 和 set 此时并无触发。
闭包

二、收集依赖

  经过 Watcher 函数为响应式数据添加依赖。所谓依赖,是指当数据变动时须要触发的回调函数。
  Watcher 函数实例化时经过调用 get() 方法先将实例对象设置成当前观察者对象,而后读取数据,数据的 get 方法被调用,接着调用数据闭包引用的数据 dep 的 depend() 方法。
  在 depend() 中,会将 dep 传入当前观察者的 addDep() 方法。在 addDep() 方法中,首先防止重复收集依赖,而后调用 dep.addSub() 方法将当前观察者添加到 dep 的subs 属性中,完成依赖的收集。
app

三、触发依赖

  触发依赖是指在数据发生改变时,将数据闭包引用的变量 dep 中存储的观察者对象的回调依次执行。
  当数据改变时,会调用 set 方法,而后执行 dep.notify() 方法,该方法遍历数组 dep.subs ,执行数组中每一个观察者对象的 run() 方法。Watcher 实例的 run() 方法将数据改变前、改变后的值传入回调函数中执行,完成依赖的触发。
框架

四、存在的问题

  上述代码实现了一个最简单的数据响应式系统,可是存在不少的问题,好比:若是数据类型为数组怎么办?若是对象的属性自己就是访问器属性呢?删除对象属性怎么触发依赖?等等问题。
  2019年Vue做者尤雨溪接受访问时说:
异步

“我当时一方面是想本身实现一个简单的框架练练手,另外一方面是想尝试一下用 ES5 的Object.defineProperty 实现数据变更侦测。”函数

  随着时间的推移,Vue功能愈来愈完善,早已不是当初那用来练手的框架。在了解最基础的实现思路以后,让咱们深刻Vue源码中关于数据响应式系统的实现。

2、observe

  observe 函数的功能是劫持数据,改造数据,使数据被访问时可以收集依赖,数据改变时可以触发依赖
  observe 函数代码以下所示:

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 if (
    shouldObserve && !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

  首先,保证处理的数据类型是对象;若是数据对象上有 __ob__ 属性,且该属性是 Observer 的实例,说明数据已是响应式的,再也不重复处理数据,直接返回该属性。
  而后判断是否符合如下五个条件:shouldObserve 变量为 true、不是服务器端渲染、数据是纯对象或者数组、数据是可扩展的、数据不是 Vue 实例。同时知足这五个条件,会经过函数 Observer 处理数据,并返回一样的值。也就是说函数 observe 要么返回 undefined ,要么返回 Observer 的实例。
  Observer 函数的 constructor 方法源码以下所示:

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    const augment = hasProto ? protoAugment : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
复制代码

  Observer 的实例对象上有三个属性:Dep 的实例属性 dep 、指向被劫持数据的属性 value、vmCount。而被劫持数据上会添加 __ob__ 属性,该属性指向 Observer 的实例对象,该实例对象与被劫持数据为循环引用。
  observe 函数处理的数据分为两种:纯对象、数组。虽然数组也是对象,可是有它的特殊性:数组的索引是非响应式的。observe 函数对这两种类型数据有不一样的处理方式。

一、处理纯对象

  若是被劫持数据为纯对象,则通过 walk 方法处理。

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}
复制代码

  方法 walk 是将对象上每一个属性都调用 defineReactive 方法处理。

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  const dep = new Dep()

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

  // cater for pre-defined getter/setters
  const getter = property && property.get
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    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
    },
    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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

(一)、纯对象数据劫持原理

  从 defineReactive 方法的代码能够看到:劫持数据的原理就是将数据属性转变成访问器属性,若是数据自己就是访问器属性,则在重写的 get、set 方法中调用以前对应的 get、set 方法。
  每一个属性闭包引用一个 Dep 的实例 dep ,在 get 方法中经过 dep.depend() 来收集依赖,在 set 方法中经过 dep.notify() 来触发依赖。

(二)、若是数据属性值为纯对象或者数组

  若是纯对象数据的属性值为纯对象或者数组该怎么处理?想要弄清楚这个问题首先要明白下面代码中的 childOb 值究竟是什么。

let childOb = !shallow && observe(val)
复制代码

  shallow 是 defineReactive 方法的形参,含义为是否深度观测数据,当不传该参数时,默认为深度观测。
  observe 方法处理数据为非对象时返回 undefined ,处理对象时返回 Observer 的实例。childOb 的值存在,则代表处理的属性值 val 为纯对象或者数组,且childOb 为 Observer(val) 的实例。由于循环引用的存在,childOb 与 val.__ob__ 相等。
  在属性值为对象的状况下,当触发依赖时处理比较简单,仅仅只是将新值经过 observe 方法递归处理,使其变成响应式数据。
  而在 get 方法中收集依赖则比较麻烦,首先执行以下代码:

childOb.dep.depend()
复制代码

  也就是执行下列代码:

val.__ob__.dep.depend()
复制代码

  前面说过,属性依赖的收集是存储在闭包引用的 dep 变量中的,那么每一个对象数据的 __ob__ 属性的 dep 是用来作什么的?这里为何会重复收集一遍依赖呢?其实,主要是由于这两个dep 触发的时机不一样,闭包引用的 dep 是在属性值改变时使用的,对象__ob__ 属性的 dep 是在对象引用改变时使用的。在下面讲 Vue.set 与 Vue.delete 的原理时将详细说明。

二、处理数组

  数组中有七种实例方法会改变数组自身的值:push、pop、shift、unshift、splice、sort 与 reverse。在对象类型是数组的状况下,在数组被读取时收集依赖,在用户使用这七种方法改变数组时触发依赖。

(一)、收集依赖

  数组没法将索引变成访问器属性,因此不能像纯对象同样利用每一个属性的闭包来收集和触发依赖。在处理数组时,会先经过 observe() 处理,这样数组上就添加了 __ob__ 属性,指向 Observer 的实例对象。在 __ob__.dep 中收集依赖。
  有一点比较特殊,在数组收集依赖时有以下代码:

if (Array.isArray(value)) {
    dependArray(value)
}
复制代码

  dependArray 递归函数代码以下所示:

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
复制代码

  这段代码的功能是:在数组中有纯对象、数组时,除了将依赖收集到数组的 __ob__.dep 属性中,还要递归的收集到包含的纯对象、数组的 __ob__.dep 属性中
  为何要这样处理?这是由于这样一个前提:数组中任何值的改变都算做是数组的改变,只要依赖了该数组,就等价于依赖了数组中的每个元素。

{
  a:[ {b: 1} , [1], 2 ]
}
复制代码

  如上面的例子所示,在访问数组 a 时会添加 __ob__ 属性,在 a.__ob__.dep 中存储对 a 的依赖。当经过改变数组自身的实例方法操做 a 时,会调用 a.__ob__.dep.notify() 来触发依赖;当经过 Vue.set() 来改变 a 的某个值时,会转化成实例方法调用的形式,而后调用 a.__ob__.dep.notify() 来触发依赖;。
  可是,若是改变 a[0].b 的值,因为在对象 a[0] 中并无收集对数组 a 的依赖,则没法触发 a 的依赖。这就违背了数组中任何值的改变都算做是数组的改变这一前提。
  所以 Vue 经过递归调用 dependArray 方法来将对数组依赖收集到数组包含的每个对象中,这样数组中任何数值的改变都会触发该数组的依赖。

(二)、触发依赖

  为了可以在经过实例方法改变数组时可以触发依赖,Vue重写了能够改变数组自身的方法。以下代码所示:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(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)
    ob.dep.notify()
    return result
  })
})
复制代码

  arrayMethods 对象的原型 为 Array.prototype ,在该对象上添加通过处理后的这七种方法。重写的实例方法主要包含三个功能:

一、调用原生实例方法。
二、当经过push、unshift、splice添加数据时,将新添加的数据变成响应式的。
三、当数组改变时,触发依赖。

  其中,触发依赖的原理须要注意一下,与以前说对象引用改变时触发自身属性中的 dep 同样,数组自身发生改变,触发的也是经过自身 __ob__ 属性的 dep 的 notify() 来触发依赖的。
  ES6新增了对象的 __proto__ 属性,用来读取或设置当前对象的prototype对象,兼容到IE11。Vue在处理数组时,若是数组拥有 __proto__ 属性,则直接将该属性指向 arrayMethods 对象,即修改数组的原型对象。这样调用七种改变数组自己的方法时,会调用 arrayMethods 对象的方法,从而实现触发依赖的功能。以下图所示:

处理数组原理
  Vue框架兼容到 IE9 ,对于 IE9 和 IE10 浏览器来讲,对象中没有 __ob__ 属性。Vue对于这种状况的处理方式是:直接将 arrayMethods 对象上的方法拷贝到数组自身上,且这七种方法是不可枚举的。以下代码所示:

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
复制代码

三、Vue.set与Vue.delete

  总结上面讲述的依赖收集和触发的状况以下:

一、若是是对象,将对象的属性转化为访问器属性,访问属性时收集依赖存储到属性闭包引用的变量 dep 中,更改属性时触发闭包引用的变量 dep 中的依赖。
二、若是是数组,在读取数组时添加 __ob__ 对象属性,在该对象的 dep 属性中收集依赖。而后重写可以改变自身的七种实例方法,在调用这些实例方法时,触发 __ob__.dep 中存储的依赖。

  这两种状况就致使了官网列表渲染注意事项 提到的问题:

因为 JavaScript 的限制,Vue 不能检测如下变更的数组:
一、当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
二、当你修改数组的长度时,例如:vm.items.length = newLength

仍是因为 JavaScript 的限制,Vue 不能检测对象属性的添加或删除。

  Vue 提供 Vue.set()、Vue.delete() 两个全局 API 以及 vm.set()、vm.delete() 两个实例方法来解决上述问题。

(一)、Vue.set

  Vue.set() 与 vm.$set() 都是调用 src/core/observer/index.js 中的 set 方法。

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
复制代码

  由上述代码可知,set 方法的功能以下:

一、若target为数组,key为有效索引,则先判断是否调整数组大小,再调用splice方法触发依赖。
二、若target为纯对象,key是对象是存在的属性,则直接改变key值,进而调用属性set方法触发依赖。
三、若target不是响应式的,则直接往对象上添加key属性,target上有key属性则直接覆盖。
四、若target是响应式的,且自己没有key属性,则经过defineReactive方法将值转成响应式的添加到target上,而后经过target.__ob__.dep.notify()触发依赖。

(二)、Vue.delete

  Vue.delete() 与 vm.$delete() 都是调用 src/core/observer/index.js 中的 del 方法。

export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}
复制代码

  由上述代码可知,del 方法的功能以下:

一、若target为数组,key为有效索引,则调用splice方法完成删除操做,而且触发依赖。
二、若target为纯对象,key属性不存在,则不执行删除操做,直接返回undefined。
三、若target为纯对象,key属性存在,则删除该属性,而后经过target.__ob__.dep.notify()触发依赖。

3、Dep

  Dep 函数的主要功能是生成被观察数据存放观察者的容器,其静态属性target指向当前要收集的观察者
  Dep 函数以下所示:

export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) { this.subs.push(sub) }

  removeSub (sub: Watcher) { remove(this.subs, sub) }

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

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

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
复制代码

  实例属性subs为存放观察者的数组,depend()收集依赖、notify()触发依赖。有一点须要注意:depend 方法调用当前观察者对象的 addDep 方法,addDep 方法又调用 Dep 实例的 addSub 方法来将 Dep.target 存入 subs 中,为何不直接将当前要收集的观察者 Dep.target push到subs中?
  这样作的缘由主要有三点:避免重复收集依赖、方便记录被观察数据变化先后的值、观察者对象中保存着被观察数据的数量。若是仅仅是为了不重复收集依赖,能够利用观察者对象的id,删除重复观察者对象来实现。
  另外,Dep.target 的值不是简单的将当前观察者赋值完成的,而是由 pushTarget 来实现,在赋值以前先存储本来观察者,当前观察者被数据收集以后,经过 popTarget 来将 Dep.target 的值恢复到本来的观察者对象。

4、Watcher

  Watcher 函数的主要功能是为被观察的数据提供依赖(回调函数)
  观察者函数观察数据的方式是读取数据,数据通过 observe 函数处理后变成响应式的,在被读取的过程当中可以存储 Watcher 实例对象的引用,这就是收集依赖的过程。
  当被观察数据发生改变时,数据会遍历存储的 Watcher 实例对象引用,来分别执行各 Watcher 实例对象上回调函数,这就是触发依赖的过程。

一、概述

  Watcher 函数的 constructor 方法源码以下所示:

constructor (vm: Component, expOrFn: string | Function,
    cb: Function, options?: ?Object, isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) { vm._watcher = this }
    vm._watchers.push(this)
    // 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
    }
    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()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy ? undefined : this.get()
  }
复制代码

  Watcher 函数接收五个参数:当前组件实例对象、被观察的目标、被观察数据发生改变后的回调函数、传递的选项、是否为渲染函数的标识。
  观察者对象的 vm 属性指向当前组件的实例对象,组件实例对象上的 _watcher 属性指向渲染函数的观察者,而 _watchers 属性则包含当前实例对象上的所有观察者对象。
  根据传入选项 options 的值来初始化观察者对象上的五个值,其含义分别为:

属性deep:是否深度观测数据,默认为 false 。
属性user:观察者是否由开发者定义的,默认为 false 。
属性lazy:观察者是否惰性求值,是内部为实现计算属性功能建立的观察者,默认为 false 。
属性sync:数据变化时是否同步求值,默认为 false 。
属性before:钩子函数,在数据变化以后、触发更新以前调用。

  观察者对象的 getter 方法是根据传入 expOrFn 参数的类型来肯定的。若是传入函数,getter 方法直接等于该函数;若是传入字符串,getter 方法为返回目标值的函数。getter 方法的功能就是可以读取目标数据。
  在 constructor 方法的最后,会 lazy 属性是否为 true 来决定 value 的值,lazy 属性只有是计算属性的观察者时才为 true 。若是不是计算属性的观察者,会调用 get() 方法,并用 value 记录返回值。
  get() 方法主要有两个功能:读取被观察数据、返回数据值。当数据被读取时,数据对应的的 dep.depend() 方法被调用,而后调用观察者对象的 addDep() 方法, 继而调用 dep.addSub() 方法,完成对当前依赖的收集。
  当被观察数据发生改变时,会调用 dep.notify() 方法,而后调用其中包含的每个观察者对象的 update() 方法。在 update 中,若是不是计算属性的观察者,最终会调用 run() 方法。run() 方法先执行 get() 方法将获取数据新值,而后将新旧值做为参数调用回调函数。

二、避免收集重复依赖

  Watcher 函数中避免收集重复依赖主要依靠两组属性:newDeps 与 newDepIds、deps 与 depIds 。
  newDeps 与 newDepIds 是为了不一次求值的过程当中收集重复依赖的。当 expOrFn 参数为函数,且在函数中存在一个值屡次使用时,使用 newDeps 与 newDepIds 来避免重复收集该值的依赖。newDeps 存储的是当前求值收集到的 Dep 实例对象
  deps 与 depIds 是为了不屡次求值的过程当中收集重复依赖的。当被观察数据改变,从新读取数据时,经过这两个属性来避免再次收集依赖。deps 存储的是上一次求值时收集的 Dep 实例对象
  在 get() 方法的 finally 部分中会调用 cleanupDeps() 方法。

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  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
}
复制代码

  cleanupDeps() 方法有两个功能:

一、移除废弃的观察者。
二、在清空 newDepIds 与 newDeps 以前,分别赋值给 depIds 与 deps 。

  避免收集重复依赖的代码在 addDep() 方法中。

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)
    }
  }
}
复制代码

  在 addDep() 方法中,首先判断当前id是否已经存在于 newDepIds 中,若是存在,则代表该依赖在本次求值时已经被收集,不用再重复收集。若是不存在,则将 id 添加到 newDepIds 中,将 dep 添加到 newDeps 中。
  接着判断依赖是否在上次求值时被收集,若是是,也不用再重复收集。若是上次求值时依赖也没有被收集,则收集依赖。

三、异步执行

  在触发依赖的过程当中调用 update() 方法,该方法有三种状况:做为计算属性的观察者、指明要同步执行、默认异步执行。

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

  当选项 sync 为 true 时,直接经过执行 run() 方法直接调用回调函数。而选项 sync 除非明确指定,默认是 false ,也就是说,默认对依赖的触发是异步执行的。
  异步执行主要是为了优化性能,例如当模板中的数据改变时,渲染函数会从新求值,完成从新渲染。若是同步执行,每次修改一个值都要从新渲染,在复杂的业务场景下可能会同时修改不少数据,屡次渲染会致使很严重的性能问题。
  若是异步执行,每次修改属性的值以后并无当即从新求值,而是将须要执行更新操做的观察者放入一个队列中,队列中没有重复的观察者对象,这样就能达到优化性能的目的。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) { i-- }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

  queueWatcher() 方法首先判断观察者队列中是否含有要添加的观察者,若是已经存在,则不作任何操做。flushing 变量为队列是否正在执行更新的标志,若是队列没有执行则直接将观察者对象存到队列中,若是队列正在执行更新,则须要保证观察者的执行顺序。
  变量 waiting 初始值为 false,在执行 if 判断以后变为 true ,也就是说 nextTick(flushSchedulerQueue) 只会执行一次。nextTick() 方法比较复杂,在这里能够简单理解成与 setTimeout(fn, 0) 功能相同,做用就是在下一事件循环开始时当即调用 flushSchedulerQueue ,从而将队列中的观察者统一执行更新。

5、总结

  Vue数据响应式系统整体来讲分为两步:一、劫持数据;二、将数据改变要触发的回调函数与数据关联起来。
  observe 函数的功能就是劫持数据,让数据在被读取时收集依赖,在改变时触发依赖。若是数据为纯对象,则将其属性转变成访问器属性;若是数据是数组类型,则经过重写可以改变自身的方法来实现。对象添加、删除属性以及数组经过直接赋值的方式改变并不会触发依赖,这时要使用 Vue.set()、Vue.delete() 方法来操做。
  Dep 函数是用来生成盛放依赖的容器。收集依赖最终都是收集到其实例对象的 subs 数组属性中,触发依赖最终操做时遍历执行 subs 中的观察者对象上的回调函数。
  Watcher 函数主要是将被观察的数据与数据改变后要执行的回调函数关联起来。为了提高性能,在这个过程当中要避免数据收集的依赖有重复;当数据改变时要异步执行更新。

如需转载,烦请注明出处:juejin.im/post/5cb6ee…

相关文章
相关标签/搜索