开发者社区 > 博文 > 解析$nextTick魔力,为啥大家都爱它?
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

解析$nextTick魔力,为啥大家都爱它?

  • jd****
  • 2023-12-11
  • IP归属:北京
  • 4960浏览

    1.为什么需要使用$nextTick?

    首先我们来看看官方对于$nextTick的定义:

    在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

    由于vue的试图渲染是异步的,生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM其实并未进行渲染,而此时进行DOM操作是徒劳的,所以一定要将DOM操作的js代码放到Vue.nextTick()的回调函数中。除了在created()钩子函数中使用之外咱们还会遇到很多种需要使用到Vue.nextTick()的场景,如下所示:

    咱们日常生活中常常会遇上上述场景,当我们点击按钮更新数据时候,如下示例:

    
    <template> 
        <div>
         <button @click="handleClick()" ref="test">{{ msg }}</button>
        </div>
    </template>
    
    <script>
    export default {
     name: 'nextTick',
     data() {
         return {
             msg: '初始值',
         }
     },
     methods: {
        handleClick: function () {
             this.msg = '修改之后的值';
             console.log(this.$refs.test.innerText); // 初始值
         }
      }
    }
    
    </script>

    以上代码模板输出的值是'初始值',而不是修改之后的值;这是因为查看是同步操作的,但是赋值是异步操作的,并没有立马执行而是把它放进一个队列中去,并将同一事件循环中发生的数据变更都会放在这一个队列中,vue会等待同一事件循环中的数据变化完成之后,才会将数据队列中的事件拿出来处理;如果不这样做的话那如果主线程中循环操作了100次数据就要更新100次dom,这一定程度上影响了项目性能,所以vue会等事件循环完成后更新dom,这样就只需要更新一次,极大的提升了性能;

    所以现在Vue.nextTick()派上了用场,Vue.nextTick() 方法的作用正是等待上一次事件循环执行完毕,并在下一次事件循环开始时再执行回调函数。这样可以保证回调函数中的 DOM 操作已经被 Vue.js 进行过更新,从而避免了一些潜在的问题。使用this.$nextTick()就可以获取到修改后的最新数据,如下代码所示:

    <template> 
     <div>
        <button @click="handleClick()" ref="test">{{ msg }}</button>
      </div>
    </template>
    
    <script>
    export default {
     name: 'nextTick',
     data() {
         return {
             msg: '初始值',
        }
     },
     methods: {
         handleClick: function () {
             this.msg = '修改之后的值';
             this.$nextTick(()=>{
             console.log(this.$refs.test.innerText);// 修改之后的值
            })
         }
     }
    }
    
     </script>

    加上this.$nextTick后输入的值就是修改之后的值,显示正确;

    总而言之Vue.nextTick()就是在下次 DOM 更新渲染后执行延迟回调函数。在日常开发中,我们在修改数据之后使用这个方法,就可以获取更新后的 DOM的同时进行在对DOM进行相对应操作的 js代码;

    2.$nextTick如何实现的?

    JS是单线程执行的,所有的同步任务都是在主线程上执行的,形成了一个执行栈,从上到下依次执行,异步代码会放在任务队列里面。

    • 同步任务

    在主线程里执行,当浏览器第一遍过滤html文件的时候可以执行完;(在当前作用域直接执行的所有内容,包括执行的方法、new出来的对象)

    • 异步任务

    耗费时间较长或者性能较差的,浏览器执行到这些的时候会将其丢到异步任务队列中,不会立即执行

    同时异步任务分为宏任务(如setTimeout、setInterval、postMessage、setImmediate等)和微任务(Promise、process.nextTick等),浏览器执行这两种任务的优先级不同;会优先执行微任务队列的代码,微任务队列清空之后再执行宏任务的队列,这样循环往复;

    JS自上向下进行代码的编译执行,遇到同步代码压入JS执行栈执行后出栈,遇到异步代码放入任务队列,当JS执行栈清空,去执行异步队列中的回调函数,先去执行微任务队列,当微任务队列清空后,去检测执行宏任务队列中的回调函数,直至所有栈和队列清空

    整体流程如下图所示:


    接下来让我们看看nextTick的源码~

    vue将nextTick的源码放在了vue/core/util/next-tick.js中。如下图所示:

    我们把这个文件拆成三个部分来看:

    1.nextTick定义函数

    我们将nextTick函数单独拿出来,callbacks是一个回调队列,其实调用nextTick就是往这个数组里面传执行任务,callbacks新增回调函数之后执行timerFunc函数,pending是用来限制同一个事件循环内只能执行一次;

    const callbacks = [] // 回调队列
    let pending = false // 
    export function nextTick (cb?: Function, ctx?: Object) {
     let _resolve
     callbacks.push(() => {
      // cb 回调函数会经统一处理压入 callbacks 数组
         if (cb) {
             try {
                 cb.call(ctx)
             } catch (e) {
                 handleError(e, ctx, 'nextTick')
             }
         } else if (_resolve) {
             _resolve(ctx)
            }
         })
      // 执行异步延迟函数 timerFunc
         if (!pending) {
         pending = true
         timerFunc()
     }
     // $flow-disable-line
     // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
    if (!cb && typeof Promise !== 'undefined') {
         return new Promise(resolve => {
         _resolve = resolve
         })
     }
    }

    2.timerFunc函数
    做了四个判断,先后尝试当前环境是否能够使用原生的Promise.then、MutationObserver和setImmediate,不断的降级处理,如果以上三个都不支持,则最后就会直接使用setTimeOut,主要操作就是将flushCallbacks中的函数放入微任务或者宏任务,等待下一个事件循环开始执行;宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务;

    export let isUsingMicroTask = false
    let timerFunc
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
     //判断1:是否原生支持Promise
     const p = Promise.resolve()
     timerFunc = () => {
     p.then(flushCallbacks)
      if (isIOS) setTimeout(noop)
     }
     isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
     isNative(MutationObserver) ||
     MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
    //判断2:是否原生支持MutationObserver 
     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 = () => {
      //判断3:是否原生支持setImmediate
     setImmediate(flushCallbacks)
     }
    } else {
     // Fallback to setTimeout.
     timerFunc = () => {
      //判断4:上面都不行,直接用setTimeout
     setTimeout(flushCallbacks, 0)
     }
    }

    3.flushCallbacks函数

    该函数只有几行,也很好理解,将pending置为false,同时将callbacks数组复制一份之后再将callbacks置为空,接下来将callbacks数组的每个函数依次进行执行,它的作用就是用来执行callbacks中的回调函数;

    function flushCallbacks () {
     pending = false
     const copies = callbacks.slice(0)
     callbacks.length = 0
     for (let i = 0; i < copies.length; i++) {
         copies[i]()
     }
    }
    值得注意的是,$nextTick 并不是一个真正意义上的微任务microtask,而是利用了事件循环机制来实现异步更新。因此,它的执行时机相对于微任务可能会有所延迟,但仍能保证在 DOM 更新后尽快执行回调函数。

    总的来说,nextTick就是

    1.将传入的回调函数放入callbacks数组等待执行,定义pending判断锁保证一个事件循环中只能调用一次timerFunc函数;

    2.根据环境判断使用异步方式,调用timerFunc函数调用flushCallbacks函数依次执行callbacks中的回调函数;

    3.个人小结

    nextTick可避免数据更新后导致DOM的数据不一致的问题,提供了更稳定的异步更新机制,解决了created钩子函数DOM未渲染会造成的异步数据渲染问题,但如果过多的使用nextTick会导致事件循环中任务数量和回调函数增多,有可能出现可怕的回调地狱,导致性能下降,同时过度依赖nextTick也会降低代码的可读性,所以大家还是"按需加载"的好~


    文章数
    2
    阅读量
    365