Vue中的$nextTick()

  • nextTick
    官方定义:在下次DOM更新循环结束之后执行回调,在修改数据之后立即使用这个方法,获取更新后的DOM。
    Vue在更新DOM时是异步执行的。当数据发生变化。Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,在统一进行更新。
    举个栗子:
    HTML结构

    {{message}}

    构建一个vue实例
    const vm = new Vue({
    el:’#app’,
    data:{
    message:’旧值’
    }
    })

    修改message
    this.message = ‘新值’

    这时想获取页面的DOM节点,发现获取的是旧值
    console.log(vm.$el.textContent) //旧值

    这是因为message数据在发生变化的时候,vue并不会立刻去更新DOM,而是将修改的操作放在一个异步操作队列中。如果一直修改数据,异步操作队列还会进行去重。
    等到同一事件循环中的所有数据变化完成之后,会将队列中的事件进行处理,进行DOM的更新
    为什么要有nextTick
    举个栗子
    {{num}}
    for(let i = 0;i < 100000;i++){ num = i } 如果没有nextTick更新机制,那么num每次更新值都会触发视图更新,有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略。

  • 使用场景
    如果想要在修改数据后立即得到更新后的DOM结构,可以使用Vue.nextTick()
    第一个参数为:回调函数(可以获取最近的DOM结构)
    第二个参数为:执行函数上下文
    //修改数据
    vm.message = ‘修改后的值’
    //DOM还没有更新
    console.log(vm.$el.textContent) //原始的值
    Vue.nextTick(function(){
    //DOM更新了
    console.log(vm.$el.textContent) //修改后的值
    })

    组件内使用vm.$nextTick()实例方法只需要通过this.$nextTick(),并且回调函数中的this将自动绑定到当前的Vue实例上
    this.message = ‘修改后的值’
    console.log(this.$el.textContent) // => ‘原始的值’
    this.$nextTick(function(){
    console.log(this.$el.textContent) // => ‘修改后的值’
    })

    $nextTick()会返回一个Promise对象,可以使用async/await完成相同作用的事情
    this.message = ‘修改后的值’
    console.log(this.$el.textContent) // => ‘原始的值’
    await this.$nextTick()
    console.log(this.$el.textContent) // => ‘修改后的值’

  • 实现原理

    源码位置:/src/core/util/next-tick.js

    callbacks也就是异步操作队列
    callbacks新增回调函数后又执行了timeFunc函数,pending是用来标志同一个时间只能执行一次
    export function nextTick(cb ?: Function,ctx ?: Object){
    let _resolve
    //cb回调函数会经统一处理压入 callbacks 数组
    callbacks.push(() => {
    if(cb){
    //给 cb 回调函数执行加上了 try-catch 错误处理
    try{
    cb.call(ctx)
    }catch(e){
    handleError(e,ctx,’nextTick’)
    }
    }else if(_resolve){
    _resolve(ctx)
    }
    })

    //执行异步延迟函数 timerFunc
    if(!pending){
    pending = true
    timerFunc()
    }

    //当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
    if(!cb && typeof Promise !== ‘undefined’){
    return new Promise(resolve => {
    _resolve = resolve
    })
    }
    }

    timerFunc函数定义,这是根据当前环境确定的,分别有:Promise.then、MutationObserver、setImmediate、setTimeout
    通过上面任意一种方法进行降级操作
    export let isUsingMicroTask = false
    if(typeof Promise !== ‘undefined’ && isNative(Promise)){
    //判断:是否原生支持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’])){
    //判断:是否原生支持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)){
    //判断:是否原生支持setImmediate
    timerFunc = () => {
    setImmediate(flushCallbacks)
    }
    }else{
    //判断:若都不行,直接使用setTimeout
    timerFunc = () => {
    setTimeout(flushCallbacks,0)
    }
    }

    无论是微任务还是宏任务,都会放到flushCallbacks使用
    这里将callbacks里面的函数复制一份,同时callbacks置空
    依次执行callbacks里面的函数
    function flushCallbasks(){
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for(let i = 0;i < copies.length;i++){ copies[i]() } }

  • 小结

    把回调函数放入callbacks等待执行
    将执行函数放到微任务或者宏任务中
    事件循环到了微任务或者宏任务,执行函数依次callbacks中的回调

  • 把回调函数放入callbacks等待执行
  • 将执行函数放到微任务或者宏任务中
  • 事件循环到了微任务或者宏任务,执行函数依次callbacks中的回调

参考函数:

https://vue3js.cn/interview/vue/nexttick.html

————————
  • nextTick
    官方定义:在下次DOM更新循环结束之后执行回调,在修改数据之后立即使用这个方法,获取更新后的DOM。
    Vue在更新DOM时是异步执行的。当数据发生变化。Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,在统一进行更新。
    举个栗子:
    HTML结构

    {{message}}

    构建一个vue实例
    const vm = new Vue({
    el:’#app’,
    data:{
    message:’旧值’
    }
    })

    修改message
    this.message = ‘新值’

    这时想获取页面的DOM节点,发现获取的是旧值
    console.log(vm.$el.textContent) //旧值

    这是因为message数据在发生变化的时候,vue并不会立刻去更新DOM,而是将修改的操作放在一个异步操作队列中。如果一直修改数据,异步操作队列还会进行去重。
    等到同一事件循环中的所有数据变化完成之后,会将队列中的事件进行处理,进行DOM的更新
    为什么要有nextTick
    举个栗子
    {{num}}
    for(let i = 0;i < 100000;i++){ num = i } 如果没有nextTick更新机制,那么num每次更新值都会触发视图更新,有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略。

  • 使用场景
    如果想要在修改数据后立即得到更新后的DOM结构,可以使用Vue.nextTick()
    第一个参数为:回调函数(可以获取最近的DOM结构)
    第二个参数为:执行函数上下文
    //修改数据
    vm.message = ‘修改后的值’
    //DOM还没有更新
    console.log(vm.$el.textContent) //原始的值
    Vue.nextTick(function(){
    //DOM更新了
    console.log(vm.$el.textContent) //修改后的值
    })

    组件内使用vm.$nextTick()实例方法只需要通过this.$nextTick(),并且回调函数中的this将自动绑定到当前的Vue实例上
    this.message = ‘修改后的值’
    console.log(this.$el.textContent) // => ‘原始的值’
    this.$nextTick(function(){
    console.log(this.$el.textContent) // => ‘修改后的值’
    })

    $nextTick()会返回一个Promise对象,可以使用async/await完成相同作用的事情
    this.message = ‘修改后的值’
    console.log(this.$el.textContent) // => ‘原始的值’
    await this.$nextTick()
    console.log(this.$el.textContent) // => ‘修改后的值’

  • 实现原理

    源码位置:/src/core/util/next-tick.js

    callbacks也就是异步操作队列
    callbacks新增回调函数后又执行了timeFunc函数,pending是用来标志同一个时间只能执行一次
    export function nextTick(cb ?: Function,ctx ?: Object){
    let _resolve
    //cb回调函数会经统一处理压入 callbacks 数组
    callbacks.push(() => {
    if(cb){
    //给 cb 回调函数执行加上了 try-catch 错误处理
    try{
    cb.call(ctx)
    }catch(e){
    handleError(e,ctx,’nextTick’)
    }
    }else if(_resolve){
    _resolve(ctx)
    }
    })

    //执行异步延迟函数 timerFunc
    if(!pending){
    pending = true
    timerFunc()
    }

    //当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
    if(!cb && typeof Promise !== ‘undefined’){
    return new Promise(resolve => {
    _resolve = resolve
    })
    }
    }

    timerFunc函数定义,这是根据当前环境确定的,分别有:Promise.then、MutationObserver、setImmediate、setTimeout
    通过上面任意一种方法进行降级操作
    export let isUsingMicroTask = false
    if(typeof Promise !== ‘undefined’ && isNative(Promise)){
    //判断:是否原生支持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’])){
    //判断:是否原生支持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)){
    //判断:是否原生支持setImmediate
    timerFunc = () => {
    setImmediate(flushCallbacks)
    }
    }else{
    //判断:若都不行,直接使用setTimeout
    timerFunc = () => {
    setTimeout(flushCallbacks,0)
    }
    }

    无论是微任务还是宏任务,都会放到flushCallbacks使用
    这里将callbacks里面的函数复制一份,同时callbacks置空
    依次执行callbacks里面的函数
    function flushCallbasks(){
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for(let i = 0;i < copies.length;i++){ copies[i]() } }

  • 小结

    把回调函数放入callbacks等待执行
    将执行函数放到微任务或者宏任务中
    事件循环到了微任务或者宏任务,执行函数依次callbacks中的回调

  • 把回调函数放入callbacks等待执行
  • 将执行函数放到微任务或者宏任务中
  • 事件循环到了微任务或者宏任务,执行函数依次callbacks中的回调

参考函数:

https://vue3js.cn/interview/vue/nexttick.html