vue2.x响应式原理

本文深入探讨Vue2.x的响应式原理,包括Observer如何转换data为getter/setter,Watcher如何记录依赖,以及Dep如何作为Observer和Watcher的桥梁。还讲解了ComputedWatcher的缓存机制、异步更新策略,并简要介绍了后续的编译和渲染过程。
摘要由CSDN通过智能技术生成

Talk is cheap,show me the code。

先抄一把官方文档的简单介绍吧,然后再贴个总览图,再细细说来。逐行源码分析,并不是单纯的理论。

当你把一个普通的 JavaScript 对象传入vue实例作为 data 选项,vue将遍历此对象所有的 property,并使用  Object.defineProperty  把这些 property 全部转为  getter/setter 。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是vue不支持 IE8 以及更低版本浏览器的原因。

这些 gettersetter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 gettersetter 的格式化并不同,所以建议安装  vue-devtools来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

data

Observer

这章节主要对上述Data(紫色那块)来个源码分析吧,基于2.16.14版本。

Data中的数据对象从配置属性用Observer类变成了访问器属性,访问器中的getter属性通过Dep类收集依赖(Watcher类),访问器中的setter属性通知依赖更新。

从observe方法开始,observe方法作用是筛查value,源码(有删减)如下:

function observe (value) {

   // 初步过滤一些不合适的value
   // 在对象的基础上,过滤虚拟节点对象,为什么要过滤掉虚拟节点?
   // Vnode是内部的一个类,什么全局方法会暴露出这个类呢?
   // 然而查找文档并没有,倒是存在一个Vnode接口暴露出来,也就解释了为什么要过滤掉VNode
   if (!isObject(value) || value instanceof VNode) { return }

   // 函数的最后会将这个观测的对象返回,这样做的目的为了递归侦测
   var ob;

   // 进一步过滤一些已经被observe的value
   // 是自身的属性而不是委托而来的__ob__属性,判定__ob__对象是否是Observer的实例    
   if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
      /* 最后把控综合作用下的value
         shouldObserve用来代码内部来判定是否可被侦测的时机
         isServerRendering()是否为服务端渲染,暂不考虑服务端渲染
         Array.isArray判断value是一个数组或者isPlainObject判断是一个普通的对象
         Object.isExtensible确保是一个可拓展的对象,不然新属性的增加会被忽略
         value._isVue确保不是一个Vue实例
       */
    } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
    ) {
      // 层层筛查下调用Observer类,生成ob实例
        ob = new Observer(value);
    }  
    return ob
}

Observer类生成ob实例,且是数组和对象不同响应式策略的分水岭。源码如下:

var Observer = function Observer (value) {
    // this.value = value; ob对包含对侦测value的引用
    // this.dep = new Dep(); ob中存一个dep对依赖的管理    
    this.value = value;
    this.dep = new Dep();

    // 在value上打上__ob__标签,表明已经被ob过了
    // 通过Object.defineProperty添加引用ob,且不可枚举
    def(value, '__ob__', this);

    /*
      数组和对象分别处理的分水岭,这里为什么会有分水岭?
      defineProperty仅作用于对象属性的更改,不包括增删。
      而没有监听数组的方式,怎么办呢?数组存在七个方法可以改变自身的,
      那么可以通过代理这七个方法来监听数组的变化。
      因此存在一些问题:
      数组某个值的改变是无法监听到的,数组中的长度增减是无法监听到的,
      通过索引的方式改变某个值无法监听
      针对对象,对象属性值的新增和删除是无法监听到的
      针对监听的不足,Vue通过两个全局的Api set和del来弥补
    */
    if (Array.isArray(value)) {
       /*
          数组方法拦截判断是否存在隐式原型即__prototype__,
          如果存在的话,直接将value的__prototype__指向arrayMethods
          function protoAugment (target, src) { target.__proto__ = src;}
          如果不存在,就在value上新增这七个代理方法并使之无法被枚举出来
          function copyAugment (target, src, keys) {
            for (var i = 0, l = keys.length; i < l; i++) {
              var key = keys[i];
              def(target, key, src[key]);
            }
          }
       */
        if (hasProto) {
            protoAugment(value, arrayMethods);
        } else {
            copyAugment(value, arrayMethods, arrayKeys);
        }

       /*
          用observeArray对每一个数组的元素进行侦听
          function observeArray (items) {
            for (var i = 0, l = items.length; i < l; i++) {
              observe(items[i]);
            }
        };

       */
        this.observeArray(value);
    } else {
       /*
          对象的话,用walk方法对每一个属性进行侦听,
          function walk (obj) {
             var keys = Object.keys(obj);
             for (var i = 0; i < keys.length; i++) {
                defineReactive(obj, keys[i]);
             }
          };
       */
        this.walk(value);
    }
};

数组方法如何拦截的细节是我们现在关注的重点,源代码如下:

/*
  var arrayMethods = Object.create(arrayProto);
  派生一个对象,隐式原型链委托到Aarry.prototype
  也就是arrayMethods.push()方法会调用Aarry.prototype.push()
*/
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

// 数组原生api中能改变自身的七种方法
var methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];
var arrayKeys = Object.getOwnPropertyNames(arrayMethods);

methodsToPatch.forEach(function (method) {
    // 用来缓存原始的数组方法,即Aarry.prototype上的方法
    var original = arrayProto[method];

    /*
      然后arrayMethods覆盖原先的七种方法,并且重写成mutator,
      来执行额外操作ob.dep.notify();通知依赖更新
    */
    def(arrayMethods, method, function mutator () {
      var args = [], len = arguments.length;
      // 浅复制,参数不会隔离,目的单纯的就是将一个类数组对象进行数组化
      while ( len-- ) args[ len ] = arguments[ len ];
      // 先执行原生操作
      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      // 对于新增的元素,继续ob
      if (inserted) { ob.observeArray(inserted); }
      ob.dep.notify();
      // 返回执行原生的结果,拦截完成
      return result
    });
});

Set和Del是两个全局api,存在的目的是弥补defineProperty响应式的不足,删减的源码如下:

function set (target, key, val) {
    // 如果是数组,则用已经被拦截的splice的方式来增加属性
    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
    }
    // target可能是一个表达式,确保其优先级
    var ob = (target).__ob__;
    // 排除非响应式系统中的数据
    if (!ob) {
      target[key] = val;
      return val
    }
    // 最后遴选出已经被ob的对象新增属性,加入到响应式中,并且通知依赖更新
    defineReactive(ob.value, key, val);
    ob.dep.notify();
    return val
  }

  function del (target, key) {
    // 依旧是splice方法
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.splice(key, 1);
      return
    }
    var ob = (target).__ob__;
    // 检测非自有属性
    if (!hasOwn(target, key)) { return }
    delete target[key];
    // 检测非ob的对象
    if (!ob) { return }
    ob.dep.notify();
  }

最后看一下核心的方法defineReactive,也是整个响应式系统侦听数据的核心,源码如下:

// 参数说明:响应式的对象,key和值
function defineReactive$$1 (obj, key, val) {
    /*
      为每一个属性实例化一个dep用来管理依赖
      和Observer对象里new Dep()不同的是那是对于对象本身而不是其属性,例如:
      data: {
        a: {
          b:'c'
        }
      }
      a赋值成'd',是由Observer中的dep管理,而a.b赋值成'd'是这里的dep管理依赖
      说白了就是一个深度依赖,我们在外部是看不到属性值是原始值的dep,
      它们被困在函数的闭包里,也就是下面的setter/getter
    */
    var dep = new Dep();

    // 检查对象中某个属性是否可配置
    // 不可配置就是没有get和set方法直接return
    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) { return }

    /*
      对于get和set的检查,目的是正确的取属性值,有的对象属性可能已经被configurable过,
      存在setter或者getter方法或者两个都存在,那么就会对val进行一个判定获取的规则
      如果没有设置getter方法,又没有传入val那么只能从obj[key]获取,
      问题是没有get方法这样能获取到吗?
      没有设置get方法,默认返回undefined,
      也就是configurable过后没有设置get方法就是undefined,就是undefined
      如果设置了get方法,没有设置set方法,那么val的值暂时不能获取到,特殊情况是:
      插入以下代码到此处
      Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get:()=>({a:'b'}),
      })
      此时val为undefined,childOb为undefined,
      这就存在疑惑,这样不能进行深度依赖!
      Why?为什么这样?
      没有设置set方法,说明这个属性只能读,对于只能读的属性,主观上只读,
      Vue尊重主观意愿,因此加入响应式没有意义!
      需要注意的是:这会导致属性原有的 set 和 get 方法被覆盖,
      所以要将属性原有的 setter/getter 缓存
    */
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    /*
      如果没有get方法,又没有传入val那么只能从obj[key]获取,
      如果值是一个对象那么进行递归处理
      注意这里的赋值,是赋给子对象的ob对象,ob对象中存在:
      this.value = value;
      this.dep = new Dep();
      这里已经形成了层层被侦听,以及每一层都存在ob.dep存依赖,
      有点抽象举个简单的例子:
      data: {
         a:{
           e:234
           b:{
              c:{
                 d:123
              }
           }
         }
      }
      data的整个对象的ob中的dep的id为0,a属性ob中的dep的id为1,
      a属性对象(a属性的值为对象或者数组)ob中的dep的id为2,依次
      e属性3,b属性4,b对象5,c属性6,c对象7,d属性8
      a属性和a属性对象有说明区别吗?
      this.a = {},这时候对a属性重新赋值,可以检测到这样的更新
      但是当this.a.e = {},显然是无法检测到重新赋值的更新,
      因此还需要一个a对象:
      {
        e:234,
        b:{
          ...
        }
        __ob__: {...}
      }
    */
    var childOb = observe(val);
        
    /*
      将对象obj中的每一个属性重新配置成get/set
      dep.depend();在get中收集依赖
      dep.notify();在set中通知依赖更新
    */
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        // 取值来说如果已经存在get,因为存在已经被配置的情况,
        // 那么直接调用get函数来获取属性值
        var value = getter ? getter.call(obj) : val;

        /*
          Dep紧接下文会说到
          Dep.target是一个依赖,也就是watcher
          dep.depend();是收集这个当前的依赖,即Dep.target的指向
          如果存在属性对象的ob对象
          childOb.dep.depend();那么继续收集依赖到属性对象的ob.dep上
          如果属性值对象是一个数组,
          那么递归的逐个收集数组中每个元素的依赖
          function dependArray (value) {
            for (var e = (void 0),
               i = 0, l = value.length; i < l; i++) {
               e = value[i];
               e && e.__ob__ && e.__ob__.dep.depend();
               if (Array.isArray(e)) {
                 dependArray(e);
               }
            }
          }         
        */
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },

      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        // 比较新旧值是否相等, 后者是考虑NaN情况
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }

        // 只读属性重新定义set不会加入响应式
        if (getter && !setter) { return }

        // 没有get,仅仅只有set和没有get和没有set两种情况都是返回newVal
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }

        // 给对象值添加侦听
        childOb = observe(newVal);
        dep.notify();
      }
    });
}

Watcher

Watcher对应总览图的紫色的部分,也叫依赖,它是Watcher实现的,什么是依赖?用到上述响应式数据的地方就叫依赖,比如,视图用到了数据,我们称之为视图依赖,也叫渲染依赖,computed中用到了数据,我们称之为计算依赖,也叫惰性依赖,watch中用到的数据,称之为侦听器依赖。惰性依赖的源码如下:

// computed计算属性是一个依赖
// 在实例上用_computedWatchers和_watcher进行区分
function initComputed (vm, computed) {
    var watchers = vm._computedWatchers = Object.create(null);
    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      );
    }
  }

侦听器依赖的源码如下:

// 可以根据不同的配置和形式来生成侦听器依赖,用法可以看官方文档api
  function initWatch (vm, watch) {
     for (var key in watch) {
          var handler = watch[key];
          // 如果是数组的话,数组元素逐个创建
          if (Array.isArray(handler)) {
            for (var i = 0; i < handler.length; i++) {
              createWatcher(vm, key, handler[i]);
            }
          } else {
          // 对于其他值,同样只会创建一个依赖
            createWatcher(vm, key, handler);
          }
     }
  }

  function createWatcher (
        vm,
        expOrFn,
        handler,
        options
  ) { return vm.$watch(expOrFn, handler, options) }

  Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
  ) {
      var vm = this;
      if (isPlainObject(cb)) {
        // 递归生成侦听器依赖
        return createWatcher(vm, expOrFn, cb, options)
      }
      var watcher = new Watcher(vm, expOrFn, cb, options);
      return function unwatchFn () {
        watcher.teardown();
      }
  };

最后是视图依赖,源码如下:

// 在beforeMount之后,就是生成初始化实例即将进行挂载
   new Watcher(vm, updateComponent, noop, {
          before: function before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate');
          }
      }
   }, true /* isRenderWatcher */);

它们都是来自于Watcher类:

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    // 视图依赖的标示
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // 传入依赖的配置
    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$2;
    this.active = true;
    this.dirty = this.lazy; // 惰性依赖
    // 防止依赖收集的细节,暂时不需要关注
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    this.getter = expOrFn;
    // 当是惰性依赖的时候,并不执行get()方法
    this.value = this.lazy
      ? undefined
      : this.get();
  };

再来看看get方法,重点关注是不同的依赖如何收集的,源码如下:

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var 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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

  /*
    先是计算依赖,并不会执行这个方法,此时watcher.value为undefined,也不会进行依赖收集。
    然后是侦听器依赖,会执行this.getter方法,watch内声明的方法会先执行,这里会进行依赖收集,
    即内部的数据会首先收集到当前的侦听器依赖。
    最后是视图依赖,会调用render函数,render会调用响应式数据中的getter,
    其中包括获取计算依赖的值,进行依赖收集。
    当某一个响应式系统中的数据变化了,会调用setter方法,通知这个数据下的所有依赖更新
  */

Dep

Dep类是总览图中的Data到Watcher的联系,也就是Observe和Watcher的桥梁。在讨论Dep的作用时,先考虑下Dep.target的作用,如下源码所示:

  // Dep.target是一个watcher,也就是所说的依赖,
  // Dep.target 保持唯一性是因为同一时间只会有一个 watcher 被计算,
  // Dep.target 就表示正在计算的 watcher
  // targetStack是用来管理当前的依赖

  /* 如果没记错的话 targetStack 是 Vue2 中才引入的机制,
     而 Vue1 中则是仅靠 Dep.target 来进行依赖收集的。
     根据我自己对 Vue1 和 Vue2 差异的理解,
     引入 targetStack 的原因在于 Vue2 使用了新的视图更新方式。
     具体来说,vue1 视图更新采用的是细粒度绑定的方式,而 vue2 采取的是 virtual DOM 的方式。 
     举个例子来说可能比较容易理解,对于下面的模版:
     div>{{ a }}<my :text="b"></my> {{ c }}<div><span>{{ b }}</span>
     Vue1 的处理方式可以简化理解为:watch(for a) -> directive(update {{ a }})watch(for b) -> directive(update {{ b }})watch(for c) -> directive(update {{ c }})
     由于是数据到 DOM 操作操作指令的细粒度绑定,所以不论是指令还是 watcher 都是原子化的。
     对于上面的模版,在处理完{{ a }}的视图绑定后,
     创建新的 vue 实例 my 并且处理{{ b }}的视图绑定,随后继续处理{ c }}的绑定      
     而在 Vue2 中情况就完全不同,视图被抽象为一个 render 函数,
     一个 render 函数只会生成一个 watcher,其处理机制可以简化理解为:
     renderRoot () { renderMy ()...}
     可以看到在 Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,
     有嵌套调用就会有调用栈。当 evaluate root 时,调用到 my 的 render 函数,
     此时就需要中断 root 而进行 my 的 evaluate,
     当 my 的 evaluate 结束后 root 将会继续进行,这就是 targetStack 的意义。
  */
  Dep.target = null;
  var targetStack = [];

  function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  }

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }

看看Dep类的实现:

// 某一个数据对应一个dep,subs中存着这个数据对应的依赖
  var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };
  // 加依赖
  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
  };

  // 移除依赖
  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  };

  // 依赖收集,依赖收集数据,数据收集依赖,双向的
  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

  // 通知依赖更新
  Dep.prototype.notify = function notify () {
    // 稳定sub,在update时确保不会变动
    var subs = this.subs.slice();
    // 暂时不考虑异步
    if (!config.async) {
      /*
         排序是给依赖进行排序,先侦听器依赖,然后按照视图中的顺序进行依赖
         这样保证数据的按顺序合理显示
      */
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    // 依赖的逐个更新
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

ComputedWatcher的缓存原理

//  初始化计算属性  _init() => initState() => initComputed() => createComputedGetter
// return是一个函数,是记忆函数常用的操作	  
  function createComputedGetter (key) {
    return function computedGetter () {
      // 判断实例上存在计算属性依赖
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        /** 
        	依赖上的dirty是懒加载的flag
        	evaluate只会在lazy watcher执行
        	Watcher.prototype.evaluate = function evaluate () {
                this.value = this.get();
                this.dirty = false;
            };
            该函数调用get(),然后将该flag设为false,
            下一次访问的时候就不会执行watcher.evaluate();
            而是直接返回watcher.value
            看这个Watcher.prototype.get方法 
            在get中调用的this现在并不是渲染依赖而是计算属性依赖
            this.getter.call(vm, vm);就是调用计算属性值函数
        */
        if (watcher.dirty) {
          watcher.evaluate();
        }
        // 上面计算属性的target退栈了,这里是视图依赖
        // this.cleanupDeps()上面清空,这里需要重新收集
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }

  // 没有缓存直接调用,和方法的执行没什么两样了
  function createGetterInvoker(fn) {
    return function computedGetter () {
      return fn.call(this, this)
    }
  }

走一下流程(完全理解Vue的渲染watcher、computed和user watcher_前端开发博客-CSDN博客):

  • 1、首先在render函数里面会读取this.info,这个会触发createComputedGetter(key)中的computedGetter(key)

  • 2、然后会判断watcher.dirty,执行watcher.evaluate()

  • 3、进到watcher.evaluate(),才真想执行this.get方法,这时候会执行pushTarget(this)把当前的computed watcher push到stack里面去,并且把Dep.target 设置成当前的computed watcher`;

  • 4、然后运行this.getter.call(vm, vm) 相当于运行computedinfo: function() { return this.name + this.age },这个方法;

  • 5、info函数里面会读取到this.name,这时候就会触发数据响应式Object.defineProperty.get的方法,这里name会进行依赖收集,把watcer收集到对应的dep上面;并且返回name = '张三'的值,age收集同理;

  • 6、依赖收集完毕之后执行popTarget(),把当前的computed watcher从栈清除,返回计算后的值('张三+10'),并且this.dirty = false

  • 7、watcher.evaluate()执行完毕之后,就会判断Dep.target 是不是true,如果有就代表还有渲染watcher,就执行watcher.depend(),然后让watcher里面的deps都收集渲染watcher,这就是双向保存的优势。

  • 8、此时name都收集了computed watcher 和 渲染watcher。那么设置name的时候都会去更新执行watcher.update()

  • 9、如果是computed watcher的话不会重新执行一遍只会把this.dirty 设置成 true,如果数据变化的时候再执行watcher.evaluate()进行info更新,没有变化的的话this.dirty 就是false,不会执行info方法。这就是computed缓存机制。

异步更新策略

这章节是图中Watcher到黄色的过程分析,主要是nextTick的源码分析:

// 回顾下nextTick的官方介绍,主要是要给异步更新策略

/*
  可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,
  并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,
  只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
  然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
  Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,
  如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
  例如,当你设置 vm.someData = 'new value',
  该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。
  多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,
  这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,
  避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,
  可以在数据变化之后立即使用 Vue.nextTick(callback)。
  这样回调函数将在 DOM 更新完成后被调用。例如:
  <div id="example">{{message}}</div>
    var vm = new Vue({
      el: '#example',
      data: {
        message: '123'
      }
    })
    vm.message = 'new message' // 更改数据
    vm.$el.textContent === 'new message' // false
    Vue.nextTick(function () {
      vm.$el.textContent === 'new message' // true
    })
*/

//  如果是同步的,那么直接run去更新视图
//  否则将依赖推入一个依赖队列queueWatcher
  Watcher.prototype.update = function update () {
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

// queueWatcher依赖队列,需要整体理解的一个过程
// 为了弄懂每一个标志的作用,需要整体的看一下
  var queue = [];
  var activatedChildren = [];
  var has = {};
  var circular = {}; // 阻止循环更新依赖
  var waiting = false;
  var flushing = false;
  var index = 0;

  function queueWatcher (watcher) {
    var 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.
        var 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 (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }

/*
  当进来了第一个依赖,
  has[id] = true,queue里push了这个依赖,waiting为true
  如果是同步的那么直接执行flushSchedulerQueue
  否则进入了nextTick
  首先看下同步更新策略的情况,即执行flushSchedulerQueue
  记录当前刷新队列的时间戳
  flushing标志位为true,说明队列正在刷新
  当第二个依赖进来,
  因为是同步的原因已经执行完了resetSchedulerState()重置掉了flushing为false
  一个接着一个
  看下异步更新策略也就是nextTick,只有等下一次事件循环才会调用flushSchedulerQueue
  当第二个依赖进来,入队列
  当第三个依赖进来,依旧入队列
  等到下一次事件循环才会执行flushSchedulerQueue
  所以很好理解has,flushing,waiting的作用
  has是用来防止重复的依赖入队列
  flushing是是否正在调用flushSchedulerQueue,因为flushSchedulerQueue可能是一个异步
  waiting是等待下一个nextTick
  flushSchedulerQueue是一个异步执行的话,继续可以执行queueWatcher()
  动态的将queue排序,确保按顺序更新
  这里的splice是神来之笔,处理一些边界情况,如依赖的id小与当前更新的依赖id那么需要立即插入
  var i = queue.length - 1;
  while (i > index && queue[i].id > watcher.id) {
    i--;
  }
  queue.splice(i + 1, 0, watcher);
*/

  function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    //...
    resetSchedulerState();
  }

// nextTick
  var callbacks = [];
  var pending = false;
  function nextTick (cb, ctx) {
    var _resolve;
    // 将cb推出队列
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    // 和waiting的作用一样
    if (!pending) {
      pending = true;
      timerFunc();
    }

    // 没cb的边界情况
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }

/*
  这里把注释贴出来看看,第一段注释主要讲述使用宏任务还是微任务的抉择
  这段代码的主要作用其实是兼容性的尝试,
  大多数情况下优选依旧是Promise
  因此核心是:
  p.then(flushCallbacks);
*/

// 这里我们有使用微任务的异步延迟包装器。
// 在 2.5 中,我们使用了(宏)任务(结合微任务)。
// 但是,在重绘之前更改状态时会出现一些微妙的问题
//(例如#6813,出入转换)。
// 此外,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
// 无法绕过的(例如#7109、#7153、#7546、#7834、#8109)。
// 所以我们现在再次使用微任务。
// 这种权衡的一个主要缺点是有一些场景
// 微任务的优先级太高,并且应该在两者之间触发
// 顺序事件(例如#4521、#6690,有变通方法)
// 甚至在同一事件的冒泡之间(#6566)。
  var timerFunc;
// nextTick 行为利用了可以访问的微任务队列
// 通过原生 Promise.then 或 MutationObserver。
// MutationObserver 有更广泛的支持,但它被严重窃听
// 当在触摸事件处理程序中触发时,iOS 中的 UIWebView >= 9.3.3。 它
// 触发几次后完全停止工作......所以,如果是原生的
// Promise 可用,我们将使用它:
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      if (isIOS) { setTimeout(noop); }
    };
    isUsingMicroTask = true;
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

// flushCallbacks
  function flushCallbacks () {
    // 更换pending的状态
    pending = false;
    var copies = callbacks.slice(0);
    // 浅复制后清空callbacks
    callbacks.length = 0;
    // 还是异步调用了flushSchedulerQueue
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }

// flushSchedulerQueue
// watcher.run();没有给watcher进行run一下
  function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    var watcher, id;
    // 在刷新前对队列进行排序。
    // 这确保:
    // 1. 组件从父级更新为子级。 (因为父母总是
    // 在孩子之前创建)
    // 2. 组件的用户观察者在其渲染观察者之前运行(因为
    // 在渲染观察者之前创建用户观察者)
    // 3. 如果一个组件在父组件的观察者运行期间被销毁,
    // 可以跳过它的观察者。
    queue.sort(function (a, b) { return a.id - b.id; });
    // 当我们运行现有的观察者时,不要缓存长度,因为可能会推送更多的观察者
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      if (watcher.before) {
        watcher.before();
      }
      id = watcher.id;
      has[id] = null;
      watcher.run();
      // 在开发构建中,检查并停止循环更新。
      if (has[id] != null) {
        circular[id] = (circular[id] || 0) + 1;
        if (circular[id] > MAX_UPDATE_COUNT) {
          warn(
            'You may have an infinite update loop ' + (
              watcher.user
                ? ("in watcher with expression \"" + (watcher.expression) + "\"")
                : "in a component render function."
            ),
            watcher.vm
          );
          break
        }
      }
    }
    resetSchedulerState();
  }

// 重置状态
  function resetSchedulerState () {
    index = queue.length = activatedChildren.length = 0;
    has = {};
    {
      circular = {};
    }
    waiting = flushing = false;
  }

后续编译和渲染

后续的编译和渲染实在懒得写,意义也不大,也脱离了本文的主题,直接简述下吧,加深下整体的认识。

编译

平时使用模板时,可以在模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那vue中为什么可以实现?这就归功于模板编译功能。

模板编译的作用是生成渲染函数,通过执行渲染函数生成最新的vnode,最后根据vnode进行渲染。那么,如何将模板编译成渲染函数?此过程可以分成两个步骤:先将模板解析成AST(abstract syntax tree,抽象语法树),然后使用AST生成渲染函数。

由于静态节点不需要总是重新渲染,所以生成AST之后,生成渲染函数之前这个阶段,需要做一个优化操作:遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:

  1. 将模板解析成AST
  2. 遍历AST标记静态节点
  3. 使用AST生成渲染函数

这三部分内容在模板编译中分别抽象出三个模块实现各自的功能:解析器、优化器和代码生成器。

渲染

还是通过一个例子来看渲染的过程。假设给定如下模板:

<div id="app" @click="add">{{count}}</div>

通过上述的编译过程,得到渲染函数:

function render() {
  with(this){
    return _c('div', {attrs:{"id":"app"}, on:{"click":add}}, [_v(_s(count))])
  }
}

_c、_v、_s是生成不同的虚拟节点vnode,vnode 通过 parent 和 children 连接父节点和子节点,组成vnode树。有了vnode后,vue还需要根据vnode来创建DOM节点。如果是首次渲染,那么vue会走创建的逻辑。如果是数据的更新导致的重新渲染,那么vue会走更新的逻辑。

如果不是首次渲染,而是由数据变化所触发的重新渲染,那么vue会最大限度地复用已创建的DOM元素。而复用的前提就是通过比较新老vnode,找出需要更新的内容,然后最小限度地进行替换。这也是vue设计vnode的核心用途。

大量的DOM操作会极损耗浏览器性能。vue在每次数据发生变化后,都会重新生成vnode节点。通过比较新老vnode节点,找出需要进行操作的最小DOM元素子集。根据变化点,进行DOM元素属性、DOM子节点的更新。这种设计方式大大减少了DOM操作的次数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值