让人茶饭不思一个 vue nextTick 的困惑——按钮点击去重

nextTick 按钮去重场景

说一个 nextTick 场景,提单按钮去重的场景。网上很多方案是用节流或者防抖,但是问题——延迟,一般不会手写,多引一个库lodash。

于是,聪明人于是想了一个办法,点击按钮后,加一个 loadding,第2次点击在一个空白的遮罩。于是得到下面类似代码。

export default {
methods: {
async clickHander () {
  vue.loadinng.show()
      const res = await fetchList(params)
      vue.loadinng.hide()
      // ...
    }
}
}

但是,上面代码还是不去重,见太多开发这种写法出现问题,于是有加上节流和防抖,有种哭笑不得感觉。

为什么不能去重?如果你加上 nextTick 技能去重了,如下面代码。很多同学,这是啥,await nextTick() 不是异步,再加一个异步怎么可能解决?事实就能解决,是不是头大?

export default {
methods: {
async clickHander () {
  vue.loadinng.show()
  await nextTick()
      const res = await fetchList(params)
      vue.loadinng.hide()
      // ...
    }
}
}

需要先说明一下 await nextTick() 和下面 nextTick(cb) 形式是一样,也就上面的代码可以改成下面,有很多同学基本上使用下面一种,甚至认为 nextTick 不是异步的。这个后面说道。

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

// 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。具体如下

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 调度器,用于异步执行更新,源码:https://github.com/vuejs/vue/blob/main/src/core/observer/scheduler.ts#L92

export function queueWatcher(watcher: Watcher) {
  // ……
  if (!waiting) {
    waiting = true
if (__DEV__ && !config.async) {
  flushSchedulerQueue()
  return
}
  • nextTick(flushSchedulerQueue)
    }
    }

在网上看到绝大数,告诉你是下面这张图,是不觉得奇怪,看不懂。很少说明 nextTick 怎么做的保证 dom 一定渲染后触发 nextTick 的回调 ?

解释 nextTick 按钮提单去重?怎么去验证调试源码

你的了解一个前提,点击是宏任务,nextTick 几乎是微任务。再看上图,是不是就能理解,nextTick 按钮提单去重。

还不是能理解,那就调试呗?调试代码如下:


  
切换 {{ count }}
显示隐藏

这里没使用loading,loading使用动画过程,不好验证结论,换成打印console.log。

你猜双击点击上面代码中按钮,打印的顺序那种

第1种

1
2
3
change
click

第2种 还是 click 穿插在1change 之间

答案永远是第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 使用场景有哪些? 提单按钮去重

这是一个从 https://juejin.cn/post/7368637177428410368 下的原始话题分离的讨论话题