关于vue.js:vue源码分析响应式系统二

6次阅读

共计 12919 个字符,预计需要花费 33 分钟才能阅读完成。

为了深刻介绍响应式零碎的外部实现原理,咱们花了一整节的篇幅介绍了数据 (包含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)
}
// observe
function 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如果遇到属性值为对象时,会为该对象的每个值收集依赖

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

  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 属性为 true
var 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 咱们会放到下一大节剖析。文章还留有一个纳闷,依赖收集时如果遇到的数据是数组时应该怎么解决,这些纳闷都会在之后的文章一一解开。

正文完
 0