深刻了解 Vue.js 是如何进行「依赖收集]

前言

在上一章节咱们已经粗略的分析了整个的Vue 的源码(还在草稿箱,须要梳理清楚才放出来),可是还有不少东西没有深刻的去进行分析,我会经过以下几个重要点,进行进一步深刻分析。react

  1. 深刻了解 Vue 响应式原理(数据拦截)
  2. 深刻了解 Vue.js 是如何进行「依赖收集」,准确地追踪全部修改
  3. 深刻了解 Virtual DOM
  4. 深刻了解 Vue.js 的批量异步更新策略
  5. 深刻了解 Vue.js 内部运行机制,理解调用各个 API 背后的原理

这一章节咱们针对2. 深刻了解 Vue.js 是如何进行「依赖收集」,准确地追踪全部修改 来进行分析。express

初始化Vue

咱们简单实例化一个Vue的实例, 下面的咱们针对这个简单的实例进行深刻的去思考:数组

// app Vue instance
var app = new Vue({
  data: {
    newTodo: '', 
  },

  // watch todos change for localStorage persistence
  watch: {
    newTodo: {
      handler: function (newTodo) {
        console.log(newTodo);
      },
      sync: false, 
      before: function () {

      }
    }
  }  
})
// mount
app.$mount('.todoapp')
复制代码

initState

在上面咱们有添加一个watch的属性配置:bash

从上面的代码咱们可知,咱们配置了一个key为newTodo的配置项, 咱们从上面的代码能够理解为:服务器

newTodo的值发生变化了,咱们须要执行hander方法,因此咱们来分析下具体是怎么实现的。app

咱们仍是先从initState方法查看入手:异步

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }
复制代码

咱们来具体分析下initWatch方法:async

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);
      }
    }
  }
复制代码

从上面的代码分析,咱们能够发现watch 能够有多个hander,写法以下:函数

watch: {
    todos:
      [
        {
          handler: function (todos) {
            todoStorage.save(todos)
          },
          deep: true
        },
        {
          handler: function (todos) {
            console.log(todos)
          },
          deep: true
        }
      ]
  },
复制代码

咱们接下来分析createWatcher方法:工具

function createWatcher (
    vm,
    expOrFn,
    handler,
    options
  ) {
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }
复制代码

总结:

  1. 从这个方法可知,其实咱们的hanlder还能够是一个string
  2. 而且这个handervm对象上的一个方法,咱们以前已经分析methods里面的方法都最终挂载在vm 实例对象上,能够直接经过vm["method"]访问,因此咱们又发现watch的另一种写法, 直接给watchkey 直接赋值一个字符串名称, 这个名称能够是methods里面定一个的一个方法:
watch: {
    todos: 'newTodo'
  },
复制代码
methods: {
    handlerTodos: function (todos) {
      todoStorage.save(todos)
    }
  }
复制代码

接下来调用$watch方法

Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
        }
      }
      return function unwatchFn () {
        watcher.teardown();
      }
    };
复制代码

在这个方法,咱们看到有一个immediate的属性,中文意思就是当即, 若是咱们配置了这个属性为true, 就会当即执行watchhander,也就是同步 执行, 若是没有设置, 则会这个watcher异步执行,下面会具体分析怎么去异步执行的。 因此这个属性可能在某些业务场景应该用的着。

在这个方法中new 了一个Watcher对象, 这个对象是一个重头戏,咱们下面须要好好的分析下这个对象。 其代码以下(删除只保留了核心的代码):

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    vm._watchers.push(this);
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };
复制代码

主要作了以下几件事:

  1. watcher 对象保存在vm._watchers
  2. 获取getter,this.getter = parsePath(expOrFn);
  3. 执行this.get()去获取value

其中parsePath方法代码以下,返回的是一个函数:

var bailRE = /[^\w.$]/;
  function parsePath (path) {
    if (bailRE.test(path)) {
      return
    }
    var segments = path.split('.');
    return function (obj) {
      for (var i = 0; i < segments.length; i++) {
        if (!obj) { return }
        obj = obj[segments[i]];
      }
      return obj
    }
  }
复制代码

在调用this.get()方法中去调用value = this.getter.call(vm, vm);

而后会调用上面经过obj = obj[segments[i]];去取值,如vm.newTodo, 咱们从 深刻了解 Vue 响应式原理(数据拦截),已经知道,Vue 会将data里面的全部的数据进行拦截,以下:

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        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;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
复制代码

因此咱们在调用vm.newTodo时,会触发getter,因此咱们来深刻的分析下getter的方法

getter

getter 的代码以下:

get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      }
复制代码
  1. 首先取到值var value = getter ? getter.call(obj) : val;
  2. 调用Dep对象的depend方法, 将dep对象保存在target属性中Dep.target.addDep(this);target是一个Watcher对象 其代码以下:
Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
复制代码

生成的Dep对象以下图:

3. 判断是否有自属性,若是有自属性,递归调用。

如今咱们已经完成了依赖收集, 下面咱们来分析当数据改变是,怎么去准确地追踪全部修改

准确地追踪全部修改

咱们能够尝试去修改data里面的一个属性值,如newTodo, 首先会进入set方法,其代码以下:

set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
复制代码

下面我来分析这个方法。

  1. 首先判断新的value 和旧的value ,若是相等,则就直接return
  2. 调用dep.notify();去通知全部的subs, subs是一个类型是Watcher对象的数组 而subs里面的数据,是咱们上面分析的getter逻辑维护的watcher对象.

notify方法,就是去遍历整个subs数组里面的对象,而后去执行update()

Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; 复制代码

上面有一个判断config.async,是不是异步,若是是异步,须要排序,先进先出, 而后去遍历执行update()方法,下面咱们来看下update()方法。

Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };
复制代码

上面的方法,分红三种状况:

  1. 若是watch配置了lazy(懒惰的),不会当即执行(后面会分析会何时执行)
  2. 若是配置了sync(同步)为true则会当即执行hander方法
  3. 第三种状况就是会将其添加到watcher队列(queue)中

咱们会重点分析下第三种状况, 下面是queueWatcher源码

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);
      }
    }
  }
复制代码
  1. 首先flushing默认是false, 因此将watcher保存在queue的数组中。
  2. 而后waiting默认是false, 因此会走if(waiting)分支
  3. configVue的全局配置, 其async(异步)值默认是true, 因此会执行nextTick函数。

下面咱们来分析下nextTick函数

nextTick

nextTick 代码以下:

function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      if (useMacroTask) {
        macroTimerFunc();
      } else {
        microTimerFunc();
      }
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }
复制代码

nextTick 主要作以下事情:

  1. 将传递的参数cb 的执行放在一个匿名函数中,而后保存在一个callbacks 的数组中
  2. pendinguseMacroTask的默认值都是false, 因此会执行microTimerFunc()(微Task) microTimerFunc()的定义以下:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)   
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}
复制代码

其实就是用Promise函数(只分析Promise兼容的状况), 而Promise 是一个i额微Task 必须等全部的宏Task 执行完成后才会执行, 也就是主线程空闲的时候才会去执行微Task;

如今咱们查看下flushCallbacks函数:

function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }
复制代码

这个方法很简单,

  1. 第一个是变动pending的状态为false
  2. 遍历执行callbacks数组里面的函数,咱们还记得在nextTick 函数中,将cb 保存在callbacks 中。

咱们下面来看下cb 的定义,咱们调用nextTick(flushSchedulerQueue);, 因此cb 指的就是flushSchedulerQueue 函数, 其代码以下:

function flushSchedulerQueue () {
    flushing = true;
    var watcher, id; 
    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();
      // 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();

    // call component updated and activated hooks
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

    // devtool hook
    /* istanbul ignore if */
    if (devtools && config.devtools) {
      devtools.emit('flush');
    }
  }
复制代码
  1. 首先将flushing 状态开关变成true
  2. queue 进行按照ID 升序排序,queue是在queueWatcher 方法中,将对应的Watcher 保存在其中的。
  3. 遍历queue去执行对应的watcherrun 方法。
  4. 执行resetSchedulerState()是去重置状态值,如waiting = flushing = false
  5. 执行callActivatedHooks(activatedQueue);更新组件 ToDO:
  6. 执行callUpdatedHooks(updatedQueue);调用生命周期函数updated
  7. 执行devtools.emit('flush');刷新调试工具。

咱们在3. 遍历queue去执行对应的watcher的run 方法。, 发现queue中有两个watcher, 可是咱们在咱们的app.js中初始化Vue的 时候watch的代码以下:

watch: { 
    newTodo: {
      handler: function (newTodo) {
        console.log(newTodo);
      },
      sync: false
    }
  }
复制代码

从上面的代码上,咱们只Watch了一个newTodo属性,按照上面的分析,咱们应该只生成了一个watcher, 可是咱们却生成了两个watcher了, 另一个watcher究竟是怎么来的呢?

总结:

  1. 在咱们配置的watch属性中,生成的Watcher对象,只负责调用hanlder方法。不会负责UI的渲染
  2. 另一个watch其实算是Vue内置的一个Watch(我的理解),而是在咱们调用Vue$mount方法时生成的, 如咱们在咱们的app.js中直接调用了这个方法:app.$mount('.todoapp'). 另一种方法不直接调用这个方法,而是在初始化Vue的配置中,添加了一个el: '.todoapp'属性就能够。这个Watcher 负责了UI的最终渲染,很重要,咱们后面会深刻分析这个Watcher
  3. $mount方法是最后执行的一个方法,因此他生成的Watcher对象的Id 是最大的,因此咱们在遍历queue以前,咱们会进行一个升序 排序, 限制性全部的Watch配置中生成的Watcher 对象,最后才执行$mount中生成的Watcher对象,去进行UI渲染。

$mount

咱们如今来分析$mount方法中是怎么生成Watcher对象的,以及他的cb 是什么。其代码以下:

new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
复制代码
  1. 从上面的代码,咱们能够看到最后一个参数isRenderWatcher设置的值是true , 表示是一个Render Watcher, 在watch 中配置的,生成的Watcher 这个值都是false, 咱们在Watcher 的构造函数中能够看到:
if (isRenderWatcher) {
      vm._watcher = this;
    }
复制代码

若是isRenderWatchertrue 直接将这个特殊的Watcher 挂载在Vue 实例的_watcher属性上, 因此咱们在flushSchedulerQueue 方法中调用callUpdatedHooks 函数中,只有这个watcher才会执行生命周期函数updated

function callUpdatedHooks (queue) {
    var i = queue.length;
    while (i--) {
      var watcher = queue[i];
      var vm = watcher.vm;
      if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'updated');
      }
    }
  }
复制代码
  1. 第二个参数expOrFn , 也就是Watchergetter, 会在实例化Watcher 的时候调用get方法,而后执行value = this.getter.call(vm, vm);, 在这里就是会执行updateComponent方法,这个方法是UI 渲染的一个关键方法,咱们在这里暂时不深刻分析。
  2. 第三个参数是cb, 传入的是一个空的方法
  3. 第四个参数传递的是一个options对象,在这里传入一个before的function, 也就是,在UI从新渲染前会执行的一个生命中期函数beforeUpdate

上面咱们已经分析了watch的一个工做过程,下面咱们来分析下computed的工做过程,看其与watch 有什么不同的地方。

computed

首先在实例化Vue 对象时,也是在initState 方法中,对computed 进行了处理,执行了initComputed方法, 其代码以下:

function initComputed (vm, computed) {
    // $flow-disable-line
    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    var isSSR = isServerRendering();

    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }

      if (!isSSR) {
        // create internal watcher for the computed property.
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if (!(key in vm)) {
        defineComputed(vm, key, userDef);
      } else {
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
  }
复制代码

上面代码比较长,可是咱们能够总结以下几点:

  1. var watchers = vm._computedWatchers = Object.create(null);vm实例对象上面挂载了一个_computedWatchers的属性,保存了由computed 生成的全部的watcher
  2. 而后遍历全部的key, 每个key 都生成一个watcher
  3. var getter = typeof userDef === 'function' ? userDef : userDef.get; 从这个代码能够延伸computed 的两种写法,以下:
computed: {
    // 写法1:直接是一个function
    // strLen: function () {
    //   console.log(this.newTodo.length)
    //   return this.newTodo.length
    // },
    // 写法2: 能够是一个对象,可是必需要有get 方法,
    // 不过写成对象没有什么意义, 由于其余的属性,都不会使用。
    strLen: {
      get: function () {
        console.log(this.newTodo.length)
        return this.newTodo.length
      }
    }
  }
复制代码
  1. 若是不是服务端渲染,就生成一个watcher 对象,而且保存在vm._computedWatchers属性中,可是这个与watch 生成的watcher 有一个重要的区别就是, 传递了一个属性computedWatcherOptions对象,这个对象就配置了一个lazy: ture

咱们在Watcher的构造函数中,有以下逻辑:

this.value = this.lazy
      ? undefined
      : this.get();
复制代码

由于this.lazytrue 因此不会执行this.get();, 也就不会当即执行computed 里面配置的对应的方法。

  1. defineComputed(vm, key, userDef);就是将computed 的属性,直接挂载在vm 上,能够直接经过vm.strLen去访问,不过在这个方法中,有针对是否是服务器渲染作了区别,服务器渲染会当即执行computed 的函数,获取值,可是在Web 则不会当即执行,而是给get 赋值一个函数:
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
      }
    }
  }
复制代码

若是咱们在咱们的template中引用了computed的属性,如:<div>{{strLen}}</div>, 会执行$mount去渲染模版的时候,会去调用strLen,而后就会执行上面的computedGetter的方法去获取值, 执行的就是:

Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };
复制代码

执行了this.get() 就是上面分析watch 中的this.get().

思考:

咱们上面基本已经分析了computed逻辑的基本过程,可是咱们好像仍是没有关联上, 当咱们的data里面的值变了,怎么去通知computed 更新的呢?咱们的computed以下:

computed: {
    strLen: function () {
      return this.newTodo.length
    }, 
  }
复制代码

当咱们改变this.newTodo 的时候,会执行strLen的方法呢?

答案:

  1. 在上面咱们已经分析了咱们在咱们的template 中有引用strLen,如<div>{{strLen}}</div>,在执行$mount去渲染模版的时候,会去调用strLen,而后就会执行的computedGetter的方法去获取值,而后调用get 方法,也就是咱们computed 配置的函数:
computed: {
    strLen: function () {
      return this.newTodo.length
    }
  },
复制代码
  1. 在执行上面方法的时候,会引用this.newTodo , 就会进入reactiveGetter方法(深刻了解 Vue 响应式原理(数据拦截))
get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      }
复制代码

会将当前的Watcher 对象添加到dep.subs队列中。

  1. this.newTodo值改变时,就会执行reactiveSetter方法,当执行dep.notify();时,也就会执行computed 里面的方法,从而达到当data里面的值改变时,其有引用这个data 属性的computed 也就会当即执行。
  2. 若是咱们定义了computed 可是没有任何地方去引用这个computed , 即便对应的data 属性变动了,也不会执行computed 方法的, 即便手动执行computed 方法, 如:app.strLen也不会生效,由于在WatcheraddDep 方法,已经判断当前的watcher 不是一个新加入的watcher
Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
复制代码
相关文章
相关标签/搜索