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

vue

前言

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

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

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

初始化Vue

我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:

 

  1. // app Vue instance

  2. var app = new Vue({

  3. data: {

  4. newTodo: '',

  5. },

  6.  
  7. // watch todos change for localStorage persistence

  8. watch: {

  9. newTodo: {

  10. handler: function (newTodo) {

  11. console.log(newTodo);

  12. },

  13. sync: false,

  14. before: function () {

  15.  
  16. }

  17. }

  18. }

  19. })

  20. // mount

  21. app.$mount('.todoapp')

  22. 复制代码

initState

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

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

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

我们还是先从initState方法查看入手:

 

  1. function initState (vm) {

  2. vm._watchers = [];

  3. var opts = vm.$options;

  4. if (opts.props) { initProps(vm, opts.props); }

  5. if (opts.methods) { initMethods(vm, opts.methods); }

  6. if (opts.data) {

  7. initData(vm);

  8. } else {

  9. observe(vm._data = {}, true /* asRootData */);

  10. }

  11. if (opts.computed) { initComputed(vm, opts.computed); }

  12. if (opts.watch && opts.watch !== nativeWatch) {

  13. initWatch(vm, opts.watch);

  14. }

  15. }

  16. 复制代码

我们来具体分析下initWatch方法:

 

  1. function initWatch (vm, watch) {

  2. for (var key in watch) {

  3. var handler = watch[key];

  4. if (Array.isArray(handler)) {

  5. for (var i = 0; i < handler.length; i++) {

  6. createWatcher(vm, key, handler[i]);

  7. }

  8. } else {

  9. createWatcher(vm, key, handler);

  10. }

  11. }

  12. }

  13. 复制代码

从上面的代码分析,我们可以发现watch 可以有多个hander,写法如下:

 

  1. watch: {

  2. todos:

  3. [

  4. {

  5. handler: function (todos) {

  6. todoStorage.save(todos)

  7. },

  8. deep: true

  9. },

  10. {

  11. handler: function (todos) {

  12. console.log(todos)

  13. },

  14. deep: true

  15. }

  16. ]

  17. },

  18. 复制代码

我们接下来分析createWatcher方法:

 

  1. function createWatcher (

  2. vm,

  3. expOrFn,

  4. handler,

  5. options

  6. ) {

  7. if (isPlainObject(handler)) {

  8. options = handler;

  9. handler = handler.handler;

  10. }

  11. if (typeof handler === 'string') {

  12. handler = vm[handler];

  13. }

  14. return vm.$watch(expOrFn, handler, options)

  15. }

  16. 复制代码

总结:

  1. 从这个方法可知,其实我们的hanlder还可以是一个string

  2. 并且这个handervm对象上的一个方法,我们之前已经分析methods里面的方法都最终挂载在vm 实例对象上,可以直接通过vm["method"]访问,所以我们又发现watch的另外一种写法, 直接给watchkey 直接赋值一个字符串名称, 这个名称可以是methods里面定一个的一个方法:

 

  1. watch: {

  2. todos: 'newTodo'

  3. },

  4. 复制代码

 

  1. methods: {

  2. handlerTodos: function (todos) {

  3. todoStorage.save(todos)

  4. }

  5. }

  6. 复制代码

接下来调用$watch方法

 

  1. Vue.prototype.$watch = function (

  2. expOrFn,

  3. cb,

  4. options

  5. ) {

  6. var vm = this;

  7. if (isPlainObject(cb)) {

  8. return createWatcher(vm, expOrFn, cb, options)

  9. }

  10. options = options || {};

  11. options.user = true;

  12. var watcher = new Watcher(vm, expOrFn, cb, options);

  13. if (options.immediate) {

  14. try {

  15. cb.call(vm, watcher.value);

  16. } catch (error) {

  17. handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));

  18. }

  19. }

  20. return function unwatchFn () {

  21. watcher.teardown();

  22. }

  23. };

  24. 复制代码

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

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

 

  1. var Watcher = function Watcher (

  2. vm,

  3. expOrFn,

  4. cb,

  5. options,

  6. isRenderWatcher

  7. ) {

  8. this.vm = vm;

  9. vm._watchers.push(this);

  10. // parse expression for getter

  11. if (typeof expOrFn === 'function') {

  12. this.getter = expOrFn;

  13. } else {

  14. this.getter = parsePath(expOrFn);

  15. if (!this.getter) {

  16. this.getter = noop;

  17. }

  18. }

  19. this.value = this.lazy

  20. ? undefined

  21. : this.get();

  22. };

  23. 复制代码

主要做了如下几件事:

  1. watcher 对象保存在vm._watchers
  2. 获取getter,this.getter = parsePath(expOrFn);

  3. 执行this.get()去获取value

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

 

  1. var bailRE = /[^\w.$]/;

  2. function parsePath (path) {

  3. if (bailRE.test(path)) {

  4. return

  5. }

  6. var segments = path.split('.');

  7. return function (obj) {

  8. for (var i = 0; i < segments.length; i++) {

  9. if (!obj) { return }

  10. obj = obj[segments[i]];

  11. }

  12. return obj

  13. }

  14. }

  15. 复制代码

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

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

 

  1. Object.defineProperty(obj, key, {

  2. enumerable: true,

  3. configurable: true,

  4. get: function reactiveGetter () {

  5. var value = getter ? getter.call(obj) : val;

  6. if (Dep.target) {

  7. dep.depend();

  8. if (childOb) {

  9. childOb.dep.depend();

  10. if (Array.isArray(value)) {

  11. dependArray(value);

  12. }

  13. }

  14. }

  15. return value

  16. },

  17. set: function reactiveSetter (newVal) {

  18. var value = getter ? getter.call(obj) : val;

  19. /* eslint-disable no-self-compare */

  20. if (newVal === value || (newVal !== newVal && value !== value)) {

  21. return

  22. }

  23. /* eslint-enable no-self-compare */

  24. if (customSetter) {

  25. customSetter();

  26. }

  27. // #7981: for accessor properties without setter

  28. if (getter && !setter) { return }

  29. if (setter) {

  30. setter.call(obj, newVal);

  31. } else {

  32. val = newVal;

  33. }

  34. childOb = !shallow && observe(newVal);

  35. dep.notify();

  36. }

  37. });

  38. 复制代码

所以我们在调用vm.newTodo时,会触发getter,所以我们来深入的分析下getter的方法

getter

getter 的代码如下:

 

  1. get: function reactiveGetter () {

  2. var value = getter ? getter.call(obj) : val;

  3. if (Dep.target) {

  4. dep.depend();

  5. if (childOb) {

  6. childOb.dep.depend();

  7. if (Array.isArray(value)) {

  8. dependArray(value);

  9. }

  10. }

  11. }

  12. return value

  13. }

  14. 复制代码

  1. 首先取到值var value = getter ? getter.call(obj) : val;

  2. 调用Dep对象的depend方法, 将dep对象保存在target属性中Dep.target.addDep(this);target是一个Watcher对象 其代码如下:

 

  1. Watcher.prototype.addDep = function addDep (dep) {

  2. var id = dep.id;

  3. if (!this.newDepIds.has(id)) {

  4. this.newDepIds.add(id);

  5. this.newDeps.push(dep);

  6. if (!this.depIds.has(id)) {

  7. dep.addSub(this);

  8. }

  9. }

  10. };

  11. 复制代码

生成的Dep对象如下图:

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

现在我们已经完成了依赖收集, 下面我们来分析当数据改变是,怎么去准确地追踪所有修改。

准确地追踪所有修改

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

 

  1. set: function reactiveSetter (newVal) {

  2. var value = getter ? getter.call(obj) : val;

  3. /* eslint-disable no-self-compare */

  4. if (newVal === value || (newVal !== newVal && value !== value)) {

  5. return

  6. }

  7. /* eslint-enable no-self-compare */

  8. if (customSetter) {

  9. customSetter();

  10. }

  11. // #7981: for accessor properties without setter

  12. if (getter && !setter) { return }

  13. if (setter) {

  14. setter.call(obj, newVal);

  15. } else {

  16. val = newVal;

  17. }

  18. childOb = !shallow && observe(newVal);

  19. dep.notify();

  20. }

  21. 复制代码

下面我来分析这个方法。

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

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

 

  1. Dep.prototype.notify = function notify () {

  2. // stabilize the subscriber list first

  3. var subs = this.subs.slice();

  4. if (!config.async) {

  5. // subs aren't sorted in scheduler if not running async

  6. // we need to sort them now to make sure they fire in correct

  7. // order

  8. subs.sort(function (a, b) { return a.id - b.id; });

  9. }

  10. for (var i = 0, l = subs.length; i < l; i++) {

  11. subs[i].update();

  12. }

  13. };

  14. 复制代码

上面有一个判断config.async,是否是异步,如果是异步,需要排序,先进先出, 然后去遍历执行update()方法,下面我们来看下update()方法。

 

  1. Watcher.prototype.update = function update () {

  2. /* istanbul ignore else */

  3. if (this.lazy) {

  4. this.dirty = true;

  5. } else if (this.sync) {

  6. this.run();

  7. } else {

  8. queueWatcher(this);

  9. }

  10. };

  11. 复制代码

上面的方法,分成三种情况:

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

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

 

  1. function queueWatcher (watcher) {

  2. var id = watcher.id;

  3. if (has[id] == null) {

  4. has[id] = true;

  5. if (!flushing) {

  6. queue.push(watcher);

  7. } else {

  8. // if already flushing, splice the watcher based on its id

  9. // if already past its id, it will be run next immediately.

  10. var i = queue.length - 1;

  11. while (i > index && queue[i].id > watcher.id) {

  12. i--;

  13. }

  14. queue.splice(i + 1, 0, watcher);

  15. }

  16. // queue the flush

  17. if (!waiting) {

  18. waiting = true;

  19.  
  20. if (!config.async) {

  21. flushSchedulerQueue();

  22. return

  23. }

  24. nextTick(flushSchedulerQueue);

  25. }

  26. }

  27. }

  28. 复制代码

  1. 首先flushing默认是false, 所以将watcher保存在queue的数组中。
  2. 然后waiting默认是false, 所以会走if(waiting)分支
  3. configVue的全局配置, 其async(异步)值默认是true, 所以会执行nextTick函数。

下面我们来分析下nextTick函数

nextTick

nextTick 代码如下:

 

  1. function nextTick (cb, ctx) {

  2. var _resolve;

  3. callbacks.push(function () {

  4. if (cb) {

  5. try {

  6. cb.call(ctx);

  7. } catch (e) {

  8. handleError(e, ctx, 'nextTick');

  9. }

  10. } else if (_resolve) {

  11. _resolve(ctx);

  12. }

  13. });

  14. if (!pending) {

  15. pending = true;

  16. if (useMacroTask) {

  17. macroTimerFunc();

  18. } else {

  19. microTimerFunc();

  20. }

  21. }

  22. // $flow-disable-line

  23. if (!cb && typeof Promise !== 'undefined') {

  24. return new Promise(function (resolve) {

  25. _resolve = resolve;

  26. })

  27. }

  28. }

  29. 复制代码

nextTick 主要做如下事情:

  1. 将传递的参数cb 的执行放在一个匿名函数中,然后保存在一个callbacks 的数组中
  2. pendinguseMacroTask的默认值都是false, 所以会执行microTimerFunc()(微Task) microTimerFunc()的定义如下:

 

  1. if (typeof Promise !== 'undefined' && isNative(Promise)) {

  2. const p = Promise.resolve()

  3. microTimerFunc = () => {

  4. p.then(flushCallbacks)

  5. if (isIOS) setTimeout(noop)

  6. }

  7. } else {

  8. // fallback to macro

  9. microTimerFunc = macroTimerFunc

  10. }

  11. 复制代码

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

现在我们查看下flushCallbacks函数:

 

  1. function flushCallbacks () {

  2. pending = false;

  3. var copies = callbacks.slice(0);

  4. callbacks.length = 0;

  5. for (var i = 0; i < copies.length; i++) {

  6. copies[i]();

  7. }

  8. }

  9. 复制代码

这个方法很简单,

  1. 第一个是变更pending的状态为false

  2. 遍历执行callbacks数组里面的函数,我们还记得在nextTick 函数中,将cb 保存在callbacks 中。

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

 

  1. function flushSchedulerQueue () {

  2. flushing = true;

  3. var watcher, id;

  4. queue.sort(function (a, b) { return a.id - b.id; });

  5.  
  6. for (index = 0; index < queue.length; index++) {

  7. watcher = queue[index];

  8. if (watcher.before) {

  9. watcher.before();

  10. }

  11. id = watcher.id;

  12. has[id] = null;

  13. watcher.run();

  14. // in dev build, check and stop circular updates.

  15. if (has[id] != null) {

  16. circular[id] = (circular[id] || 0) + 1;

  17. if (circular[id] > MAX_UPDATE_COUNT) {

  18. warn(

  19. 'You may have an infinite update loop ' + (

  20. watcher.user

  21. ? ("in watcher with expression \"" + (watcher.expression) + "\"")

  22. : "in a component render function."

  23. ),

  24. watcher.vm

  25. );

  26. break

  27. }

  28. }

  29. }

  30.  
  31. // keep copies of post queues before resetting state

  32. var activatedQueue = activatedChildren.slice();

  33. var updatedQueue = queue.slice();

  34.  
  35. resetSchedulerState();

  36.  
  37. // call component updated and activated hooks

  38. callActivatedHooks(activatedQueue);

  39. callUpdatedHooks(updatedQueue);

  40.  
  41. // devtool hook

  42. /* istanbul ignore if */

  43. if (devtools && config.devtools) {

  44. devtools.emit('flush');

  45. }

  46. }

  47. 复制代码

  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的代码如下:

 

  1. watch: {

  2. newTodo: {

  3. handler: function (newTodo) {

  4. console.log(newTodo);

  5. },

  6. sync: false

  7. }

  8. }

  9. 复制代码

从上面的代码上,我们只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 是什么。其代码如下:

 

  1. new Watcher(vm, updateComponent, noop, {

  2. before: function before () {

  3. if (vm._isMounted) {

  4. callHook(vm, 'beforeUpdate');

  5. }

  6. }

  7. }, true /* isRenderWatcher */);

  8. 复制代码

  1. 从上面的代码,我们可以看到最后一个参数isRenderWatcher设置的值是true , 表示是一个Render Watcher, 在watch 中配置的,生成的Watcher 这个值都是false, 我们在Watcher 的构造函数中可以看到:

 

  1. if (isRenderWatcher) {

  2. vm._watcher = this;

  3. }

  4. 复制代码

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

 

  1. function callUpdatedHooks (queue) {

  2. var i = queue.length;

  3. while (i--) {

  4. var watcher = queue[i];

  5. var vm = watcher.vm;

  6. if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {

  7. callHook(vm, 'updated');

  8. }

  9. }

  10. }

  11. 复制代码

  1. 第二个参数expOrFn , 也就是Watchergetter, 会在实例化Watcher 的时候调用get方法,然后执行value = this.getter.call(vm, vm);, 在这里就是会执行updateComponent方法,这个方法是UI 渲染的一个关键方法,我们在这里暂时不深入分析。
  2. 第三个参数是cb, 传入的是一个空的方法
  3. 第四个参数传递的是一个options对象,在这里传入一个before的function, 也就是,在UI重新渲染前会执行的一个生命中期函数beforeUpdate

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



转载:https://juejin.im/post/5c75f336e51d45708f2a26da

以上是 深入了解 Vue.js 是如何进行「依赖收集] 的全部内容, 来源链接: utcz.com/z/377785.html

回到顶部