前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue.js nextTick 源码分析

Vue.js nextTick 源码分析

作者头像
用户6256742
发布2024-07-09 09:31:12
760
发布2024-07-09 09:31:12
举报
文章被收录于专栏:网络日志

nextTick

vue版本

2.6.11

源码分析(nextTick)

nextTick源码调用过程总结:

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

代码语言:javascript
复制
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的源码如下:

代码语言:javascript
复制
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的微任务队列中,待顺序调用。

代码语言: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实例

代码语言:javascript
复制
Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
  };

源码分析(set过程)

Vue对每个组件中的data都做了数据代理(截持),对data对象中的数据进行赋值操作,实际就会调用defineProperty中的reactiveSetter函数,进行一系列操作,包括通知Watcher数据改变了等等。

其中setter源代码如下,不止进行赋值操作,还会调用dep.notify()通知数据改变了:

代码语言:javascript
复制
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方法

代码语言:javascript
复制
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函数

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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');
  }
}

实例分析

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

代码语言:javascript
复制
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函数 微任务24、promise then 222 微任务3 5、nexcTick 222 -> callbacks 6、setter调用, flushSchedulerQueue -> callbacks7、nexcTick 333 -> callbacks 在Vue源码nextTick函数中加入console输出,验证猜想:

代码语言:javascript
复制
function nextTick (cb, ctx) {
  var _resolve;
  console.log(`${cb.name?cb.name:'箭头函数'}加入了callbacks`)
  callbacks.push(...);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

结果:

Vue.js nextTick 源码分析
Vue.js nextTick 源码分析

总结

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生命周期

代码语言:javascript
复制
  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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');
    }
  }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-09-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • nextTick
    • vue版本
      • 源码分析(nextTick)
        • 源码分析(set过程)
          • 实例分析
            • 总结
              • 部分未提及源码
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档