Vue.nextTick实现及原理探究

Vue.nextTick实现及原理探究

一. nextTick() 使用场景/语法/分类

  1. 使用场景

    当更新状态(数据)后,需要对新DOM做一些操作,但这时我们获取不到更新后的DOM,因为还没有重新渲染。nextTick接收一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行。

  2. 语法

    //实例方法
    new Vue({
        data:{
            message:'oldValue'
        },
        methods:{
            fn:function(){
                // 修改数据
                this.message = 'newValue';
                //DOM 还未更新
                this.$nextTick(()=>{
                    // DOM更新了
                    doSomething...
                })
           }
        }
    })
    
    // 全局方法
    Vue.nextTick(()=>{
        //DOM更新
        doSomething...
    })
    // 作为一个 Promise 使用
    Vue.nextTick().then(()=>{
        //DOM更新
        doSomething...
    })
    
  3. 分类

    3.1 vm.$nextTick(fn):回调的this自动绑定到调用它的实例上

    3.2 Vue.nextTick(fn):全局方法

    vm.$nextTick(fn)Vue.nextTick(fn)实质是一样的,所以nextTick的具体实现并不是在Vue原型上的$nextTick方法中,而是抽象成了nextTick方法供两个方法共同调用。

二. nextTick()实现原理

这里第一步我们需要思考的是:nextTick是将回调延迟到下次DOM更新之后执行,那么下次DOM更新具体是什么时候呢?如何得知?

我们知道,Vue的响应式原理是使用订阅-发布者模式,即通过收集某状态的使用者,在状态改变时,再遍历收集告知变化,然后触发虚拟DOM的渲染流程。这里引出一个概念:触发渲染是一个异步操作。每当需要重新渲染时,会将该任务推送至一个异步任务队列中,然后在下一次事件循环中执行更新DOM的操作。

第二步我们需要思考的是:何为任务队列及事件循环机制?

JavaScript是一门单线程非阻塞的脚本语言

  • JavaScript代码在执行的任何时候都只有一个主线程来处理所有任务
  • 非阻塞是指当代码需要执行异步任务时,主线程会挂起(pending)这个任务,当同步任务执行完毕后,主线程再去执行挂起的任务。总结来非阻塞就是我在一刻也不停的做事儿。

例子:

console.log('time1:',new Date().getTime());
setTimeout(()=>{  // setTimeout相当于一个异步操作
    console.log('time2:',new Date().getTime())
},1000)
console.log('time3',new Date().getTime())
// 输出: time1 time3 time2
任务队列

所有任务分为同步任务和异步任务

  1. 同步任务:可以立即执行的任务,直接进入js主线程中执行
  2. 异步任务:如ajax网络请求,setTimeout定时器等,异步任务需要通过任务队列(Event Queue)的机制来协调

同步任务与异步任务会进入不同的执行环境,同步的进入主线程,即执行栈,异步则进入任务队列。当主线程内的任务执行完毕,会去 Event Queue读取对应任务,然后推入主线程执行。

异步任务

异步任务分为宏任务和微任务

宏任务(macrotask),主要包括:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • MessageChannel
  • requestAnimationFrame
  • I/O
  • UI交互事件

微任务(microtask),主要包括:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick

更新DOM的回调和vm.$nextTick注册的回调都是向微任务队列中添加任务

修改数据会默认将更新DOM的回调添加到微任务队列中

执行优先级:当执行栈中的任务执行完毕后,会去检查微任务队列中是否有事件存在,如果存在,则会依次执行微任务队列中事件对应的回调,直到为空。然后从宏任务队列中取出一个事件,把对应的回调推入当前执行栈。当执行栈中所有任务都执行完毕后,又去检查微任务队列是否有事件存在。

例子:

setTimeout(()=>{
    console.log(1)
},0)
console.log(2)
new Promise(resolve=>{
    console.log(3)
    resolve()
}).then(()=>{
    console.log(4)
}).then(()=>{
    conlole.log(5)
})
// 输出:2 3 4 5 1

分析:

  1. 浏览器执行js进入第一个宏任务,遇到setTimeout,分发到宏任务Event Queue
  2. 遇到console.log(),直接打印
  3. 遇到Promise,new Promise直接打印
  4. 遇到then(),分发到微任务Event Queue
  5. 第一轮宏任务执行结束,检查是否有微任务执行,有则打印
  6. 第一轮微任务执行结束,开启第二轮宏任务,打印

注意:宏任务是一个一个取出执行,而微任务是一个队列一个队列执行

事件循环机制(event loop

当执行栈中所有任务执行完毕,会去检查微任务队列是否有事件存在,有则执行,执行完毕再去宏任务队列取一个事件加入到当前执行栈,执行完毕再去检查微任务队列,上述过程的不断重复即事件循环

在这里插入图片描述

看到这里,可以解答上述遗留问题了,下次DOM更新 即是:下次微任务执行时更新DOM。

最后我们来讲nextTick方法的具体实现

const callbacks = [] // 定义存放回调任务的列表
let pending = false  // 状态值,控制是否需要向任务队列中添加任务

function flushCallbacks (){
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0 // 清空任务队列
  for(let i = 0; i < copies.length; i++){
      copies[i]()   // 遍历任务队列并执行
  }
}

let microTimerFunc
const p = Promise.resolve()
microTimerFunc = () => {
  p.then(flushCallbacks)  // microTimerFunc函数封装Promise.then()将任务添加到微任务中
}

export function nextTick (cb, ctx){
  callbacks.push(()=>{
      if(cb){
          cb.call(ctx)
      }
  })
  if(!pending) {  // 判断是否是第一次添加该任务
      pending = true   // 每当向队列中插入任务,更改状态值
      microTimerFunc()
  }
}
  • 在一轮事件循环中,nextTick只会向任务队列添加一个任务,即flushCallbacks只会执行一次
  • microTimerFunc函数的作用是使用Promise.thenflushCallbacks添加到微任务中

流程图如下:
在这里插入图片描述

扩展

前面提到 microTimerFunc函数封装Promise.thenflushCallbacks添加到微任务中,但Promise是ES6新增的东西,存在兼容问题。这时为了解决一些特殊情况,Vue提出了降级策略

降级策略:即在特殊情况强制使用宏任务的方法

// 首先判断浏览器是否支持 Promise
if(typeof Promise !== 'undefined' && isNative(Promise)){
    const p = Promise.resolve()
    microTimerFunc = ()=>{
        p.then(flushCallbacks)
    }
}else{
    microTimerFunc = macroTimerFunc
}

具体实现:

// 新增代码
let useMacroTask = false
export function withMacroTask (fn) {
    return fn._withTask || (fn._withTask = function () {
        useMacroTask = true
        const res = fn.apply(null, arguments)
        useMacroTask = false
        return res
    })
}
export function nextTick (cb,ctx){
    callbacks.push(()=>{
        if(cb){
            cb.call(ctx)
        }
    })
    if(!pending){
        pending = true
        if(useMacroTask){
            macroTimerFunc()
        } else {
            microTimerFunc()
        }
    }
}
  • withMacroTask包裹的函数所使用的nextTick方法都会将回调添加到宏任务队列中,其中包括状态被修改后触发的更新DOM的回调和用户自己使用nextTick注册的回调等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值