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 
                        
关键字:
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信