vue系列---响应式原理实现及Observer源码解析(七)


一. 什么是响应式?
  • 二:如何侦测数据的变化? 2.1 Object.defineProperty() 侦测对象属性值变化 2.2 如何侦测数组的索引值的变化 2.3 如何监听数组内容的增加或减少? 2.4 使用Proxy来实现数据监听
  • 三. Observer源码解析
  • 回到顶部回到顶部回到顶部回到顶部回到顶部回到顶部

    三. Observer源码解析

    首先我们先来看一个简单的demo如下:
    DOCTYPE html>
    <html>
      <head>
        <title>Vue.js github commits exampletitle>
        
        <script src="../../dist/vue.js">script>
      head>
      <body>
        <div id="demo">
          <span v-for="(item, index) in arrs"> {{item}} span>
        div>
        <script type="text/javascript">
          new Vue({
            el: '#demo',
            data: {
              branches: ['master', 'dev'],
              currentBranch: 'master',
              commits: null,
              arrs: [1, 2]
            }
          });
        script>
      body>
    html>

    如上demo代码,我们在vue实例化页面后,会首先调用 src/core/instance/index.js 的代码,基本代码如下:

    import { initMixin } from './init'
    import { stateMixin } from './state'
    import { renderMixin } from './render'
    import { eventsMixin } from './events'
    import { lifecycleMixin } from './lifecycle'
    import { warn } from '../util/index'
    
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue

    如上Vue构造函数中首先会判断是否是正式环境和是否实例化了Vue。然后会调用 this._init(options)方法。因此进入:src/core/instance/init.js代码,主要代码如下:

    import { initState } from './state';
    export function initMixin (Vue: Class) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this;
        ..... 省略很多代码
        initState(vm);
        ..... 省略很多代码
      }
    }

    因此就会进入 src/core/instance/state.js 主要代码如下:

    import {
      set,
      del,
      observe,
      defineReactive,
      toggleObserving
    } from '../observer/index'
    
    .... 省略很多代码
    
    export function initState (vm: Component) {
      .....
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      .....
    }
    
    .... 省略很多代码
    
    function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      
      .... 省略了很多代码
    
      // observe data
      observe(data, true /* asRootData */)
    }

    如上代码我们就可以看到,首先会调用 initState 这个函数,然后会进行 if 判断 opts.data 是否有data这个属性,该data就是我们的在 Vue实例化的时候传进来的,之前实列化如下:

    new Vue({
      el: '#demo',
      data: {
        branches: ['master', 'dev'],
        currentBranch: 'master',
        commits: null,
        arrs: [1, 2]
      }
    });

    如上的data,因此 opts.data 就为true,有这个属性,因此会调用 initData(vm) 方法,在 initData(vm) 函数中,如上代码我们也可以看到,最后会调用 observe(data, true /* asRootData */) 方法。该方法中的data参数值就是我们之前 new Vue({ data: {} }) 中的data值,我们通过打断点的方式可以看到如下值:

    因此会进入 src/core/observer/index.js 中的代码 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 (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }

    执行 observe 函数代码,如上代码所示,该代码的作用是给data创建一个 Observer实列并返回,从最后一句代码我们可以看得到,如上代码 ob = new Observer(value); return ob;

    如上代码首先会if 判断,该value是否有 '__ob__' 这个属性,我们value是没有 __ob__ 这个属性的,如果有 __ob__这个属性的话,说明已经实列化过Observer,如果实列化过,就直接返回该实列,否则的话,就实例化 Observer, Vue的响应式数据都会有一个__ob__的属性,里面存放了该属性的Observer实列,目的是防止重复绑定。我们现在先来看看 代码:

    if (hasOwn(value, '__ob__')) {} 中的value属性值如下所示:

    如上我们可以看到,value是没有 __ob__ 这个属性的,因此会执行 ob = new Observer(value); 我们再来看看new Observer 实列化过程中发生了什么。代码如下:

    export class Observer {
      value: any;
      dep: Dep;
      vmCount: number;
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        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)
        }
      }
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
      observeArray (items: Array) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }

    如上代码我们可以看得到,首先会调用 this.dep = new Dep() 代码,该代码在 src/core/observer/dep.js中,基本代码如下:

    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 () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          // subs aren't sorted in scheduler if not running async
          // we need to sort them now to make sure they fire in correct
          // order
          subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    Dep.target = null;
    
    ......

    Dep代码的作用和我们之前讲的一样,就是消息订阅器,该订阅器的作用是收集所有的订阅者。
    代码往下执行,我们就会执行 def(value, '__ob__', this) 这句代码,因此会调用 src/core/util/lang.js 代码,
    代码如下:

    // ...... 省略了很多的代码
    import { arrayMethods } from './array';
    // ...... 省略了很多的代码
    /**
     @param obj;
     obj = {
       arrs: [1, 2],
       branches: ["master", "dev"],
       commits: null,
       currentBranch: "master"
     };
     @param key "__ob__";
     @param val: Observer对象 
     val = {
       dep: { "id": 2, subs: [] },
       vmCount: 0,
       value: {
         arrs: [1, 2],
         branches: ["master", "dev"],
         commits: null,
         currentBranch: "master"
       }
     };
     */
    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }

    如上代码我们可以看得到,我们使用了 Object.defineProperty(obj, key, {}) 这样的方法监听对象obj中的 __ob__ 这个key。但是obj对象中又没有该key,因此Object.defineProperty会在该对象上定义一个新属性为 __ob__, 也就是说,如果我们的数据被 Object.defineProperty绑定过的话,那么绑定完成后,就会有 __ob__这个属性,因此我们之前通过了这个属性来判断是否已经被绑定过了。我们可以看下demo代码来理解下 Object.defineProperty的含义:
    代码如下所示:

    var obj = {
      arrs: [1, 2],
      branches: ["master", "dev"],
      commits: null,
      currentBranch: "master"
    };
    var key = "__ob__";
    var val = {
      dep: { "id": 2, subs: [] },
      vmCount: 0,
      value: {
        arrs: [1, 2],
        branches: ["master", "dev"],
        commits: null,
        currentBranch: "master"
      }
    };
    Object.defineProperty(obj, key, {
      value: val,
      writable: true,
      configurable: true
    });
    console.log(obj);

    打印obj的值如下所示:

    如上我们看到,我们通过 Object.defineProperty()方法监听对象后,如果该对象没有该key的话,就会在该obj对象中添加该key属性。

    再接着 就会执行如下代码:

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

    如上代码,首先会判断该 value 是否是一个数组,如果不是数组的话,就执行 this.walk(value)方法,如果是数组的话,就判断 hasProto 是否为true(也就是判断浏览器是否支持__proto__属性),hasProto 源码如下:

    export const hasProto = '__proto__' in {};

    如果__proto__指向了对象原型的话(换句话说,浏览器支持__proto__),就调用 protoAugment(value, arrayMethods) 函数,该函数的代码如下:

    function protoAugment (target, src: Object) {
      target.__proto__ = src
    }

    其中 arrayMethods 基本代码在 源码中: src/core/observer/array.js 中,该代码是对数组中的方法进行重写操作,和我们之前讲的是一样的。基本代码如下所示:

    import { def } from '../util/index'
    
    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)
        // notify change
        ob.dep.notify()
        return result
      })
    });

    现在我们再来看之前的代码 protoAugment 函数中,其实这句代码和我们之前讲的含义是一样的,是让 value对象参数指向了 arrayMethods 原型上的方法,然后我们使用 Obejct.defineProperty去监听数组中的原型方法,当我们在data对象参数arrs中调用数组方法,比如push,unshift等方法就可以理解为映射到 arrayMethods 原型上,因此会被 Object.defineProperty方法监听到。因此会执行对应的set/get方法。

    如上 methodsToPatch.forEach(function (method) { } 代码中,为什么针对 方法为 'push, unshift, splice' 等一些数组新增的元素也会调用 ob.observeArray(inserted) 进行响应性变化。inserted 参数为一个数组。也就是说我们不仅仅对data现有的元素进行响应性监听,还会对数组中一些新增删除的元素也会进行响应性监听。...args运算符会转化为数组。
    比如如下简单的测试代码如下:

    function a(...args) { 
      console.log(args); // 会打印 [1] 
    }; 
    a(1); // 函数方法调用
    
    // observeArray 函数代码如下:
    
    observeArray (items: Array) {
      for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
      }
    }

    如上代码可以看到,我们对使用 push, unshift, splice 新增/删除 的元素也会遍历进行监听, 再回到代码中,为了方便查看,继续看下代码,回到如下代码中:

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

    如果我们的浏览器不支持 hasProto, 也就是说 有的浏览器不支持__proto__这个属性的话,我们就会调用copyAugment(value, arrayMethods, arrayKeys); 方法去处理,我们再来看下该方法的源码如下:

    /*
     @param {target} 
     target = {
       arrs: [1, 2],
       branches: ["master", "dev"],
       commits: null,
       currentBranch: "master",
       __ob__: {
         dep: {
           id: 2,
           sub: []
         },
         vmCount: 0,
         commits: null,
         branches: ["master", "dev"],
         currentBranch: "master"
       }
     };
     @param {src} arrayMethods 数组中的方法实列
     @param {keys} ["push", "shift", "unshift", "pop", "splice", "reverse", "sort"]
    */
    function copyAugment (target: Object, src: Object, keys: Array) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
      }
    }

    如上代码可以看到,对于浏览器不支持 __proto__属性的话,就会对数组的方法进行遍历,然后继续调用def函数进行监听:
    如下 def代码,该源码是在 src/core/util/lang.js 中:

    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }

    回到之前的代码,如果是数组的话,就会调用 this.observeArray(value) 方法,observeArray方法如下所示:

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

    如果它不是数组的话,那么有可能是一个对象,或其他类型的值,我们就会调用 else 里面中 this.walk(value) 的代码,walk函数代码如下所示:

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

    如上代码,进入walk函数,obj是一个对象的话,使用 Object.keys 获取所有的keys, 然后对keys进行遍历,依次调用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
      /*
       检查属性之前是否设置了 getter / setter
       如果设置了,则在之后的 get/set 方法中执行 设置了的 getter/setter
      */
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
      /*
       observer源码如下:
       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 (
            shouldObserve &&
            !isServerRendering() &&
            (Array.isArray(value) || isPlainObject(value)) &&
            Object.isExtensible(value) &&
            !value._isVue
          ) {
            ob = new Observer(value)
          }
          if (asRootData && ob) {
            ob.vmCount++
          }
          return ob
       }
       let childOb = !shallow && observe(val); 代码的含义是:递归循环该val, 判断是否还有子对象,如果
       还有子对象的话,就继续实列化该value,
      */
      let childOb = !shallow && observe(val);
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          // 如果属性原本拥有getter方法的话则执行该方法
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              // 如果有子对象的话,对子对象进行依赖收集
              childOb.dep.depend();
              // 如果value是数组的话,则递归调用
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          /*
           如果属性原本拥有getter方法则执行。然后获取该值与newValue对比,如果相等的
           话,直接return,否则的值,执行赋值。
          */
          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()
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            // 如果属性原本拥有setter方法的话则执行
            setter.call(obj, newVal)
          } else {
            // 如果属性原本没有setter方法则直接赋新值
            val = newVal
          }
          // 继续判断newVal是否还有子对象,如果有子对象的话,继续递归循环遍历
          childOb = !shallow && observe(newVal);
          // 有值发生改变的话,我们需要通知所有的订阅者
          dep.notify()
        }
      })
    }

    如上 defineReactive 函数,和我们之前自己编写的代码类似。上面都有一些注释,可以稍微的理解下。

    如上代码,如果数据有值发生改变的话,它就会调用 dep.notify()方法来通知所有的订阅者,因此会调用 Dep中的notice方法,我们继续跟踪下看下该对应的代码如下(源码在:src/core/observer/dep.js):

    import type Watcher from './watcher'
    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array;
      ....
      notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          // subs aren't sorted in scheduler if not running async
          // we need to sort them now to make sure they fire in correct
          // order
          subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
      ....
    }

    在notice方法中,我们循环遍历订阅者,然后会调用watcher里面的update的方法来进行派发更新操作。因此我们继续可以把视线转移到 src/core/observer/watcher.js 代码内部看下相对应的代码如下:

    export default class Watcher {
      ...
    
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      ...
    }

    如上update方法,首先会判断 this.lazy 是否为true,该参数的含义可以理解为懒加载类型。
    其次会判断this.sync 是否为同步类型,如果是同步类型的话,就会直接调用 run()函数方法,因此就会直接立刻执行回调函数。我们下面可以稍微简单的看下run()函数方法如下所示:

    run () {
      if (this.active) {
        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
        ) {
          // set new value
          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)
          }
        }
      }
    }

    如上代码我们可以看到,const value = this.get(); 获取到了最新值,然后立即调用 this.cb.call(this.vm, value, oldValue); 执行回调函数。
    否则的话就调用 queueWatcher(this);函数,从字面意思我们可以理解为队列Watcher, 也就是说,如果某一次数据发生改变的话,我们先把该更新的数据缓存起来,等到下一次DOM更新的时候会执行。我们可以理解为异步更新,异步更新往往是同一事件循环中多次修改同一个值,那么Watcher就会被缓存多次。

    理解同步更新和异步更新

    同步更新:

    上面代码中执行 this.run()函数是同步更新,所谓的同步更新是指当观察者的主体发生改变的时候会立刻执行回调函数,来触发更新代码。但是这种情况,在日常的开发中并不会有很多,在同一个事件循环中可能会改变很多次,如果我们每次都触发更新的话,那么对性能来讲会非常损耗的,因此在日常开发中,我们使用的异步更新比较多。

    异步更新:

    Vue异步执行DOM更新,只要观察到数据的变化,Vue将开启一个队列,如果同一个Watcher被触发多次,它只会被推入到队列中一次。那么这种缓冲对于去除一些重复操作的数据是很有必要的,因为它不会重复DOM操作。
    在下一次的事件循环nextTick中,Vue会刷新队列并且执行,Vue在内部会尝试对异步队列使用原生的Promise.then和MessageChannel。如果不支持原生的话,就会使用setTimeout(fn, 0)代替操作。

    我们现在再回到代码中,我们需要运行 queueWatcher (this) 函数,该函数的源码在 src/core/observer/scheduler.js 中,如下代码所示:

    let flushing = false;
    let has = {}; // 简单用个对象保存一下wather是否已存在
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          let 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
    
          if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue()
            return
          }
          nextTick(flushSchedulerQueue)
        }
      }
    }

    如上代码,首先获取 const id = watcher.id; 如果 if (has[id] == null) {} 为null的话,就执行代码,如果执行后会把 has[id] 设置为true。防止重复执行。接着代码又会判断 if (!flushing) {};如果flushing为false的话,就执行代码: queue.push(watcher); 可以理解为把 Watcher放入一个队列中,那为什么要判断 flushing 呢?那是因为假如我们正在更新队列中watcher的时候,这个时候我们的数据又被放入队列中怎么办呢?因此我们加了flushing这个参数来表示队列的更新状态。

    如上flushing代表的更新状态的含义,那么这个更新状态又分为2种情况。

    第一种情况是:flushing 为false,说明这个watcher还没有处理,就找到这个watcher在队列中的位置,并且把最新的放在后面,如代码:queue.push(watcher);

    第二种情况是:flushing 为true,说明这个watcher已经更新过了,那么就把这个watcher再放到当前执行的下一位,当前watcher处理完成后,再会立即处理这个新的。如下代码:

    let i = queue.length - 1
    while (i > index && queue[i].id > watcher.id) {
      i--
    }
    queue.splice(i + 1, 0, watcher);

    最后代码就会调用 nextTick 函数的代码去异步执行回调。nextTick下文会逐渐讲解到,我们这边只要知道他是异步执行即可。因此watcher部分代码先理解到此了。

    相关