nextTick
在 Vue 中是一个很出名的工具函数,我们在实际运用的时候也经常会用到,那么它实际上到底有什么样的作用,Vue 中又是如何设计的,我们在日常中有什么场景是可以借鉴的。
我们以 Vue 最新的 v2.6.14 版本来分析,链接 https://github.com/vuejs/vue/blob/v2.6.14/src/core/util/next-tick.js
nextTick
是个什么东西,参考 Vue 2 的官方 API 文档:https://cn.vuejs.org/v2/api/#Vue-nextTick
可以看出是执行一个回调函数,我们这里可以成为一个任务,那在 Vue 中文档已经讲明白了,在下次 DOM 更新循环结束后执行这个任务(回调),这样你就可以取到更新后的 DOM 了。
先来看下 nextTick 全部的代码,把flow相关去掉,加上我们自己的注释:
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// 是否使用的是 MicroTask,如 Promise MutationObserver
// 如果浏览器不支持 则会使用 MacroTask setImmediate setTimeout
// 相关进一步知识可以参考 浏览器 eventloop 相关文章
export let isUsingMicroTask = false
// 储存所有的 callback 队列,可以认为是一个个任务
const callbacks = []
// 是否在等待执行
let pending = false
// 依次执行任务队列,循环 & 执行
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 实现异步的函数,从名字上看下一个 tick,即一个 timer
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 原生 Promise 异步
const p = Promise.resolve()
timerFunc = () => {
// 利用 promise.then 实现,一个 micro task 之后执行 flushCallbacks
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)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 降级使用 MutationObserver
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
// 触发textNode的改变,进而触发MutationObserver的回调执行 flushCallbacks
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
// 直接利用 setImmediate
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
// 经典的 setTimeout
setTimeout(flushCallbacks, 0)
}
}
// 主实现
export function nextTick (cb, ctx) {
let _resolve
// 往 callbacks 队列中添加一个一个任务
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果不是在等待中,即上一轮的callbacks任务队列已经执行完毕
// 那么就进入等待状态,重新进入新一轮的等待下一个timer然后执行新一轮存下来的callbacks任务队列
if (!pending) {
pending = true
timerFunc()
}
// nextTick的另一种用法,nextTick().then()
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们可以看出来,代码虽然不多,但是处理的情况还是很多的,也有很多兼容性的处理。如果我们来翻译下,nextTick 最核心的实现就是:拿一个队列存储所有要执行的任务,在下一个tick(异步)执行这些任务。
那根据这个核心实现,在不考虑兼容性和异常的情况下,我们可以实现一个极简版本的 nextTick:
let pending = false
const tasks = []
const flushCallbacks = () => {
pending = false
tasks.forEach(task => task())
tasks.length = 0
}
const p = Promise.resolve()
const timerFunc = () => {
p.then(flushCallbacks)
}
function nextTick(task) {
tasks.push(task)
if (!pending) {
pending = true
timerFunc()
}
}
短短20行,但是功能很核心也很强大,我们可以像这样使用:
const task1 = () => console.log('1')
const task2 = () => console.log('2')
console.log('before')
nextTick(task1)
nextTick(task2)
console.log('after')
// 运行的结果:before after 1 2
这个时候,相信你已经更进一步理解了 nextTick
:将需要异步执行的任务收集起来在下一个 tick 依次执行他们。
那为什么需要 nextTick
呢,我们不能直接执行这些任务吗?在 Vue 中的话,官网也给到了大家答案,详情 https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97。
如果简化来理解的话就是:为了更好的性能,将更新 DOM 操作存放在异步更新队列中,在下一个 tick 统一进行更新 DOM 操作。
试想下,如果我们每更新一次数据,Vue 就需要去更新一次 DOM 操作的话,得有多卡顿,因为日常我们处理逻辑一定是这样的:
const data = {
title: 'hello',
desc: 'world'
}
this.msg = data.title
this.context = data.desc
这个还是一个局部场景,更别想说,我们的整个 Vue 应用的数据更新,DOM 更新了。
所以 Vue 中就采用了异步更新队列这种方式来进行优化,也就是依赖上边我们分析的 nextTick
所做的最核心的事情。
在 nextTick
之中,我们可以从其中学到什么或者我们可以进一步了解什么呢?
看出这里边对于队列的操作(当然,用数组模拟的,本质是一样的):队列里添加任务,执行队列里的任务,清空队列。
队列是一个我们十分常用的数据结构,上边所提到的 eventloop,你会发现和 nextTick 本质是一样的,只是变得更复杂了,存在多个队列的情况,需要处理。
我们知道了部分 timerFunc
的实现,相对应的也就是我们需要知道,哪些 API 的操作是异步的,以及是哪种异步处理(MacroTask、MicroTask),他们之间有什么差异和使用的影响,我们遇到异步场景的时候应该如何去选择。
还有一个点,这里用到了降级的方案 setTimeout
,传的第二个参数是 0,那么这个时候的效果是啥样的;setTimeout 还可以有其他的什么用法,到底可以有几个参数,返回值是啥类型的,什么时候需要我们手工去 clearTimeout。
相对应的延伸,就是大名鼎鼎的 eventloop 相关知识,也需要去区分浏览器环境和 Node.js 环境。
异步和队列碰撞在一起,可以有很多火花。
我们有很多时候时候都需要处理异步任务,而对于这些任务的处理,最合适的数据结构就是队列了,例如大名鼎鼎的 async 库 https://github.com/caolan/async 简直就是把异步玩到了极致,里边有很多很好的实现思路以及技巧,感兴趣的也是可以深入了解的。
我们的现实需求也一样,例如,在小程序场景中,不能超出10个的并发请求,超出的请求会被取消掉,所以我们需要对请求进行封装一层,在mpx中是封装为了mpx-fetch,而且我们还要求了高低优先级两种请求,这种情况,就需要我们借助于队列来实现我们的需求。
在 flushCallbacks
中,我们看到了一个技巧,正常我们自己的简单实现中,是直接便利 callbacks 然后执行的,而 Vue 中则不是,他是复制了一份新的,然后循环执行的。
这么做的原因,其实是考虑了一种特殊情况,如果某一个 callback 执行的时候,又一次调用了 nextTick,进而更新了 callbacks,那这个时候的执行就不是我们所期望的了。所以需要先拷贝一份原有的,即使在 cb 中更新了 callbacks 也不影响我们的循环和执行,符合预期。
这是一个很严谨的地方,我们在实际场景中,也要有这种思考和意识。
同时这个问题还可以有很多的延伸,针对于数组循环,正向循环和逆向循环有啥区别吗,是不是都一样;以及我们用 for 循环和用数组本身的 forEach 会有啥不一样吗;还有 for 循环的终止条件,我们写 i < array.length
和 const len = array.length; i < len
有啥区别没有?
Promise 是一个很好的东西,相当有用,我们需要深入理解并使用它。这里有一个比较有意思的一个点是 nextTick
的返回值处理,应用到了一个技巧:外部如何更新 Promise 的状态,即你所看到的 _resolve
这个变量的作用。
Promise,一个各大厂基本都在考察的,Promise有哪些规范,包含哪些定义,哪些API,如何实现一个 Promise。
希望你去深入学习和理解它,做到精通 Promise!
isNative
的处理,他是如何判断的Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8