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

栏目: JavaScript · 发布时间: 5年前

内容简介:在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。这一章节我们针对2. 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改来进行分析。我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:

在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。

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

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

初始化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 的属性配置:

从上面的代码我们可知,我们配置了一个key为 newTodo 的配置项, 我们从上面的代码可以理解为:

newTodo 的值发生变化了,我们需要执行 hander 方法,所以我们来分析下具体是怎么实现的。

我们还是先从 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 方法:

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 对象如下图:

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

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

TODO:继续分析 computed 的工作方式。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

代码阅读方法与实践

代码阅读方法与实践

斯平内利斯 / 赵学良 / 清华大学出版社 / 2004-03-01 / 45.00元

代码阅读有自身的一套技能,重要的是能够确定什么时候使用哪项技术。本书中,作者使用600多个现实的例子,向读者展示如何区分好的(和坏的)代码,如何阅读,应该注意什么,以及如何使用这些知识改进自己的代码。养成阅读高品质代码的习惯,可以提高编写代码的能力。 阅读代码是程序员的基本技能,同时也是软件开发、维护、演进、审查和重用过程中不可或缺的组成部分。本书首次将阅读代码作为一项独立课题......一起来看看 《代码阅读方法与实践》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具