Vue源码解析之nextTick
vue源码解析之nexttick
前言
nexttick是vue的一个核心功能,在vue内部实现中也经常用到nexttick。但是,很多新手不理解nexttick的原理,甚至不清楚nexttick的作用。
那么,我们就先来看看nexttick是什么。
nexttick功能
看看官方文档的描述:
在下次 dom 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 dom。
再看看官方示例:
// 修改数据 vm.msg = 'hello' // dom 还没有更新 vue.nexttick(function () { // dom 更新了 }) // 作为一个 promise 使用 (2.1.0 起新增,详见接下来的提示) vue.nexttick() .then(function () { // dom 更新了 })
2.1.0 起新增:如果没有提供回调且在支持 promise 的环境中,则返回一个 promise。请注意 vue 不自带 promise 的 polyfill,所以如果你的目标浏览器不原生支持 promise (ie:你们都看我干嘛),你得自己提供 polyfill。
可以看到,nexttick主要功能就是改变数据后让回调函数作用于dom更新后。很多人一看到这里就懵逼了,为什么需要在dom更新后再执行回调函数,我修改了数据后,不是dom自动就更新了吗?
这个和js中的event loop有关,网上教程不计其数,在此就不再赘述了。建议明白event loop后再继续向下阅读本文。
举个实际的例子:
我们有个带有分页器的表格,每次翻页需要选中第一项。正常情况下,我们想的是点击翻页器,向后台获取数据,更新表格数据,操纵表格api选中第一项。
但是,你会发现,表格数据是更新了,但是并没有选中第一项。因为,你选中第一项时,虽然数据更新了,但是dom并没有更新。此时,你可以使用nexttick,在dom更新后再操纵表格第一项的选中。
那么,nexttick到底做了什么了才能实现在dom更新后执行回调函数?
源码分析
nexttick的源码位于src/core/util/next-tick.js,总计118行,十分的短小精悍,十分适合初次阅读源码的同学。
nexttick源码主要分为两块:
1.能力检测
2.根据能力检测以不同方式执行回调队列
能力检测
这一块其实很简单,众所周知,event loop分为宏任务(macro task)以及微任务( micro task),不管执行宏任务还是微任务,完成后都会进入下一个tick,并在两个tick之间执行ui渲染。
但是,宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务。
nexttick在能力检测这一块,就是遵循的这种思想。
// 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 */ // 如果浏览器不支持promise,使用宏任务来执行nexttick回调函数队列 // 能力检测,测试浏览器是否支持原生的setimmediate(setimmediate只在ie中有效) if (typeof setimmediate !== 'undefined' && isnative(setimmediate)) { // 如果支持,宏任务( macro task)使用setimmediate macrotimerfunc = () => { setimmediate(flushcallbacks) } // 同上 } else if (typeof messagechannel !== 'undefined' && ( isnative(messagechannel) || // phantomjs messagechannel.tostring() === '[object messagechannelconstructor]' )) { const channel = new messagechannel() const port = channel.port2 channel.port1.onmessage = flushcallbacks macrotimerfunc = () => { port.postmessage(1) } } else { /* istanbul ignore next */ // 都不支持的情况下,使用settimeout macrotimerfunc = () => { settimeout(flushcallbacks, 0) } }
首先,检测浏览器是否支持setimmediate,不支持就使用messagechannel,再不支持只能使用效率最差但是兼容性最好的settimeout了。
之后,检测浏览器是否支持promise,如果支持,则使用promise来执行回调函数队列,毕竟微任务速度大于宏任务。如果不支持的话,就只能使用宏任务来执行回调函数队列。
执行回调函数队列
执行回调函数队列的代码刚好在一头一尾
// 回调函数队列 const callbacks = [] // 异步锁 let pending = false // 执行回调函数 function flushcallbacks () { // 重置异步锁 pending = false // 防止出现nexttick中包含nexttick时出现问题,在执行回调函数队列前,提前复制备份,清空回调函数队列 const copies = callbacks.slice(0) callbacks.length = 0 // 执行回调函数队列 for (let i = 0; i < copies.length; i++) { copies[i]() } } ... // 我们调用的nexttick函数 export function nexttick (cb?: function, ctx?: object) { let _resolve // 将回调函数推入回调队列 callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleerror(e, ctx, 'nexttick') } } else if (_resolve) { _resolve(ctx) } }) // 如果异步锁未锁上,锁上异步锁,调用异步函数,准备等同步函数执行完后,就开始执行回调函数队列 if (!pending) { pending = true if (usemacrotask) { macrotimerfunc() } else { microtimerfunc() } } // $flow-disable-line // 2.1.0新增,如果没有提供回调,并且支持promise,返回一个promise if (!cb && typeof promise !== 'undefined') { return new promise(resolve => { _resolve = resolve }) } }
总体流程就是,接收回调函数,将回调函数推入回调函数队列中。
同时,在接收第一个回调函数时,执行能力检测中对应的异步方法(异步方法中调用了回调函数队列)。
如何保证只在接收第一个回调函数时执行异步方法?
nexttick源码中使用了一个异步锁的概念,即接收第一个回调函数时,先关上锁,执行异步方法。此时,浏览器处于等待执行完同步代码就执行异步代码的情况。
打个比喻:相当于一群旅客准备上车,当第一个旅客上车的时候,车开始发动,准备出发,等到所有旅客都上车后,就可以正式开车了。
当然执行flushcallbacks函数时有个难以理解的点,即:为什么需要备份回调函数队列?执行的也是备份的回调函数队列?
因为,会出现这么一种情况:nexttick套用nexttick。如果flushcallbacks不做特殊处理,直接循环执行回调函数,会导致里面nexttick中的回调函数会进入回调队列。这就相当于,下一个班车的旅客上了上一个班车。
实现一个简易的nexttick
说了这么多,我们来实现一个简单的nexttick:
let callbacks = [] let pending = false function nexttick (cb) { callbacks.push(cb) if (!pending) { pending = true settimeout(flushcallback, 0) } } function flushcallback () { pending = false let copies = callbacks.slice() callbacks.length = 0 copies.foreach(copy => { copy() }) }
可以看到,在简易版的nexttick中,通过nexttick接收回调函数,通过settimeout来异步执行回调函数。通过这种方式,可以实现在下一个tick中执行回调函数,即在ui重新渲染后执行回调函数。
推荐阅读
-
Vue源码解析之nextTick
-
1.vue的MVVM响应式原理介绍——vue源码解析
-
23-Spring源码解析之AOP(2)——AnnotationAwareAspectJAutoProxyCreator类源码
-
spring源码解析之七(spring-mybatis)
-
Ucenter源码解析之--index.php
-
Vue源码解析之数组变异的实现
-
死磕 java同步系列之CountDownLatch源码解析
-
Spring源码解析之-TypeConverter、TypeConverterDelegate分析
-
andorid jar/库源码解析之Butterknife
-
RocketMQ源码深度解析一之消息存储