为了深刻介绍响应式零碎的外部实现原理,咱们花了一整节的篇幅介绍了数据(包含data, computed,props)如何初始化成为响应式对象的过程。有了响应式数据对象的常识,上一节的后半局部咱们还在保留源码构造的根底上构建了一个以data为数据的响应式零碎,而这一节,咱们持续深刻响应式零碎外部构建的细节,详细分析Vue在响应式零碎中对data,computed的解决。

7.8 相干概念

在构建繁难式响应式零碎的时候,咱们引出了几个重要的概念,他们都是响应式原理设计的外围,咱们先简略回顾一下:

  • Observer类,实例化一个Observer类会通过Object.defineProperty对数据的getter,setter办法进行改写,在getter阶段进行依赖的收集,在数据产生更新阶段,触发setter办法进行依赖的更新
  • watcher类,实例化watcher类相当于创立一个依赖,简略的了解是数据在哪里被应用就须要产生了一个依赖。当数据产生扭转时,会告诉到每个依赖进行更新,后面提到的渲染wathcer便是渲染dom时应用数据产生的依赖。
  • Dep类,既然watcher了解为每个数据须要监听的依赖,那么对这些依赖的收集和告诉则须要另一个类来治理,这个类便是Dep,Dep须要做的只有两件事,收集依赖和派发更新依赖。

这是响应式零碎构建的三个根本外围概念,也是这一节的根底,如果还没有印象,请先回顾上一节对极简风响应式零碎的构建

7.9 data

7.9.1 问题思考

在开始剖析data之前,咱们先抛出几个问题让读者思考,而答案都蕴含在接下来内容分析中。

  • 后面曾经晓得,Dep是作为治理依赖的容器,那么这个容器在什么时候产生?也就是实例化Dep产生在什么时候?
  • Dep收集了什么类型的依赖?即watcher作为依赖的分类有哪些,别离是什么场景,以及区别在哪里?
  • Observer这个类具体对getter,setter办法做了哪些事件?
  • 手写的watcher和页面数据渲染监听的watch如果同时监听到数据的变动,优先级怎么排?
  • 有了依赖的收集是不是还有依赖的解除,依赖解除的意义在哪里?

带着这几个问题,咱们开始对data的响应式细节开展剖析。

7.9.2 依赖收集

data在初始化阶段会实例化一个Observer类,这个类的定义如下(疏忽数组类型的data):

// initData function initData(data) {  ···  observe(data, true)}// observefunction observe(value, asRootData) {  ···  ob = new Observer(value);  return ob}// 观察者类,对象只有设置成领有察看属性,则对象下的所有属性都会重写getter和setter办法,而getter,setting办法会进行依赖的收集和派发更新var Observer = function Observer (value) {    ···    // 将__ob__属性设置成不可枚举属性。内部无奈通过遍历获取。    def(value, '__ob__', this);    // 数组解决    if (Array.isArray(value)) {        ···    } else {      // 对象解决      this.walk(value);    }  };function def (obj, key, val, enumerable) {  Object.defineProperty(obj, key, {    value: val,    enumerable: !!enumerable, // 是否可枚举    writable: true,    configurable: true  });}

Observer会为data增加一个__ob__属性, __ob__属性是作为响应式对象的标记,同时def办法确保了该属性是不可枚举属性,即外界无奈通过遍历获取该属性值。除了标记响应式对象外,Observer类还调用了原型上的walk办法,遍历对象上每个属性进行getter,setter的改写。

Observer.prototype.walk = function walk (obj) {    // 获取对象所有属性,遍历调用defineReactive###1进行改写    var keys = Object.keys(obj);    for (var i = 0; i < keys.length; i++) {        defineReactive###1(obj, keys[i]);    }};

defineReactive###1是响应式构建的外围,它会先实例化一个Dep类,即为每个数据都创立一个依赖的治理,之后利用Object.defineProperty重写getter,setter办法。这里咱们只剖析依赖收集的代码。

function defineReactive###1 (obj,key,val,customSetter,shallow) {    // 每个数据实例化一个Dep类,创立一个依赖的治理    var dep = new Dep();    var property = Object.getOwnPropertyDescriptor(obj, key);    // 属性必须满足可配置    if (property && property.configurable === false) {      return    }    // cater for pre-defined getter/setters    var getter = property && property.get;    var setter = property && property.set;    // 这一部分的逻辑是针对深层次的对象,如果对象的属性是一个对象,则会递归调用实例化Observe类,让其属性值也转换为响应式对象    var childOb = !shallow && observe(val);    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,s      get: function reactiveGetter () {        var value = getter ? getter.call(obj) : val;        if (Dep.target) {          // 为以后watcher增加dep数据          dep.depend();          if (childOb) {            childOb.dep.depend();            if (Array.isArray(value)) {              dependArray(value);            }          }        }        return value      },      set: function reactiveSetter (newVal) {}    });  }

次要看getter的逻辑,咱们晓得当data中属性值被拜访时,会被getter函数拦挡,依据咱们旧有的常识体系能够晓得,实例挂载前会创立一个渲染watcher

new Watcher(vm, updateComponent, noop, {  before: function before () {    if (vm._isMounted && !vm._isDestroyed) {      callHook(vm, 'beforeUpdate');    }  }}, true /* isRenderWatcher */);

与此同时,updateComponent的逻辑会执行实例的挂载,在这个过程中,模板会被优先解析为render函数,而render函数转换成Vnode时,会拜访到定义的data数据,这个时候会触发gettter进行依赖收集。而此时数据收集的依赖就是这个渲染watcher自身。

代码中依赖收集阶段会做上面几件事:

  1. 为以后的watcher(该场景下是渲染watcher)增加领有的数据
  2. 为以后的数据收集须要监听的依赖

如何了解这两点?咱们先看代码中的实现。getter阶段会执行dep.depend(),这是Dep这个类定义在原型上的办法。

dep.depend();Dep.prototype.depend = function depend () {    if (Dep.target) {      Dep.target.addDep(this);    }  };

Dep.target为以后执行的watcher,在渲染阶段,Dep.target为组件挂载时实例化的渲染watcher,因而depend办法又会调用以后watcheraddDep办法为watcher增加依赖的数据。

Watcher.prototype.addDep = function addDep (dep) {    var id = dep.id;    if (!this.newDepIds.has(id)) {      // newDepIds和newDeps记录watcher领有的数据      this.newDepIds.add(id);      this.newDeps.push(dep);      // 防止反复增加同一个data收集器      if (!this.depIds.has(id)) {        dep.addSub(this);      }    }  };

其中newDepIds是具备惟一成员是Set数据结构,newDeps是数组,他们用来记录以后watcher所领有的数据,这一过程会进行逻辑判断,防止同一数据增加屡次。

addSub为每个数据依赖收集器增加须要被监听的watcher

Dep.prototype.addSub = function addSub (sub) {  //将以后watcher增加到数据依赖收集器中    this.subs.push(sub);};
  1. getter如果遇到属性值为对象时,会为该对象的每个值收集依赖

这句话也很好了解,如果咱们将一个值为根本类型的响应式数据扭转成一个对象,此时新增对象里的属性,也须要设置成响应式数据。参考

  1. 遇到属性值为数组时,进行非凡解决,这点放到前面讲。

艰深的总结一下依赖收集的过程,每个数据就是一个依赖管理器,而每个应用数据的中央就是一个依赖。当拜访到数据时,会将以后拜访的场景作为一个依赖收集到依赖管理器中,同时也会为这个场景的依赖收集领有的数据。

参考 前端进阶面试题具体解答

7.9.3 派发更新

在剖析依赖收集的过程中,可能会有不少困惑,为什么要保护这么多的关系?在数据更新时,这些关系会起到什么作用?带着纳闷,咱们来看看派发更新的过程。
在数据产生扭转时,会执行定义好的setter办法,咱们先看源码。

Object.defineProperty(obj,key, {  ···  set: function reactiveSetter (newVal) {      var value = getter ? getter.call(obj) : val;      // 新值和旧值相等时,跳出操作      if (newVal === value || (newVal !== newVal && value !== value)) {        return      }      ···      // 新值为对象时,会为新对象进行依赖收集过程      childOb = !shallow && observe(newVal);      dep.notify();    }})

派发更新阶段会做以下几件事:

  • 判断数据更改前后是否统一,如果数据相等则不进行任何派发更新操作
  • 新值为对象时,会对该值的属性进行依赖收集过程
  • 告诉该数据收集的watcher依赖,遍历每个watcher进行数据更新,这个阶段是调用该数据依赖收集器的dep.notify办法进行更新的派发。
Dep.prototype.notify = function notify () {    var subs = this.subs.slice();    if (!config.async) {      // 依据依赖的id进行排序      subs.sort(function (a, b) { return a.id - b.id; });    }    for (var i = 0, l = subs.length; i < l; i++) {      // 遍历每个依赖,进行更新数据操作。      subs[i].update();    }  };
  • 更新时会将每个watcher推到队列中,期待下一个tick到来时取出每个watcher进行run操作
 Watcher.prototype.update = function update () {    ···    queueWatcher(this);  };

queueWatcher办法的调用,会将数据所收集的依赖顺次推到queue数组中,数组会在下一个事件循环'tick'中依据缓冲后果进行视图更新。而在执行视图更新过程中,难免会因为数据的扭转而在渲染模板上增加新的依赖,这样又会执行queueWatcher的过程。所以须要有一个标记位来记录是否处于异步更新过程的队列中。这个标记位为flushing,当处于异步更新过程时,新增的watcher会插入到queue中。

function queueWatcher (watcher) {    var id = watcher.id;    // 保障同一个watcher只执行一次    if (has[id] == null) {      has[id] = true;      if (!flushing) {        queue.push(watcher);      } else {        var i = queue.length - 1;        while (i > index && queue[i].id > watcher.id) {          i--;        }        queue.splice(i + 1, 0, watcher);      }      ···      nextTick(flushSchedulerQueue);    }  }

nextTick的原理和实现先不讲,概括来说,nextTick会缓冲多个数据处理过程,等到下一个事件循环tick中再去执行DOM操作,它的原理,实质是利用事件循环的微工作队列实现异步更新

当下一个tick到来时,会执行flushSchedulerQueue办法,它会拿到收集的queue数组(这是一个watcher的汇合),并对数组依赖进行排序。为什么进行排序呢?源码中解释了三点:

  • 组件创立是先父后子,所以组件的更新也是先父后子,因而须要保障父的渲染watcher优先于子的渲染watcher更新。
  • 用户自定义的watcher,称为user watcheruser watcherrender watcher执行也有先后,因为user watchersrender watcher要先创立,所以user watcher要优先执行
  • 如果一个组件在父组件的 watcher 执行阶段被销毁,那么它对应的 watcher 执行都能够被跳过。
function flushSchedulerQueue () {    currentFlushTimestamp = getNow();    flushing = true;    var watcher, id;    // 对queue的watcher进行排序    queue.sort(function (a, b) { return a.id - b.id; });    // 循环执行queue.length,为了确保因为渲染时增加新的依赖导致queue的长度一直扭转。    for (index = 0; index < queue.length; index++) {      watcher = queue[index];      // 如果watcher定义了before的配置,则优先执行before办法      if (watcher.before) {        watcher.before();      }      id = watcher.id;      has[id] = null;      watcher.run();      // in dev build, check and stop circular updates.      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        }      }    }    // keep copies of post queues before resetting state    var activatedQueue = activatedChildren.slice();    var updatedQueue = queue.slice();    // 重置复原状态,清空队列    resetSchedulerState();    // 视图扭转后,调用其余钩子    callActivatedHooks(activatedQueue);    callUpdatedHooks(updatedQueue);    // devtool hook    /* istanbul ignore if */    if (devtools && config.devtools) {      devtools.emit('flush');    }  }

flushSchedulerQueue阶段,重要的过程能够总结为四点:

  • queue中的watcher进行排序,起因下面曾经总结。
  • 遍历watcher,如果以后watcherbefore配置,则执行before办法,对应后面的渲染watcher:在渲染watcher实例化时,咱们传递了before函数,即在下个tick更新视图前,会调用beforeUpdate生命周期钩子。
  • 执行watcher.run进行批改的操作。
  • 重置复原状态,这个阶段会将一些流程管制的状态变量复原为初始值,并清空记录watcher的队列。
new Watcher(vm, updateComponent, noop, {  before: function before () {    if (vm._isMounted && !vm._isDestroyed) {      callHook(vm, 'beforeUpdate');    }  }}, true /* isRenderWatcher */);

重点看看watcher.run()的操作。

Watcher.prototype.run = function run () {    if (this.active) {      var value = this.get();      if ( value !== this.value || isObject(value) || this.deep ) {        // 设置新值        var oldValue = this.value;        this.value = value;        // 针对user watcher,临时不剖析        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);        }      }    }  };

首先会执行watcher.prototype.get的办法,失去数据变动后的以后值,之后会对新值做判断,如果判断满足条件,则执行cb,cb为实例化watcher时传入的回调。

在剖析get办法前,回头看看watcher构造函数的几个属性定义

var watcher = function Watcher(  vm, // 组件实例  expOrFn, // 执行函数  cb, // 回调  options, // 配置  isRenderWatcher // 是否为渲染watcher) {  this.vm = vm;    if (isRenderWatcher) {      vm._watcher = this;    }    vm._watchers.push(this);    // options    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; // uid for batching    this.active = true;    this.dirty = this.lazy; // for lazy watchers    this.deps = [];    this.newDeps = [];    this.depIds = new _Set();    this.newDepIds = new _Set();    this.expression = expOrFn.toString();    // parse expression for getter    if (typeof expOrFn === 'function') {      this.getter = expOrFn;    } else {      this.getter = parsePath(expOrFn);      if (!this.getter) {        this.getter = noop;        warn(          "Failed watching path: \"" + expOrFn + "\" " +          'Watcher only accepts simple dot-delimited paths. ' +          'For full control, use a function instead.',          vm        );      }    }    // lazy为计算属性标记,当watcher为计算watcher时,不会了解执行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) {     ···    } finally {      ···      // 把Dep.target复原到上一个状态,依赖收集过程实现      popTarget();      this.cleanupDeps();    }    return value  };

get办法会执行this.getter进行求值,在以后渲染watcher的条件下,getter会执行视图更新的操作。这一阶段会从新渲染页面组件

new Watcher(vm, updateComponent, noop, { before: () => {} }, true);updateComponent = function () {  vm._update(vm._render(), hydrating);};

执行完getter办法后,最初一步会进行依赖的革除,也就是cleanupDeps的过程。

对于依赖革除的作用,咱们列举一个场景: 咱们常常会应用v-if来进行模板的切换,切换过程中会执行不同的模板渲染,如果A模板监听a数据,B模板监听b数据,当渲染模板B时,如果不进行旧依赖的革除,在B模板的场景下,a数据的变动同样会引起依赖的从新渲染更新,这会造成性能的节约。因而旧依赖的革除在优化阶段是有必要。
// 依赖革除的过程  Watcher.prototype.cleanupDeps = function cleanupDeps () {    var i = this.deps.length;    while (i--) {      var dep = this.deps[i];      if (!this.newDepIds.has(dep.id)) {        dep.removeSub(this);      }    }    var tmp = this.depIds;    this.depIds = this.newDepIds;    this.newDepIds = tmp;    this.newDepIds.clear();    tmp = this.deps;    this.deps = this.newDeps;    this.newDeps = tmp;    this.newDeps.length = 0;  };

把下面剖析的总结成依赖派发更新的最初两个点

  • 执行run操作会执行getter办法,也就是从新计算新值,针对渲染watcher而言,会从新执行updateComponent进行视图更新
  • 从新计算getter后,会进行依赖的革除

7.10 computed

计算属性设计的初衷是用于简略运算的,毕竟在模板中放入太多的逻辑会让模板过重且难以保护。在剖析computed时,咱们仍旧遵循依赖收集和派发更新两个过程进行剖析。

7.10.1 依赖收集

computed的初始化过程,会遍历computed的每一个属性值,并为每一个属性实例化一个computed watcher,其中{ lazy: true}computed watcher的标记,最终会调用defineComputed将数据设置为响应式数据,对应源码如下:

function initComputed() {  ···  for(var key in computed) {    watchers[key] = new Watcher(        vm,        getter || noop,        noop,        computedWatcherOptions      );  }  if (!(key in vm)) {    defineComputed(vm, key, userDef);  }}// computed watcher的标记,lazy属性为truevar computedWatcherOptions = { lazy: true };

defineComputed的逻辑和剖析data的逻辑类似,最终调用Object.defineProperty进行数据拦挡。具体的定义如下:

function defineComputed (target,key,userDef) {  // 非服务端渲染会对getter进行缓存  var shouldCache = !isServerRendering();  if (typeof userDef === 'function') {    //     sharedPropertyDefinition.get = shouldCache      ? createComputedGetter(key)      : createGetterInvoker(userDef);    sharedPropertyDefinition.set = noop;  } else {    sharedPropertyDefinition.get = userDef.get      ? shouldCache && userDef.cache !== false        ? createComputedGetter(key)        : createGetterInvoker(userDef.get)      : noop;    sharedPropertyDefinition.set = userDef.set || noop;  }  if (sharedPropertyDefinition.set === noop) {    sharedPropertyDefinition.set = function () {      warn(        ("Computed property \"" + key + "\" was assigned to but it has no setter."),        this      );    };  }  Object.defineProperty(target, key, sharedPropertyDefinition);}

在非服务端渲染的情景,计算属性的计算结果会被缓存,缓存的意义在于,只有在相干响应式数据发生变化时,computed才会从新求值,其余状况屡次拜访计算属性的值都会返回之前计算的后果,这就是缓存的优化computed属性有两种写法,一种是函数,另一种是对象,其中对象的写法须要提供gettersetter办法。

当拜访到computed属性时,会触发getter办法进行依赖收集,看看createComputedGetter的实现。

function createComputedGetter (key) {    return function computedGetter () {      var watcher = this._computedWatchers && this._computedWatchers[key];      if (watcher) {        if (watcher.dirty) {          watcher.evaluate();        }        if (Dep.target) {          watcher.depend();        }        return watcher.value      }    }  }

createComputedGetter返回的函数在执行过程中会先拿到属性的computed watcher,dirty是标记是否曾经执行过计算结果,如果执行过则不会执行watcher.evaluate反复计算,这也是缓存的原理。

Watcher.prototype.evaluate = function evaluate () {    // 对于计算属性而言 evaluate的作用是执行计算回调    this.value = this.get();    this.dirty = false;  };

get办法后面介绍过,会调用实例化watcher时传递的执行函数,在computer watcher的场景下,执行函数是计算属性的计算函数,他能够是一个函数,也能够是对象的getter办法。

列举一个场景防止和data的解决脱节,computed在计算阶段,如果拜访到data数据的属性值,会触发data数据的getter办法进行依赖收集,依据后面剖析,dataDep收集器会将以后watcher作为依赖进行收集,而这个watcher就是computed watcher,并且会为以后的watcher增加拜访的数据Dep

回到计算执行函数的this.get()办法,getter执行实现后同样会进行依赖的革除,原理和目标参考data阶段的剖析。get执行结束后会进入watcher.depend进行依赖的收集。收集过程和data统一,将以后的computed watcher作为依赖收集到数据的依赖收集器Dep中。

这就是computed依赖收集的残缺过程,比照data的依赖收集,computed会对运算的后果进行缓存,防止反复执行运算过程。

7.10.2 派发更新

派发更新的条件是data中数据产生扭转,所以大部分的逻辑和剖析data时统一,咱们做一个总结。

  • 当计算属性依赖的数据产生更新时,因为数据的Dep收集过computed watch这个依赖,所以会调用depnotify办法,对依赖进行状态更新。
  • 此时computed watcher和之前介绍的watcher不同,它不会立即执行依赖的更新操作,而是通过一个dirty进行标记。咱们再回头看依赖更新的代码。
Dep.prototype.notify = function() {  ···   for (var i = 0, l = subs.length; i < l; i++) {      subs[i].update();    }}Watcher.prototype.update = function update () {  // 计算属性分支    if (this.lazy) {    this.dirty = true;  } else if (this.sync) {    this.run();  } else {    queueWatcher(this);  }};

因为lazy属性的存在,update过程不会执行状态更新的操作,只会将dirty标记为true

  • 因为data数据领有渲染watcher这个依赖,所以同时会执行updateComponent进行视图从新渲染,而render过程中会拜访到计算属性,此时因为this.dirty值为true,又会对计算属性从新求值。

7.11 小结

咱们在上一节的实践根底上深入分析了Vue如何利用data,computed构建响应式零碎。响应式零碎的外围是利用Object.defineProperty对数据的getter,setter进行拦挡解决,解决的外围是在拜访数据时对数据所在场景的依赖进行收集,在数据产生更改时,告诉收集过的依赖进行更新。这一节咱们具体的介绍了data,computed对响应式的解决,两者解决逻辑存在很大的相似性但却各有的个性。源码中会computed的计算结果进行缓存,防止了在多个中央应用时频繁反复计算的问题。因为篇幅无限,对于用户自定义的watcher咱们会放到下一大节剖析。文章还留有一个纳闷,依赖收集时如果遇到的数据是数组时应该怎么解决,这些纳闷都会在之后的文章一一解开。