乐趣区

关于javascript:Vuejs-nextTick-源码分析

nextTick

vue 版本

2.6.11

源码剖析(nextTick)

nextTick 源码调用过程总结:

init->timerFunc = (Promise/MutationObserver/setImmediate)
初始化阶段为 timerFunc 的执行形式赋值,一般来说在 Windows 浏览器环境下运行 timerFunc 函数的执行形式都会是 Promise.then 的形式,应用微工作队列的形式。

if (typeof Promise !== 'undefined' && isNative(Promise)) {var p = Promise.resolve();
  timerFunc = function () {p.then(flushCallbacks);
    if (isIOS) {setTimeout(noop); }
  };
  isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {characterData: true});
  timerFunc = function () {counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = function () {setImmediate(flushCallbacks);
  };
} else {timerFunc = function () {setTimeout(flushCallbacks, 0);
  };
}

$nextTick(fn)->callbacks.push(function(){fn.call(this)})->timerFunc()
应用 nextTick 的源码如下:

function nextTick (cb, ctx) {console.log('vue nexttick')
  var _resolve;
  callbacks.push(function () { // 全局变量 callbacks
    if (cb) {
      try {cb.call(ctx); // 这里调用回调
      } catch (e) {handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {_resolve(ctx);
    }
  });
  if (!pending) {
    pending = true; // 只执行一次 timerFunc 函数
    timerFunc();}
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {return new Promise(function (resolve) {_resolve = resolve;})
  }
}

如上所示,在一次宏工作中执行屡次 nextTick 只会调用一次 timerFunc(),timerFunc()会将 flushCallbacks 函数放入 JavaScript 的微工作队列中,待顺序调用。

......
var p = Promise.resolve();
  timerFunc = function () {p.then(flushCallbacks);
    if (isIOS) {setTimeout(noop); }
  };
  ......
function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {copies[i]();}
}

其次,$nextTick()是即时调用的, 并且会将传入的函数的 this 值变成以后 Vue 实例

Vue.prototype.$nextTick = function (fn) {return nextTick(fn, this)
  };

源码剖析(set 过程)

Vue 对每个组件中的 data 都做了数据代理(截持), 对 data 对象中的数据进行赋值操作,理论就会调用 defineProperty 中的 reactiveSetter 函数,进行一系列操作,包含告诉 Watcher 数据扭转了等等。

其中 setter 源代码如下,不止进行赋值操作,还会调用 dep.notify()告诉数据扭转了:

set: function reactiveSetter (newVal) {var value = getter ? getter.call(obj) : val;
      if (newVal === value || (newVal !== newVal && value !== value)) {return}
      if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter();
      }
      if (getter && !setter) {return}
      if (setter) {setter.call(obj, newVal); // 这里进行赋值操作
      } else {val = newVal;}
      childOb = !shallow && observe(newVal);
      dep.notify(); // 这里进行告诉}

Dep 对象次要作用是记录以后组件依赖的 Watcher(? 不分明,之后再来看)

总而言之,调用了 Dep 原型上的 notify 函数, 再接着调用 Watcher 原型上的 update 办法

Dep.prototype.notify = function notify () {var subs = this.subs.slice();
  if (process.env.NODE_ENV !== 'production' && !config.async) {subs.sort(function (a, b) {return a.id - b.id;});
  }
  for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}
};

update 办法, 这里调用了要害的 queueWatcher 函数

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

queueWatcher 函数做了两件要害的事
1、向 queue 变量中 push watcher
2、调用一次 nextTick, 将 flushSchedulerQueue 塞进微工作队列。
重要:也就是说,只有在宏工作运行过程中对 data 进行了一次赋值, 就会往微工作队列中塞一个 flushSchedulerQueue 函数的微工作(个别是 Promise)。waiting 只会在 flushSchedulerQueue 执行之后再次赋为 false

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {has[id] = true;
    if (!flushing) {queue.push(watcher);  // 退出 queue
    } else {
      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 (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue();
        return
      }
      nextTick(flushSchedulerQueue);  // nextTick flushSchedulerQueue
    }
  }
}

要害 :flushSchedulerQueue 函数做了什么:
1、遍历 queue 变量,获得 watcher
2、watcher.before() 调用,这个时候就是组件生命周期中的 beforeUpdate 回调告诉的时候。
3、watcher.run()调用,如果 watcher 对应的组件有配置 watch,就是这个时候执行回调,并且进行数据和 DOM 更新。
4、resetSchedulerState()调用,将 waiting=false,此时数据曾经更新结束,下次触发 reactiveSetter,则从新调用 nextTick
5、callUpdatedHooks,callActivatedHooks 调用,别离对应生命周期中的 activated 和 updated

function flushSchedulerQueue () {
  // debugger
  currentFlushTimestamp = getNow();
  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();  // beforeUpdate 回调(如果 before 属性存在的话)
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();  // 如果有配置 watche 监督属性

    // .... loop 报错揭示  ....
  }
  // 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');
  }
}

实例剖析

这是我本人刚刚碰到的案例,组件中触发以下代码:

const pro = new Promise((resolve, reject)=>{console.log('promise immediate 111')
    resolve('ok')
}).then(()=>{console.log('promise then 111')
})
this.$nextTick(()=>{console.log('nexcTick 111')
})
new Promise((resolve, reject)=>{console.log('promise immediate 222')
    resolve('ok')
}).then(()=>{console.log('promise then 222')
})
this.$nextTick(()=>{console.log('nexcTick 222')
})
this.visible = false // 数据操作
this.$nextTick(()=>{console.log('nexcTick 333')
})

以上代码,依照我一开始的认知,输入程序应该是:
promise immediate 111
promise immediate 222
nexcTick 111
promise then 222
nexcTick 222
nexcTick 333
但其实不然,操作数据触发了 reactiveSetter,它理论退出微工作队列的程序是:
1、promise then 111 微工作 1
2、nexcTick 111 -> callbacks
3、flushCallbacks 函数 微工作 2
4、promise then 222 微工作 3
5、nexcTick 222 -> callbacks
6、setter 调用, flushSchedulerQueue -> callbacks
7、nexcTick 333 -> callbacks
在 Vue 源码 nextTick 函数中退出 console 输入,验证猜测:

function nextTick (cb, ctx) {
  var _resolve;
  console.log(`${cb.name?cb.name:'箭头函数'}退出了 callbacks`)
  callbacks.push(...);
  if (!pending) {
    pending = true;
    timerFunc();}
}

后果:

总结

setter 触发时的总过程 :
1、reactiveSetter。这里首先扭转 data 对象中的值, 然而 DOM 尚未更新,能够说先存着
2、dep.notify。这里告诉该组件[依赖] 的每个 watcher
3、Watcher.update。这里调用 queueWatcher,让 wacher 入队列,为更新做筹备。另:如果强制同步更新 DOM 的话,这里就执行 this.run(), 执行对应的 DOM 更新操作。
4、queueWatcher。这里让 watcher 入待执行队列,并且如果是本次更新操作第一次 setter,则调用 nextTick 函数,让 flushSchedulerQueue 函数退出微工作队列。
5、flushSchedulerQueue。这里函数开始执行,代表宏工作曾经执行结束,开始执行微工作队列,这里将通过 beforeUpdate-> 更新 DOM->updated 的过程

nextTick 触发时的总过程:
0、timerFunc 赋值。依据操作系统不同,个别是 Promise 形式执行异步工作。
1、nextTick。往 callbacks 队列中退出一个待执行的回调,如果是一个更新周期中首次执行该函数,则调用 timerFunc,将 flushCallbacks 函数退出微工作队列。
2、flushCallbacks。这里顺次遍历 callbacks 队列中的待执行工作,程序执行,此时可能有用户本人调用的 nextTick 回调,也有可能中途执行了 setter 操作,插入了 flushSchedulerQueue 回调。在 flushSchedulerQueue 工作前后执行代码,状况齐全不同,这也是为什么在编写代码的过程中可能呈现不合乎预期的状况。

总结
Vue 中对于微工作的解决,尽管只插入一个微工作,然而数组形式存的待执行工作,就算是后执行的 setter 或者 nextTick,都能排在第一个 nextTick 或者 setter 调用时的优先程序执行。有种插队的感觉。

局部未提及源码

flushSchedulerQueue 中 watcher.before 函数, 对应 beforeUpdate 生命周期

  new Watcher(vm, updateComponent, noop, {before: function before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);

flushSchedulerQueue 中 watcher.run 函数, 此时进行数据更新

Watcher.prototype.run = function run () {if (this.active) {var value = this.get();
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {var info = "callback for watcher \"" + (this.expression) + "\"";
        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
      } else {this.cb.call(this.vm, value, oldValue); // 这里是 watch 回调
      }
    }
  }
};

flushSchedulerQueue 中 callUpdatedHooks 函数,生命周期 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');
    }
  }
}
退出移动版