Vue2源码-响应式原理

423 阅读3分钟

Vue

响应式原理概述

大致流程,首先实例化Vue,调用_init方法,初始化data、methods、watch、props、生命周期等,通过调用观察者函数observer生成被代理的对象,对象上会有个__ob__属性就是observer的实例,也就是表示该对象被代理了,被代理对象中每个属性通过defineReactive?1方法来代理(如果属性值是对象或数组就递归向下执行),对象代理功能是使用Object.defineProperty这个原生api来实现的。Object.defineProperty分别定义getset方法,get方法当对象属性被引用时触发依赖收集,set方法用来修改属性并执行更新方法。在$mount方法中,会解析模板,引用对象属性触发依赖收集生成vNode,再渲染真实视图。后面在属性更新时,会触发set方法,进行更新。

从源码角度分析

初始化实例

_init
  function Vue (options) {
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }

_init初始化Vue实例,_init定义在原型链上,查看Vue.prototype._init中的内容,其中initLifecycle,确定组件的层级,父组件,根组件,定义属性来保存子组件、wather对象、记录生命周期执行状态属性等;initEvents做一些承父级相关监听函数的操作;initRender,初始化一些跟虚拟DOM相关属性和方法。

注入

initInjections属性注入,配合文档,这里和props属性很相似,inject格式应该是字符串数组或者对象,具体在resolveInject函数中,如果是数组,每一项就对应provide中的属性;如果是对象,再对对象的属性值类型做判断,如果是对象,则需要包含一个default属性返回默认属性的值,反之,则该值就对应provide中的属性。同时注入的属性会不断向上查询,直到找到包含该属性的provide或者到根组件没找到为止。最后生成的对象会注入到当前Vue实例中,注入过程中会使用修改toggleObserving修改shouldObserve状态为false,注入的对象不会递归向下做代理。 initState初始化data、props、methods、computed、watch等属性。这里也是Vue实现响应式原理地方,先放一下,先介绍注入。 initProvide为前面的initInjections注入方法提供provide。

这里查看这几个函数的执行顺序

initInjections => initState => initProvide

首先initState放在initInjections后面,这个就是防止data中的属性被覆盖,而initProvide放在最后执行,因为它是提供数据的,最外层不需要注入操作。而后面的子孙组件中的inject通过向上查找祖先组件的方式,可以访问到最外层Provide

响应式实现

再回头看initState方法,这其中最重要的就是对data做对象代理。 执行observe方法,observer作为构造函数,data作为参数,生成一个observer对象,value属性来保存data,被代理对象使用__ob__属性来保存这个observer实例,dep属性生成对该对象的订阅者,然后调用原型上的方法walk,依次使用defineReactive?1data对象中的每个属性做代理操作。

defineReactive?1

defineReactive?1首先生成一个Dep订阅者,然后判断属性是否可配置,再判断属性值如果是对象或数组会进一步向下构造观察者对象,接下来就是Object.defineProperty方法,这也是让Vue能实现响应式原理的API,Object.defineProperty原意是用来定义属性修饰符,包含属性的可修改性、可配置性、可枚举性,同时也可以定义getset方法,分别在属性读取和修改时执行,这样利用这两个方法进行一些扩展,get方法在执行时在获取属性值的基础上再进行依赖收集,set方法则通知依赖该属性对应的dep的所有watcher观察者来更新视图。

简单介绍完defineReactive?1,再仔细看下这个函数中两个关键的东西:DepWatcher

Dep

订阅者构造函数,每个属性会新建一个dep订阅者,dep中有个subs属性,其中Dep.target一个全局的变量会指向当前watcher观察者,然后当某个视图中对这个属性被引用,就会向该属性的dep订阅者中的subs添加当前的watcher观察者对象。当属性更新时,会调用对应dep中的notify方法,通知subs中所有的watcher执行update更新视图。

Watcher

观察者构造函数,上面已介绍部分,每个Vue实例中会存在_watcher属性对应一个watcher观察者,下面的依赖收集过程会详细介绍Watcher。 这时候应该来一张图描述:对象代理,Dep,Watcher,依赖收集,虚拟DOM,渲染视图等。

resource

依赖收集

接下来介绍依赖收集是什么时候发生的,回顾前面在注入、初始化数据等结束后,先执行created回调函数,然后执行$mount进行组件挂载,这里有意思的是作者定义了两次Vue.prototype.$mount,开始我也觉得奇怪,当然后面定义的会覆盖前面,只是用了一行代码var mount = Vue.prototype.$mount;在覆盖前把第一次的方法保存下来了,这时再看新的$mount方法,主要是在解析编译模板等操作,最后生成一个render的函数,末尾再执行前面的保存的老的$mount方法,里面使用了mountComponent,可以看到开始前会做一些检验判断render是否存在,没有的话就赋值成一个默认方法(创建一个仅包含文本的虚拟DOM),并抛出警告。然后执行beforeMount生命周期函数,然后会先定义一个updateComponent方法,该方法的作用就是根据对象属性的引用触发依赖收集并生成vNode。

在看后面先new一个Watcher对象,updateComponent方法会作为第二个参数传递进去。接下来查看Watcher是如何定义的。

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    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
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.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对象时,会调用原型上的get方法,该方法首先调用pushTarget会将Dep.target变成当前的wathcer,之后调用对象中的getter方法,此时的getter就是对应前面updateComponent方法,updateComponent真正被执行,从而会调用Vue实例上的_render方法。

Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var _parentvNode = ref._parentvNode;
    // ......省略分割......
    var vNode;
    // ......省略分割......
    vNode.parent = _parentvNode;
    return vNode

经历一系列地操作,完成了依赖收集和返回对应vNode,即虚拟DOM,vNode有个重要的属性parent父节点,如果为空就表示该节点是根节点,有了虚拟DOM以后,再经历_update__patch__等主要是diff算法部分,除了第一次vNode还不存在的时候,更新时生成新的vNode后,不会整个拿来更新而是经过diff处理生成最小更新的单位提高性能,updateComponent执行结束前会先调用popTarget方法,让Dep.target回到上一个的值。

    Dep.target = null;
    var targetStack = [];

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

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

为什么要用一个数组targetStack来保存watcher,这个跟父子组件挂载更新规则有关,父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted。这是挂载阶段父子组件生命周期执行顺序。因为子组件依赖收集发生在beforeMount之后mounted之前,因此在父组件如果存在子组件时,子组件构造一个新的watcher对象,此时父组件依赖收集还未结束,pushTarget操作会将父组件的watcherpush进targetStack暂缓起来,子组件生成vNode后,popTargetDep.target又恢复成父组件的watcher

  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;
  };

至于cleanupDeps方法,用来更新属性所对应dep订阅者中的watcher对象,该视图中不需要依赖该属性,则从对应的dep中移除watcherwatcher中的newDepIds每次都保存最新的dep的id集合。举个栗子,比如v-if控制的节点组件,showChild值由true变成false时,由于子组件已经不需要渲染了,cleanupDeps执行以后就会取消对childVal的订阅。

<Parent>
    <Child v-if="showChild">{{childVal}}</Child>
</parent>
视图更新

如上,updateComponent执行完成后,视图就完成渲染了。当某个对象代理的数据更新时,set方法执行,dep触发notify方法通知所有watcher观察者执行update,生成新的vNode,重新进行依赖收集等,patch对新老vNode进行diff,更新差异部分的视图。

Vue3都出来了,为什么还在写Vue2的总结?害,我觉得现在不整理出来以后更没机会了🐶 。嘿嘿,有错误的话也望掘友们指出。