深入Vue系列 next-tick原理和源码解析

简介

在 vue 的官方文档中有一个 API 叫做 nextTick,将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用这个方法,获取更新后的 DOM。 语法

vm.$nextTick([callback]);复制代码

参数:

{Function;}[callback];复制代码

用法 放在Vue.nextTick()回调函数中的执行的应该是涉及 DOM操作的 JavaScript 代码。

Vue 的响应式原理:在 data 选项里所有属性都会被watcher监控,当修改了data的某一个值,并不会立即反映到视图中。Vue 会将我们对data的更改放到watcher的一个队列中(异步),只有在当前任务空闲时才会去执行watcher队列任务。这就有一个延迟时间,所以对 dom 的操作要放在$nextTick中来操作,才能获取到最新的dom。

响应式对象 Observer > 依赖收集 Dep > 派发更新 Watcher

nextTick 是 Vue 的一个核心实现,如果还不了解 js 运行机制,可以看一下另一篇文章js 运行机制,这里就不多赘述了。

在浏览器环境中常见的 macro task 和 micro task 如下: macro task

setTimeout、setTimeInterval

MessageChannel

postMessage

setImmediate

requestAnimationFrame

I/O

UI 渲染a

micro task

MutationObsever

Promise.then

process.nextTick

vue 源码解析

派发更新 Watcher里面有用到nextTick(flushScheduerQueue),其实就是vue对派发更新的一个优化。下面直接看源码,在 src/core/util/next-tick.js 中:

// nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。import{ noop }from"shared/util";import{ handleError }from"./error";import{ isIOS, isNative }from"./env";// flushScheduerQueue/*存放异步执行的回调*/constcallbacks = [];//一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送letpending =false;/*下一个tick时的回调*/functionflushCallbacks(){  pending =false;//复制callbackconstcopies = callbacks.slice(0);//清除callbackscallbacks.length =0;for(leti =0; i < copies.length; i++) {//触发callback的回调函数copies[i]();  }}// Here we have async deferring wrappers using both microtasks and (macro) tasks.// In < 2.4 we used microtasks everywhere, but there are some scenarios where// microtasks have too high a priority and fire in between supposedly// sequential events (e.g. #4521, #6690) or even between bubbling of the same// event (#6566). However, using (macro) tasks everywhere also has subtle problems// when state is changed right before repaint (e.g. #6813, out-in transitions).// Here we use microtask by default, but expose a way to force (macro) task when// needed (e.g. in event handlers attached by v-on)./**

  其大概的意思就是:在Vue2.4之前的版本中,nextTick几乎都是基于microTask实现的,

  但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,

  就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场

  景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM

  的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。

  **/letmicroTimerFunc;letmacroTimerFunc;letuseMacroTask =false;// Determine (macro) task defer implementation.// Technically setImmediate should be the ideal choice, but it's only available// in IE. The only polyfill that consistently queues the callback after all DOM// events triggered in the same loop is by using MessageChannel./* istanbul ignore if */// 而对于macroTask的执行,Vue优先检测是否支持原生setImmediate(高版本IE和Edge支持),// 不支持的话再去检测是否支持原生MessageChannel,如果还不支持的话为setTimeout(fn, 0)。if(typeofsetImmediate !=="undefined"&& isNative(setImmediate)) {  macroTimerFunc =()=>{    setImmediate(flushCallbacks);  };}elseif(typeofMessageChannel !=="undefined"&&/**

    在Vue 2.4版本以前使用的MutationObserver来模拟异步任务。

    而Vue 2.5版本以后,由于兼容性弃用了MutationObserver。

    Vue 2.5+版本使用了MessageChannel来模拟macroTask。

    除了IE以外,messageChannel的兼容性还是比较可观的。

    **/(isNative(MessageChannel) ||// PhantomJSMessageChannel.toString() ==="[object MessageChannelConstructor]")) {/**

    可见,新建一个MessageChannel对象,该对象通过port1来检测信息,port2发送信息。

    通过port2的主动postMessage来触发port1的onmessage事件,

    进而把回调函数flushCallbacks作为macroTask参与事件循环。

    **/constchannel =newMessageChannel();constport = channel.port2;  channel.port1.onmessage = flushCallbacks;  macroTimerFunc =()=>{    port.postMessage(1);  };}else{/* istanbul ignore next */macroTimerFunc =()=>{    setTimeout(flushCallbacks,0);  };}// Determine microtask defer implementation./* istanbul ignore next, $flow-disable-line */if(typeofPromise!=="undefined"&& isNative(Promise)) {constp =Promise.resolve();  microTimerFunc =()=>{    p.then(flushCallbacks);// in problematic UIWebViews, Promise.then doesn't completely break, but// it can get stuck in a weird state where callbacks are pushed into the// microtask queue but the queue isn't being flushed, until the browser// needs to do some other work, e.g. handle a timer. Therefore we can// "force" the microtask queue to be flushed by adding an empty timer.if(isIOS) setTimeout(noop);  };}else{// fallback to macromicroTimerFunc = macroTimerFunc;}/**

* Wrap a function so that if any code inside triggers state change,

* the changes are queued using a (macro) task instead of a microtask.

*//*

    推送到队列中下一个tick时执行

    cb 回调函数

    ctx 上下文

  */exportfunctionwithMacroTask(fn: Function):Function{return(    fn._withTask ||    (fn._withTask =function(){      useMacroTask =true;constres = fn.apply(null,arguments);      useMacroTask =false;returnres;    })  );}exportfunctionnextTick(cb?: Function, ctx?: Object){let_resolve;  callbacks.push(()=>{if(cb) {try{        cb.call(ctx);      }catch(e) {        handleError(e, ctx,"nextTick");      }    }elseif(_resolve) {      _resolve(ctx);    }  });if(!pending) {    pending =true;if(useMacroTask) {      macroTimerFunc();    }else{      microTimerFunc();    }  }// $flow-disable-lineif(!cb &&typeofPromise!=="undefined") {returnnewPromise(resolve=>{      _resolve = resolve;    });  }}复制代码

nextTick这就是我们在上一节执行 nextTick(flushSchedulerQueue) 所用到的函数。它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacks。 flushCallbacks 这个方法就是挨个同步的去执行callbacks中的回调函数,callbacks中的回调函数是在调用 nextTick 的时候添加进去的; 这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。 注意这里有个比较难理解的地方,第一次调用 nextTick 的时候 pending 为false。 此时已经push到浏览器event loop中一个宏任务微任务的task,如果在没有flush掉的情况下继续往callbacks里面添加。 那么在执行这个占位queue的时候会执行之后添加的回调,所以macroTimerFunc、microTimerFunc 相当于task queue的占位。 以后 pending 为true则继续往占位queue里面添加,event loop轮到这个task queue的时候将一并执行。 执行 flushCallbacks 时 pending 置false,允许下一轮执行 nextTick 时往event loop占位。

macroTimerFunc、microTimerFunc next-tick.js 申明了 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。

nextTick 实现

首先 nextTick 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中。 这么做是因为防止单个 cb 如果执行错误不至于让整个JS 线程挂掉。 每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。

然后检查 pending 状态,这个跟之前介绍的 queueWatcher 中的 waiting 是一个意思。 它是一个标记位,一开始是 false 在进入macroTimerFunc、microTimerFunc方法前被置为 true。因此下次调用 nextTick 就不会进入macroTimerFunc、microTimerFunc方法。 这两个方法中会在下一个 macro/micro tick 时候 flushCallbacks 异步的去执行callbacks队列中收集的任务,而 flushCallbacks 方法在执行一开始会把 pending 置 false。 因此下一次调用 nextTick 时候又能开启新一轮的 macroTimerFunc、microTimerFunc,这样就形成了 vue 中的 event loop。

最后检查是否传入了 cb。因为 nextTick 还支持 Promise 化的调用:nextTick().then(() => {})。所以如果没有传入 cb 就直接return了一个Promise实例,并且把resolve传递给_resolve。这样后者执行的时候就跳到我们调用的时候传递进 then 的方法中。

示例

代码如下:

 

    {{ name }}    change name   
 
  export default {    data() {      return {        name: 'userName'      }    },    methods: {      change() {        const $name = this.$refs.name        console.log('同步方式1:' + this.$refs.name.innerHTML)        this.$nextTick(() => console.log('setter前:' + $name.innerHTML))        this.name = ' setterName '        console.log('同步方式2:' + this.$refs.name.innerHTML)        // setTimeout(() => {console("setTimeout方式:" + this.$refs.name.innerHTML)}        setTimeout(() => {          console.log('setTimeout方式:' + this.$refs.name.innerHTML);        });        this.$nextTick(() => console.log('setter后:' + $name.innerHTML))        this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML))      }    }  }复制代码

执行结果如下图所示:

>need-to-insert-img

同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 update,update 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的 DOM 元素还是原来的内容。

setter 前: setter前为什么还打印原来的是原来内容呢,是因为 nextTick 在被调用的时候把回调挨个push进callbacks数组,之后执行的时候也是 for 循环出来挨个执行,所以是类似于队列这样一个概念,先入先出;在修改name之后,触发把render watcher填入 schedulerQueue 队列并把他的执行函数 flushSchedulerQueue 传递给 nextTick ,此时callbacks队列中已经有了 setter前函数 了,因为这个 cb 是在 setter前函数 之后被push进callbacks队列的,那么先入先出的执行callbacks中回调的时候先执行 setter前函数,这时并未执行render watcher的 watcher.run,所以打印 DOM 元素仍然是原来的内容。

setter 后: setter后这时已经执行完 flushSchedulerQueue,这时render watcher已经把改动 patch 到视图上,所以此时获取 DOM 是改过之后的内容。

Promise 方式: 相当于 Promise.then 的方式执行这个函数,此时 DOM 已经更改。

setTimeout 方式: 最后执行macro task的任务,此时 DOM 已经更改。

注意,在执行 setter前函数 这个异步任务之前,同步的代码已经执行完毕。异步的任务都还未执行,所有的 $nextTick 函数也执行完毕。所有回调都被push进了callbacks队列中等待执行,所以在setter前函数执行的时候。此时callbacks队列是这样的:

[setter前函数, flushSchedulerQueue, setter后函数,Promise方式函数];复制代码

它是一个micro task队列,执行完毕之后执行macro task setTimeout,所以打印出上面的结果。

总结

nextTick是把要执行的任务推入到一个队列中,在下一个tick同步执行

数据改变后触发渲染watcher的update,但是watchers的flush是在nextTick后,所以重新渲染是异步的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容