美文网首页让前端飞JavaScript 进阶营Web前端之路
学习vue2.5源码之第三篇——数据的双向绑定(数据响应式原理)

学习vue2.5源码之第三篇——数据的双向绑定(数据响应式原理)

作者: Rocky_Wong | 来源:发表于2017-12-08 16:51 被阅读362次

    数据的双向绑定(数据观测)

    上一章我们在学习initState的时候发现初始化的时候,initPropsinitData都涉及到observer文件夹中的一些方法(defineReactiveobserve),以及initComputedinitWatch也涉及到Watcher对象和$watch方法等等,这些都有是和数据的双向绑定(也叫数据观察)有关,这一章我们就来学习一下vue关于数据双向绑定的代码。核心代码主要是在src/core/observer文件夹中。

    一些概念

    vue是由render函数来生成虚拟dom,再映射到页面上。Observer将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为 getter/setter,通过Dep达到追踪依赖的效果。当我们访问属性时会添加依赖,当我们修改属性时将会通知Watcher重新计算,从而致使它关联的组件得以更新。

    • Observer: 观察者,对实例对象的所有属性进行监听

      • 思维路径 walk() => defineReactive() => Object.defineProperty() => Dep...
    • Watcher: 当数据变化时执行回调函数cb,还有一些与dep操作相关的函数

      • 思维路径 update() => sync ? (run() => cb.call()) : queueWatcher()
    • Dep: dependence依赖的缩写,依赖收集器,Observer和Watcher之间的桥梁,存放watch数组,绑定数据getter时添加依赖,更改数据setter时通知watcher更新

    observe

    还记得initData是通过observe()作为入口进行数据绑定的吗,我们就从这里开始看起吧~先看一下这个函数:

    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 if (
        observerState.shouldConvert && // 为真时,改变后的新值也会进行双向绑定
        !isServerRendering() && // 非服务端渲染
        (Array.isArray(value) || isPlainObject(value)) && // 是一个数组
        Object.isExtensible(value) && // 或者是一个单纯的可拓展的对象
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++ // 以此对象作为根节点$data的vue实例数量加一
      }
      return ob
    }
    

    第一个参数是一个data对象,假如它拥有__ob__属性的话证明该对象已经进行绑定了一个observer,就不用再次创建了;假如没有__ob__属性,而且满足以上条件(注释中有标明)的时候,我们将为它创建一个Observer对象。ob = new Observer(value),并且执行vmCount++。我们转到Observer看看:

    Observer

    export class Observer {
      value: any;
      dep: Dep;
      vmCount: number; 
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // 定义__ob__属性
        def(value, '__ob__', this)
        // 若是数组对象则另作处理(关于数组原型方法的数据监听)
        if (Array.isArray(value)) {
        const augment = hasProto
          ? protoAugment
          : copyAugment
        augment(value, arrayMethods, arrayKeys)
        this.observeArray(value)
        } else {
        // 其他途径走walk()
        this.walk(value)
        }
      }
    
      ...
      
    }
    

    每一个Observer对应一个Dep依赖,并且为该数据对象添加__ob__属性,值为这个Observer,之前在observe函数中可以看到假如已经存在__ob__属性就不需要再次创建Observer对象了。

    接下来要判断传入的值是否是一个数组,假如value不是一个数组而是一个普通对象,那么执行walk()函数。

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

    walk()中遍历对象对每个属性执行defineReactive,我们又看看defineReactive

    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
      }
    
      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
          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
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
      })
    }
    

    该方法先是为每个属性创建一个Dep,这样做的原因是每个属性对应自己的依赖集,当有属性发生变化时只触发相应属性的更新而不用更新全部属性,导致资源的浪费。

    vue允许用户自己定义getter和setter,假如用户自己设置了的话,vue就使用用户所定义的getter和setter。

    现在重头戏来了,vue的数据双向绑定就是通过Object.defineProperty这个原生的方法实现的,Object.defineProperty 是 ES5.1 规范中提供的方法,用来修改对象属性的属性描述对象,通过 Object.defineProperty 函数,我们可以通过定义对象属性的存取器(getter/setter)来劫持数据的读取,实现数据变动时的通知功能。

    回到源码中来,通过Object.defineProperty我们重写了对象的属性描述符,在getter中添加dep.depend(),让数据和watcher建立关系,并且对子对象的observer也进行依赖绑定childOb.dep.depend(),如果数组元素也是对象,那么它们的observe过程也生成了__ob__实例,那么就让__ob__也收集依赖。

    在setter中若新老值有变化的话就添加dep.notify来通知watcher更新数据,并监听新的值childOb = !shallow && observe(newVal)customSetter在开发过程中用于输出错误。

    注意,defineReactive中的depObserver构造函数的dep是不同的,原因是getter和setter只能监听到属性的更改,不能监听到属性的删除与添加,所以Observer构造函数的dep会在vue的setdelete操作中会利用上。

    看回Observer构造函数中的第二种情况,假如value是一个数组对象,会做另外的处理。由于传进来的数组对象只是一个引用地址,对于数组一些原有的操作(push、splice、sort等)是无法进行数据监听的,所以vue中有两种操作:1,在环境支持__proto__时,执行protoAugment,改写value的__proto__;2,不支持__proto__时,执行copyAugment,直接在value上重新定义这些方法。

    function protoAugment (target, src: Object, keys: any) {
      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])
      }
    }
    

    重点是在于来自同级文件array.js的arrayMethods,它是一个改写了数组方法的对象

    import { def } from '../util/index'
    
    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    
    ;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    .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
      })
    })
    

    特别对push、unshift、splice参数大于2时的三种情况进行调用ob.observeArray,因为他们都添加了新元素需要进行监听,然后给所有的原型方法都添加了数据更新ob.dep.notify()。然后对原型方法进行重新定义后,遍历这个数组为每个元素执行observe,监听新插入的值。

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

    Dep

    前面提及了好几次dep中的方法,而且我们也提到了dep的基本概念,就是联系Observer和Watcher的依赖收集器,我们现在看一下Dep对象

    export default class Dep {
      static 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)
      }
    
      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指向目标Watcher,id作为Dep的唯一标识,subs是用于存放watcher的数组。addSub向数组添加watcher,removeSub从数组中删除watcher,depend是在getter中为目标Watcher添加依赖,调用了watcher的addDep(稍后分析),notify在setter时触发watcher的更新。

    Watcher

    let uid = 0
    
    export default class Watcher {
    
      constructor (
      ) {
        this.vm = vm
        vm._watchers.push(this)
        this.id = ++uid
        this.cb = cb
        this.getter = expOrFn
        this.value = this.get()
      }
    
      get () 
        let value
        const vm = this.vm
        value = this.getter.call(vm, vm)
        return value
      }
    
      update () {
        this.run()
      }
    
      run () {
        const value = this.get()
        this.cb.call(this.vm, value, oldValue)
      }
    
    }
    

    emmm...我们直接看一下超级无敌简化版的watcher,走一下最常见的流程,例如最简单的:vm.$watch("mydata",()=>console.log("我是回调"))。主要是看传来的监听表达式/函数expOrFn(mydata)和响应回调函数cb(()=>console.log("我是回调")),大概流程就是observer监听到属性值更改后(setter)调用dep.notify通知watcher更新updateupdate中调用了函数runrun通过get获取到监听的值expOrFn,最后调用回调函数cb进行响应式更新(大概思路:update => run => get => cb)

    现在我们再具体看看那几个函数

    get

    get () {
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
          throw e
        }
      } finally {
        if (this.deep) {
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
      }
      return value
    }
    

    来个小插曲,什么是dep.target呢?它是Watcher实例,为什么要放在Dep.target里呢?是因为在getter中会执行dep.depend(),执行Dep.target.addDep(this),而在addDep中通过 dep.addSub(this)又把Watcher实例添加到dep的观察集中。

    // Dep.js
    depend () {
      if (Dep.target) {
        Dep.target.addDep(this)
      }
    }
    
    // Watcher.js
    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.js
    addSub (sub: Watcher) {
      this.subs.push(sub)
    }
    

    看回get()函数,先是执行pushTarget(this),将这个watcher设为target,对传来的表达式expOrFn进行一定的运算得到value,这个过程触发target相关依赖的getter(例如表达式为‘a+b’的话就会触发a和b的getter),也就是说会把target加到各自的dep.subs中,也就是成功地绑定了所有相关的依赖,完成操作后再执行popTarget()将target又恢复为null。每次进行数据监听都会设定当前watcher为新的target,完成更新后又会将target设为空值,这样数据之间就不会互相污染。

    update && run

    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        // 异步队列
        queueWatcher(this)
      }
    }
    
    run () {
      if (this.active) {
        const value = this.get()
        if (
          value !== this.value ||
          isObject(value) ||
          this.deep
        ) {
          const oldValue = this.value
          this.value = value
          if (this.user) {
            try {
              this.cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
          } else {
            this.cb.call(this.vm, value, oldValue)
          }
        }
      }
    }
    

    当属性值发生变化时触发setter,就会执行dep.notify,然后调用了watcher的update,在update中有两种情况,同步的话就执行run,异步的话执行queueWatcher。后者我们稍后再讨论,先看run,若新旧值不同的话就执行回调函数this.cb.call(this.vm, value, oldValue)。只有在用户设定了sync:true时才会直接执行数据更新,否则的话都会走另外一条路:异步更新队列。

    sync && immediate

    说到sync,再说说immediate,设置immediate:true也能直接执行回调,但是和sync是不一样的。代码实现在src/core/instance/state.js中有提及:

    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    

    区别在于,当指定immediate:true时,会立即以表达式的当前值触发回调函数,不会等到数据发生变化时才进行回调;而指定sync:true时是数据发生变化时不将watcher放进异步队列中而是立即执行回调。

    queueWatcher

    简单来说异步更新队列就是开启一个队列,将一个事件循环内的全部数据变动缓冲在其中,并对watcher做去重操作。在下一个tick中我们再将队列中的watcher依次取出并执行更新。这对避免不必要的计算和DOM操作非常重要。

    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)
        }
      }
    }
    

    flushing变量是正在刷新,当页面不在刷新时,把watcherpush到一个队列尾部,若正在刷新的话则把watcher放到队列相应的位置中(根据id);waiting变量是正在等待,假如没有处于等待状态的话则等到下一个nexttick中执行flushSchedulerQueue

    flushSchedulerQueue

    function flushSchedulerQueue () {
      flushing = true
      let watcher, id
    
      queue.sort((a, b) => a.id - b.id)
    
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        id = watcher.id
        has[id] = null
        watcher.run()
      }
    
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
    
      resetSchedulerState()
    
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue)
    
      if (devtools && config.devtools) {
        devtools.emit('flush')
      }
    }
    

    此函数将队列中的watcher根据它们的id从小到大排列依次执行它们的run方法,完成后resetSchedulerState重置flushingwaiting等状态,再
    callUpdatedHooks(updatedQueue)

    callUpdatedHooks

    function callUpdatedHooks (queue) {
      let i = queue.length
      while (i--) {
        const watcher = queue[i]
        const vm = watcher.vm
        if (vm._watcher === watcher && vm._isMounted) {
          callHook(vm, 'updated')
        }
      }
    }
    

    vm._watcher保存的是渲染模板时创建的watcher,如果队列中有该watcher,则说明模板有变化,调用updated生命钩子。

    在vue官网中对异步更新队列的解释是

    只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

    set && del

    前面提到了使用defineReactive进行数据监听有个缺陷,就是当属性进行删除和新增时,getter和setter并不会触发,所以vue为了弥补这个缺陷,提供Vue.set和Vue.delete方法让我们设置和删除对象的属性。

    export function set (target: Array<any> | Object, key: any, val: any): 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
      }
      // Observer构造函数中定义的dep
      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
    }
    
    export function del (target: Array<any> | Object, key: any) {
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1)
        return
      }
      // Observer构造函数中定义的dep
      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()
    }
    

    还记得Observer构造函数中定义了dep吗,这是为每个数组和对象都创建另一个依赖收集器,挂在它们的__ob__属性上。在依赖收集阶段,watcher 将被同时注册在这两个 dep 上,准备接收响应。setter 操作,通知 set/get 中的 dep;非 setter 操作,如对象 key 添加删除、数组变异方法调用,通知__ob__中的 dep

    deep

    当我们设置deep: true时即进行深度监听。举个例子,有这么个对象{a:{b:{c:{d:1}}}},当我们访问a.b.c的值时,依次触发了a.ba.b.cgetter,将 watcher 注册进对应的 dep,所以当我们修改a.b.c上游的值时a.b.c会监测到发生改变;但是相反,当我们修改a.b.c的值时a.b.c上游是不会监测到改变的,因为setter只会检测到引用变化,不会监测到对象内部的变化。所以这个时候我们可以设置deep: true,在代码中的实现是traverse(value),当发现依赖目标为一个对象时,递归进去遍历每一个子属性,这就会主动触发了深度属性的getter。实现深度监听的功能~

    这就是vue中实现数据双向绑定的整个过程,如有描述不妥或不清晰之处请随时提出,谢谢~

    相关文章

      网友评论

        本文标题:学习vue2.5源码之第三篇——数据的双向绑定(数据响应式原理)

        本文链接:https://www.haomeiwen.com/subject/fvaeixtx.html