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

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 穿插在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 使用场景有哪些? 提单按钮去重
相关推荐
Best_Liu~2 小时前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克2 小时前
下降npm版本
前端·vue.js
苏十八4 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
码农爱java4 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
st紫月4 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容5 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
skyshandianxia6 小时前
Java面试八股之如何提高MySQL的insert性能
java·mysql·面试
程楠楠&M8 小时前
vue3.0(十六)axios详解以及完整封装方法
前端·javascript·vue.js·axios·anti-design-vue
小C学安全8 小时前
【VUE基础】VUE3第三节—核心语法之ref标签、props
前端·vue.js·typescript
甜甜圈的小饼干8 小时前
Spring Boot+Vue项目从零入手
vue.js·spring boot·后端