持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
前言
大家好,在上一篇文章extend原理分享中,我们对vue2中一个不是很常用的全局API - extend的用法和使用场景做了简单介绍,并从源码层面对它的实现原理进行梳理和分析。接下来的分享中我们将继续学习vue中的另一个API - nextTick,相比extend,nextTick用的稍微会频繁一些。那么nextTick是干嘛的,在什么时候需要使用nextTick,它又是如何实现的?带着这些问题我们继续往下看。
nextTick的用法及使用场景
我们先从一个简单的获取DOM元素的案例入手:
- 假如在一个child.vue的data属性中有一个存放了用户名单的数组ary,现在想要通过v-for指令将数组中的名字渲染到页面的列表(li)元素中。
- 然后在页面渲染完成后,做如下3步操作(在mounted钩子函数中):
- 利用ref获取到ul下所有li子节点,并通过console.log将子节点个数输出
- 接着调用数组的pop方法将数组的最后一个元素删除
- 重复第一步,再利用ref获取到ul下所有li子节点,并通过console.log将子节点个数输出
- 启动程序看看两次输出的结果是否跟我们的心理预期一致
<ul ref="nameList">
<li v-for="name in ary" :key="name">{{name}}</li>
</ul>
export default{
data(){
return{
ary:["Alvin","Semon","Yannis","lyq"]
}
},
mounted(){
console.log(`pop前:${this.$refs.nameList.childNodes.length}`)
this.ary.pop();
console.log(`pop后:${this.$refs.nameList.childNodes.length}`)
}
}
如上图,从结果来看发现输出的结果并不是我们心里预期的结果,第一次输出了4是没问题的,在调用了pop删除数组中的最后一个元素后,页面上渲染出来的结果(3条内容)也是对的,但是第二次的输出就有问题了,按理说删除元素后第二次的输出应该是3才对,然而为什么第二次输出也是4呢?
原因就是:在vue中所有的DOM更新都是异步的,也就是说数据更新后不会立即触发DOM更新,而是要等到所有数据都更新完成后再一次性触发DOM更新,这么做的目的就是为了节省性能,避免不必要的性能浪费
这时就要到我们本次分享的主角 - nextTick闪亮登场了。Vue官方为我们提供的这个全局API - nextTick就是 为了解决这一问题而生的。一句话总结起来就是:
获取更新后的DOM元素
那么知道了nextTick的用途后,我们把上面的代码用nextTick来改造一下再看看能否达到我们的预期
mounted(){
console.log(`pop前:${this.$refs.nameList.childNodes.length}`)
this.ary.pop();
this.$nextTick(()=>{
console.log(`pop后:${this.$refs.nameList.childNodes.length}`)
});
}
诶,这时我们再看页面上输出了3条内容,在nextTick中的第二次输出结果也是3,这回跟我们的预期就一样了。
通过上面这个简单的案例我们知道了nextTick的用法及使用场景。简单总结一下就是:
DOM 更新是异步的,数据更新后不会立即触发DOM更新,这时如果想要获取更新后的DOM就需要调用nextTick方法来获取更新后的DOM
源码解析
那么为什么nextTick就能够获取到更新后的DOM,它又是如何做到的呢?下面我们来解读一下nextTick的源码,看看它为什么就能获取到更新后的DOM。
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
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们先来看下nextTick函数的主体都干了什么,总结起来就是三件事:
-
向数组callbacks中添加函数(callbacks在文件头部有定义)
callbacks是在文件头部定义的一个数组,在nextTick函数执行时首先向数组中添加一个函数,而在该函数体中主要又做了两件事:(概括来讲就是让函数执行)
- 在函数体中首先判断cb是否存在,如果存在则让回调函数cb执行(cb是调用nextTick时传进来的参数),并通过try catch做异常处理
- 如果cb不存在,再判断_resolve是否存在,如果存在则让_resolve执行(_resolve对应的就是Promise的resolve函数)
-
将变量pending置为true,并调用timerFunc函数执行
这里如果pending为false,则将其置为true,并调用timerFunc。关于timerFunc函数我们会在后面进行详细解析,而pending的作用:就是为了保证在同一时刻,任务队列中只能有一个 flushCallbacks 函数
-
返回一个promise实例
如果调用nextTick函数时没有传递参数,则直接返回一个promise实例,目的是把nextTick中的代码放在异步微任务中,也能够达到更新后的DOM的目的
flushCallbacks
在上面我们还提到了一个flushCallbacks函数,它又是干什么的呢
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
从代码来看这个函数很简单
- 首先就是将变量pending重新置为false,因为只有pending为false时才会执行timerFunc函数。
- 然后将callbacks拷贝一份保存在变量copies中,并将数组callbacks清空
- 遍历copies,让copies中的函数执行,其实就是让原callbacks中的函数执行(在nextTick中向callbacks数组添加的那些函数)
timerFunc
在前两步我们已经梳理出:在调用nextTick时首先会向callbacks数组中添加一个函数并在函数体中执行回调函数cb,然后再通过flushCallbacks函数让callbacks数组中的所有函数执行(在nextTick那步添加的那些函数)。那么flushCallbacks又是在什么时候执行的呢,我们继续来看剩下的最后一个模块timerFunc也是nextTick的核心模块。
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
- 首先判断浏览器是否支持Promise,如果支持则创建一个Promise实例,然后在timerFunc函数中将flushCallbacks函数添加到Promise的异步微任务队列中,并将isUsingMicroTask置为true(默认为false),意思就是使用的是异步微任务
- 如果不支持Promise,再看是否支持MutationObserver,如果支持则使用MutationObserver将flushCallbacks函数添加到异步微任务队列中,目的也是使用异步微任务让flushCallbacks执行
- 如果以上两种都不支持,再看setImmediate是否支持,如果支持则利用setImmediate将flushCallbacks添加的异步宏任务队列,注意到这时已经是宏任务了
- 最后的最后如果实在不行就使用异步宏任务setTimeout,总之就是一个目的:让flushCallbacks异步执行
总结
通过本次分享,我们了解了nextTick的基本用法和使用场景,然后又基于其源码分析梳理了nextTick是如何实现获取更新后的DOM的。简单总结起来就是:
先将nextTick中的回调函数添加到异步队列中,然后再通过异步微任务或宏任务让队列中的函数执行,其原则就是:优先使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。总之最终目的就是让nextTick中的函数异步执行,这样就能够获取到更新后的DOM了。
今天的分享就到这里了。喜欢的小伙伴欢迎点赞哦。