vue2.x响应式原理

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,从而使它关联的组件重新渲染。

vue2.x响应式原理_第1张图片

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 }} {{ c }}
{{ b }} 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 更新完成后被调用。例如:
  
{{message}}
    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生成渲染函数

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

渲染

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

{{count}}

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

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操作的次数。

你可能感兴趣的:(架构思维,vue.js)