Vue如何实现数据状态的侦测

数据侦测是Vue、React、Angular三大前端框架必须考虑的部分,所谓数据侦测也即是如何把用户输入的数据或者从后端获取的数据通过某种机制同步到视图,可以用UI=render(state)表达式简单说明,state为数据状态,render为渲染函数,UI为最终呈现给用用户的视图。一旦state状态有更新,会触发render函数重新渲染出新的视图。那么Vue是通过怎样的侦测机制监测到数据发生变化并触发视图渲染。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,
当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
复制代码

Vue的数据侦测机制采用观察者模式,当视图绑定了数据对象data,data更新后,Vue是如何监听到的?Vue2使用了Object的DefineProperty函数来实现数据更新检测,该函数可定义了get函数获取值、set函数设置值,我们可以在get、set函数自定义逻辑。

Object.defineProperty(obj, key, 
    { enumerable: true, configurable: true, 
    get: function reactiveGetter () {}, 
    set: function reactiveSetter (newVal) {} 
})
复制代码

Vue定义了统一的观察者Observer对象,所有的对象都将经过Observer转换为包含get、set函数的对象,并且在get、set函数收集所有的观察者,当有数据变更会同步给所有观察者。代码中的构造函数用于接收数据对象,判断其类型,由于Array会通过push、unshift等函数更新数组,无法通过getter、setter监听,所以Observer对Object、Array类型做了不同的处理。接下来介绍Vue是如何处理Object和Array对象。

export class Observer {
    value: any;

    constructor (value: any) {
      this.value = value
      if (Array.isArray(value)) {
        this.observeArray(value)
      } else {
        this.walk(value)
      }
    }
  
    /**
     * 遍历对象所有的属性并转为为gettter、setter,该函数用户Object类型
     */
    walk (obj: Object) {}
  
    /**
     * 观察数组类型
     */
    observeArray (items: Array<any>) {}
  }
复制代码

Observer包含walk函数专门处理Object对象,walk函数遍历Object的所有key并调用defineReactive函数实现更新响应。defineReactive函数调用Object.defineProperty函数将数据对象的所有属性都转换为get、set模式,所有的值,初始传入的val或者后续通过set设置的值,都会调用observe函数将其转换为Observer对象。有了get和set我们就可以在其过程中附加观察者,数据有更新触发set后通知到各个观察者做相应的状态更新。

walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  
    /**
   * 定义响应式属性
 */
export function defineReactive (
    obj: Object,
    key: string,
    val: any
  ) {
    // cater for pre-defined getter/setters
    const getter = property && property.get
    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

        return value
      },
      set: function reactiveSetter (newVal) {
        if (setter) {
          setter.call(obj, newVal)
        } else {
          val = newVal
        }
        childOb = !shallow && observe(newVal)
      }
    })
  }
复制代码

Observer定义了observeArray函数专门处理Array类型,函数遍历数据元素,并调用observe函数单独处理每个元素,函数判断值是否包含__ob__属性,如果包含说明该值已经成功转换为Observer对象了,可直接返回。那么__ob__属性在什么时候附加到值上的?Vue在Observer构造函数调用def(value, 'ob', this)定义了__ob__属性。observe将值转换为Observer对象,而Observer对象会向下递归遍历所有属性,最终将所有属性值都转为Observer对象。

/**
     * 观察数组类型
     */
 observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}

/**
* 尝试将属性值转换为observer对象,
* 如果属性值不为Observer类型,则new一个新的Observer对象;
* 如果属性值为Obserer类型,则直接返回该对象;
*/
export function observe (value: any): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    let ob: Observer | void
    // 如果value包含__ob__对象表示已经转换为Observer对象
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else if (
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
        ) {
            ob = new Observer(value)
        }

    return ob
}
复制代码

前面有提到因为Array内部包含push、splice、unshift等函数处理元素,而这些是getter、setter监听不到的,所以Observer会单独处理Array对象,Observer对象的构造函数新增了9至13行代码预处理数组的操作函数,如果浏览器支持__proto__属性,可直接把重写的arrayMethods赋值给__proto__属性来实现数组的原型覆盖;否则调用copyAugment函数直接覆盖数组的操作函数。

export class Observer {
    value: any;

    constructor (value: any) {
        this.value = value

        def(value, '__ob__', this)
        if (Array.isArray(value)) {
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value)
        } else {
          this.walk(value)
        }
    }
}

/**
 * 通过__proto__属性扩展目标对象的原型链
 */
  function protoAugment (target, src: Object) {
    target.__proto__ = src
  }

  /**
   * 通过定义隐藏的属性扩展目标对象
 */
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重写的数组函数定义在arrayMethods中,具体要重写的函数定义在methodsToPatch数组中,接下来遍历该数组通过自定义函数覆盖数组原始函数,自定义函数将新插入的值inserted调用ob.observeArray函数将其监听起来。当调用splice函数时,inserted会从索引2开始截取,查看splice的定义splice(start[, deleteCount[, item1[, item2[, ...]]]]),第一、二个参数表示删除索引以及删除个数,所以要排除掉。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

/**
 * 需要重写的数组函数
 */
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * 拦截原数组函数附加自定义操作
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
    
    return result
  })
})
复制代码

到目前我们清楚了Vue是如何封装Object和Array对象,但还没涉及到如何监听以及通知,Vue在dep.js文件定义了Dep对象作为目标对象,可以被多个观察者Watcher监听,观察者列表存放在subs数组中,通过addSub、removeSub来附加、移除观察者。最新的观察者Watcher会存储到全局的Dep.target上,在调用depend函数时,会执行wacher的addDep函数,该函数再调用this.dep.adSub(this)将最新的观察者附加到subs中。通知则通过notify函数触发。

/**
 * Dep为目标对象,可同时被多个观察者Watcher监听
 */
 export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array<Watcher>;
  
    constructor () {
      this.id = uid++
      this.subs = []
    }
  
    /**
     * 附加观察者
     * @param {观察者} sub 
     */
    addSub (sub: Watcher) {
      this.subs.push(sub)
    }
    /**
     * 移除观察者
     * @param {观察者} sub 
     */
    removeSub (sub: Watcher) {
      remove(this.subs, sub)
    }
  
    /**
     * 将最新的观察者附加到dep上
     * target为Watcher对象,其包含addDep函数
     */
    depend () {
      // 最新的Watcher将被存放到Dep.target上
      if (Dep.target) {
        Dep.target.addDep(this)
      }
    }
  
    /**
     * 执行更新通知,调用每个watcher的update函数执行更新。
     */
    notify () {
      const subs = this.subs.slice()
      for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
  }
复制代码

定义了Dep对象,那Vue是如何通过Observer来触发注册和通知?首先在Observer的构造函数中定义了dep属性,用于监听当前Observer对象。

export class Observer {
    value: any;

    constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        ...
    }
}
复制代码

注册部分,分Object和Array两种类型的注册,Object通过defineProperty的get函数注册,代码在15至18行添加了注册逻辑,最新的观察对象会临时存储到Dep.target对象上,当Dep.target有值,执行dep.depend函数将Dep.target(Watcher)注册到dep.subs数组中,另外,如果对象的子属性值变化(例如person.age = 30),观察者也需要监听到,通过执行cildOb.dep.depend函数将子属性值Observer也注册当前Dep.target。get函数判断了value的类型,如果是数组对象,将调用dependArray函数遍历其每个元素,通过e.ob.dep.depend函数确保每个元素都被Dep.target监听到,如果元素是Array类型,将递归调用dependArray函数循环递归遍历数组的每个元素,确保都将被观察者监听到。

/**
   * 定义响应式属性
 */
 export function defineReactive (
    obj: Object,
    key: string,
    val: any
  ) {
     const dep = new Dep()
    ...
    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) {
            // 除了当前key会附加监听,其对应的value也需要附加监听。
            // 例如 { person: { age: 20 } }对象,不管是person的变化还是age的变化,watcher都需要监听到。
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
        }
        return value
      },
      set: function reactiveSetter (newVal) {}
    })
  }
  
  /**
   * 收集数组元素的观察者
   * 循环递归遍历数组的每个元素,使其被观察者监听到
 */
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)
      }
    }
  }
复制代码

通知部分,分对象类型和数组类型的通知,当给对象赋予新的值,set函数将被触发,该函数最后一行调用了dep.notify函数,循环遍历subs中的Watcher对象并执行this.subs[i].update函数通知更新。对于数组类型,在methodsToPatch数组的操作函数中添加ob.dep.notify()来通知数组的变更。

export function defineReactive (...) {
    ...
    Object.defineProperty(obj, key, {
      ...
      get: function reactiveGetter () {},
      set: function reactiveSetter (newVal) {
        ...
        childOb = !shallow && observe(newVal)
        dep.notify()
      }
    })
  }
  
//array.js文件
methodsToPatch.forEach(function (method) {
    ...
    // notify change
    ob.dep.notify()
    
    return result
  })
})
复制代码

目前为止介绍了Vue如何实现Object、Array对象的侦测和更新,可结合如下的流程图把介绍的内容串起来,共包含Observer、Dep、Array、Watcher四个类,以及红色标示的四个独立函数。

Vue状态检测机制 (2).png

执行流程说明:
1.walk:call 调用defineReactive函数将val转换为包含getter和setter的对象
2.obsererArray:call 调用observeArray函数遍历数组的每个元素
3.val to Observer 将每个val转换为Observer对象
4.rewrite:Array 重写Array的push、splice、unshift等函数
5.ifarray:call get函数中val为数组,调用dependArray函数
6.recursive 数组的元素也可能是数组,需递归自调用
7.depend 为每个元素附加Watcher监听
8.get:depend 在get函数中为val附加Watcher监听
9.set:notify 在set函数中触发更新通知
10.mutator:notify 在数组操作函数中触发更新通知
11.depend:addDep 在Dep的depend函数中调用watcher的addDep函数附加监听
12.dep:addSub 在addDep函数中调用dep的addSub将Watcher添加到subs数组中
复制代码

流程中有提到Watcher对象,但没有对其做详细介绍,在写Vue组件时,我们常常使用watch监听属性变化,例如当firstName、lastName属性变化时,自动更新fullName属性。

watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    },
    'baseInfo.phone': function(val) {
        console.log('new phone num: ${val}')
    }
  }
复制代码

Watcher在整个状态流程中扮演了观察者的角色,Vue的属性变化最终都会通知到Watcher,Watcher支持了链式属性(例如baseInfo.phone)的变化监听,再由Watcher触发视图的重新渲染。下一章节将结合Vue源代码中的watcher.js、scheduler.js文件介绍Vue是如何监听变化并有序的同步到Component组件。

附录:
1.发布订阅者与观察者模式区别,segmentfault.com/a/119000002…
2.plain Object判断,newbedev.com/the-differe…
3.Object的__proto__属性,developer.mozilla.org/zh-CN/docs/…

猜你喜欢

转载自juejin.im/post/7032974744437653540