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