nextTick 按钮去重场景
说一个 nextTick 场景,提单按钮去重的场景。网上很多方案是用节流或者防抖,但是问题------延迟,一般不会手写,多引一个库lodash。
于是,聪明人于是想了一个办法,点击按钮后,加一个 loadding,第2次点击在一个空白的遮罩。于是得到下面类似代码。
javascript
export default {
methods: {
async clickHander () {
vue.loadinng.show()
const res = await fetchList(params)
vue.loadinng.hide()
// ...
}
}
}
但是,上面代码还是不去重,见太多开发这种写法出现问题,于是有加上节流和防抖,有种哭笑不得感觉。
为什么不能去重?如果你加上 nextTick
技能去重了,如下面代码。很多同学,这是啥,await nextTick()
不是异步,再加一个异步怎么可能解决?事实就能解决,是不是头大?
javascript
export default {
methods: {
async clickHander () {
vue.loadinng.show()
await nextTick()
const res = await fetchList(params)
vue.loadinng.hide()
// ...
}
}
}
需要先说明一下 await nextTick()
和下面 nextTick(cb)
形式是一样,也就上面的代码可以改成下面,有很多同学基本上使用下面一种,甚至认为 nextTick
不是异步的。这个后面说道。
javascript
export default {
methods: {
async clickHander () {
vue.loadinng.show()
await nextTick(() => {
const res = await fetchList(params)
vue.loadinng.hide()
})
// ...
}
}
}
nextTick 定义是什么
官方给出定义,大概可以翻译成:nextTick的回调在下次 idle 的时候执行。具体场景:是在操作 dom 数据,dom不及时显示,需要nextTick 阻塞一下。
大家想想dom为什么不及时显示,dom修改不是同步的吗?在看下下面一个问题
为什么 Vue 的渲染是异步的?
vue 的更新机制,dom更新式异步,同步更新会导致性能浪费,为什么这样说?举个例子,1个数据变更1次,dom更新一次,1个数据在极端时间(用户无感知)内,更新100次,dom更新难道就100次,显然不是,而是1次。业务项目实践,多数render、视图渲染是异步并发的,但是有些场景,需要同步串行。
而这串行的场景,就需要 nextTick
,这就是 Vue 给出的方案。
那 nextTick 怎么做的保证 dom 一定渲染后触发 nextTick 的回调
方案很简单,每当我们在使用 nextTick
,变更响应式数据(vue2 data 对象属性,vue3 ref 和 reactive), 实际执行2次 nextTick
,即执行2次 Promise.resolve()
大家先不看上方案,你会想出什么方案?那怎么验证 Vue使用这个方案了。
第一个读的源码 nextTick ,我就不贴全代码,直接到 github 读代码,很轻松的。大致说一下 nextTick
javascript
// next
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
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 本身一个观察者模式,它和 promise 、event 很类似, - pedding 实现阻塞 - flush 清楚回调,回调全部执行,当然,nextTick 1对1观察者模式 - 异步回调,直接使用promsie > mutionObserver > setTimeout
如果你了解观察者模式,过一下就行,这不是重点,重点异步回调执行 timerFunc()
,你会考到异步优先级 promsie > mutionObserver > setTimeout。具体如下
javascript
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) ||
// PhantomJS and iOS 7.x
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)
}
}
看到这里,读上面代码只是解释 await nextTick()
。是不是疑问,哪里有2次 nextTick ?
需要读另外一个代码,响应式数据变更怎么通知?其实也不看那么多代码,只需看通知后回调怎么执行?你发现有是观察模式,只需看异步回调执行就够了。
vue 调度器,用于异步执行更新,源码:github.com/vuejs/vue/b...
diff
export function queueWatcher(watcher: Watcher) {
// ......
if (!waiting) {
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
+ nextTick(flushSchedulerQueue)
}
}
在网上看到绝大数,告诉你是下面这张图,是不觉得奇怪,看不懂。很少说明 nextTick 怎么做的保证 dom 一定渲染后触发 nextTick 的回调 ?
解释 nextTick 按钮提单去重?怎么去验证调试源码
你的了解一个前提,点击是宏任务,nextTick 几乎是微任务。再看上图,是不是就能理解,nextTick 按钮提单去重。
还不是能理解,那就调试呗?调试代码如下:
htmlbars
<template>
<div>
<button @click="change"> 切换</button> {{ count }}
<div v-if="flag">显示隐藏</div>
</div>
</template>
<script>
export default {
data () {
return {
count: 0,
flag: true
}
},
methods: {
setCount () {
this.count = this.count + 1
},
async change () {
for (let i = 0; i < 1000; i++) {
this.setCount()
console.log(this.count)
}
await this.$nextTick()
console.log('change')
}
},
mounted () {
document.body.onclick = () => {
console.log('click')
this.flag = !this.flag
}
}
}
</script>
这里没使用loading,loading使用动画过程,不好验证结论,换成打印console.log。
你猜双击点击上面代码中按钮,打印的顺序那种
第1种
arduino
1
2
3
change
click
第2种 还是 click 穿插在1
到change
之间
答案永远是第1种,无论你怎么双击多快。甚至你可以,await this.$nextTick()
换成 await Promise.resolve()
,也是第1种。然后你在试 setTime(() => console.log('change'))
,就是第2种了。
接着再是调用 nextTick 2 次以上,只需要验证响应数据变更都会执行 nextTick(flushSchedulerQueue)
,可以打 debugger 看,你会发现每次都执行这里。
总结
本文示意图,不是读 nextTick 源码理解 nextTick ?而是怎么知道 nextTick ,怎么好使用nextTick。总结下来面4个问题。
- 为什么 有 nextTick? 为了性能,不是每次响应数据变更,都进行渲染
- 怎么实现 nextTick? 怎么保证dom渲染nextTick,2次执行 nextTick,第1次是响应式数据变更,通知的回调放在 nextTick 执行,第2次才是业务的
nextTick
. - nextTick 是什么? dom 不及时更新,为了确保在dom更新之后,执行回调的一个api
- nextTick 使用场景有哪些? 提单按钮去重