写点什么

Vue.nextTick 核心原理

作者:yyds2026
  • 2022-10-18
    浙江
  • 本文字数:3518 字

    阅读完需:约 1 分钟

相信大家在写 vue 项目的时候,一定会发现一个神奇的 api,Vue.nextTick。为什么说它神奇呢,那是因为在你做某些操作不生效时,将操作写在Vue.nextTick内,就神奇的生效了。那这是什么原因呢?


让我们一起来研究一下。

简述

  • vue 实现响应式并不是数据发生变化后 DOM 立即变化,而是按照一定策略异步执行 DOM 更新的

  • vue 在修改数据后,视图不会立刻进行更新,而是要等同一事件循环机制内所有数据变化完成后,再统一进行 DOM 更新

  • nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。

事件循环机制

在讨论Vue.nextTick之前,需要先搞清楚事件循环机制,算是实现的基石了,那我们来看一下。


在浏览器环境中,我们可以将我们的执行任务分为宏任务和微任务,


  • 宏任务: 包括整体代码scriptsetTimeoutsetIntervalsetImmediate、 I/O 操作、UI 渲染

  • 微任务: Promise.thenMuationObserver



事件循环的顺序,决定 js 代码的执行顺序。事件循环如下:



用代码解释,浏览器中事件循环的顺序同如下代码:


for (macroTask of macroTaskQueue) {     // 1. 执行一个宏任务    handleMacroTask();    // 2. 执行所有的微任务    for (microTask of microTaskQueue) {         handleMicroTask(microTask);    } }
复制代码

vue 数据驱动视图的处理(异步变化 DOM)

<template>  <div>    <div>{{count}}</div>    <div @click="handleClick">click</div>  </div></template>export default {    data () {        return {            number: 0        };    },    methods: {        handleClick () {            for(let i = 0; i < 10000; i++) {                this.count++;            }        }    }}
复制代码


分析上述代码:


  • 当点击按钮时,count 会被循环改变 10000 次。那么每次 count+1,都会触发 count 的setter方法,然后修改真实 DOM。按此逻辑,这整个过程,DOM 会被更新 10000 次,我们都知道 DOM 的操作是非常昂贵的,而且这样的操作完全没有必要。所以 vue 内部在派发更新时做了优化

  • 也就是,并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列 queueWatcher 里,然后在 nextTick 后执行 flushSchedulerQueue 处理

  • 当 count 增加 10000 次时,vue 内部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可

Vue.nextTick原理

由上一节我们知道,Vue 中 数据变化 => DOM 变化 是异步过程,一旦观察到数据变化,Vue 就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher(Vue 源码中的 Wacher 类是用来更新 Dep 类收集到的依赖的)推送进这个队列。


如果这个 watcher 被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和 DOM 操作。而在下一个事件循环时,Vue 会清空队列,并进行必要的 DOM 更新。


nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback),JS 是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。


vue 中 next-tick.js 的源码如下


参考 vue 实战视频讲解:进入学习


import { noop } from 'shared/util'import { handleError } from './error'import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,// 所有的 cb 都会被存在这个 callbacks 数组中const callbacks = []// pending 是一个标记位,代表一个等待的状态let pending = false
// 最后执行 flushCallbacks() 方法,遍历callbacks数组,依次执行里边的每个函数function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() }}
let timerFunc
/*判断采用哪种异步回调方式由于微任务优先级高,首先尝试微任务模拟1.首先尝试使用Promise.then(微任务)2.尝试使用MuationObserver(微任务)回调3.尝试使用 setImmediate(宏任务)回调4.最后尝试使用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) }}
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 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是 task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。


nextTick 的调用方式


  1. 回调函数方式:Vue.nextTick(callback)

  2. Promise 方式:Vue.nextTick().then(callback)

  3. 实例方式:vm.$nextTick(callback)

Vue.nextTick的应用

created 生命周期中操作 DOM

created 钩子函数执行的时候 DOM 其实并未进行挂载和渲染,此时就是无法操作 DOM 的,我们将操作 DOM 的代码中放到 nextTick 中,等待下一轮事件循环开始,DOM 就已经进行挂载好了,而与这个操作对应的就是 mounted 钩子函数,因为在 mounted 执行的时候所有的 DOM 挂载已完成。


created(){  vm.$nextTick(() => {        //不使用this.$nextTick()方法操作DOM会报错      this.$refs.test.innerHTML="created中操作了DOM"  });}

复制代码

修改数据,获取 DOM 值

当我们修改了 data 里的数据时,并不能立刻通过操作 DOM 去获取到里面的值


<template>  <div class="test">    <p ref='msg' id="msg">{{msg}}</p>  </div></template>
<script>export default { name: 'Test', data () { return { msg:"hello world", } }, methods: { changeMsg() { this.msg = "hello Vue" // vue数据改变,改变了DOM里的innerText let msgEle = this.$refs.msg.innerText //后续js对dom的操作 console.log(msgEle) // hello world // 输出可以看到data里的数据修改后DOM并没有立即更新,后续的DOM不是最新的 this.$nextTick(() => { console.log(this.$refs.msg.innerText) // hello Vue }) this.$nextTick().then(() => { console.log(this.$refs.msg.innerText) // hello Vue }) }, changeMsg2() { this.$nextTick(() => { console.log(this.$refs.msg.innerText) // 1.hello world }) this.msg = "hello Vue" // 2. console.log(this.$refs.msg.innerText) // hello world this.$nextTick().then(() => { console.log(this.$refs.msg.innerText) // hello Vue }) // nextTick中先添加的先执行,执行1后,才会执行2(Vue操作Dom的异步) } }}</script>
复制代码

v-show/v-if 由隐藏变为显示

点击按钮显示原本以 v-show=false 或 v-if 隐藏起来的输入框,并获取焦点或者获得宽高等的场景


用户头像

yyds2026

关注

还未添加个人签名 2022-09-08 加入

还未添加个人简介

评论

发布
暂无评论
Vue.nextTick核心原理_Vue_yyds2026_InfoQ写作社区