YIngChenIt/Blogs

【源码】nextTick 源码解析

Opened this issue · 0 comments

【源码】nextTick 源码解析

前言

我们知道Vue中数据更新是异步的,如果我们在修改数据之后获取DOM中对应的数据,是无法获取到最新数据来进行相关操作,但是我们可以通过nextTick这个机制来实现相关需求(本文不会涉及nextTick的基础用法)

nextTick源码解析

我们先来看下nextTick在源码中的定义(代码有省略)

// vue/src/core/util/next-tick.js 
const callbacks = []
let pending = false

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => { // 将传入的回调保存到数组callbacks中
    cb.call(ctx)
  })

  if (!pending) {
    pending = true
    timerFunc()
  }

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

我们可以发现我们使用this.$nextTick的时候传入的回调函数会保存在一个数组callbacks中,然后通过pending控制timerFunc函数在某个时机执行

那我们接下来看下timerFunc函数做了什么?

// vue/src/core/util/next-tick.js 

function flushCallbacks () { // 将callbacks中的全部回调函数拷贝一份,然后依次执行
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

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) ||
  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)
  }
}

我们可以发现代码中生成了timerFunc函数,然后把回调作为microTask或macroTask参与到事件循环中来, 并且依次为promise -> MutationObserver -> setImmediate -> setTimeout 这样的顺序进行降级,并且通过flushCallbacks方法将callbacks中的全部回调拷贝一份,然后依次执行

nextTick流程梳理

虽然是读懂了nextTick的源码,但是我们对nextTick如果实现对应功能,为什么可以在数据异步更新的机制下获取到最新DOM还是不了解,我们现在来梳理一下这个流程

首先数据更新离不开Watcher, 我们看下Watcher中的这段代码

update () {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        /*同步则执行run直接渲染视图*/
        this.run()
    } else {
        /*异步推送到观察者队列中,下一个tick时调用。*/
        queueWatcher(this)
    }
}

因为sync属性默认为false, 所以数据更新是异步的,我们继续看下queueWatcher方法

 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
    /*获取watcher的id*/
    const id = watcher.id
    /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            /*如果没有flush掉,直接push到队列中即可*/
            queue.push(watcher)
        } else {
        ...
        }
        // queue the flush
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}

我们只需要关心最后一段代码nextTick(flushSchedulerQueue)就好了

flushSchedulerQueue函数的作用主要是执行视图更新的操作,它会把queue中所有的watcher取出来并执行相应的视图更新。

然后通过我们对nextTick源码的理解,flushSchedulerQueue方法会push到一个数组callbacks中,如

this.name = 123
this.$nextTick(cb)

此时的数组callbacks中如下, flushSchedulerQueue为第一位

[flushSchedulerQueue, cb]

然后将数组callbacks中的回调函数按照promise -> MutationObserver -> setImmediate -> setTimeout的顺序包装成微任务或者宏任务,最后赋值给timerFunc,最后执行

这里就涉及到了事件环相关的知识点了

同步任务执行完之后会执行异步任务,在异步任务中先执行一个宏任务,然后清空微任务队列,然后进行GUI渲染视图

也就是我们通过将timerFunc包装成异步任务,然后我们的nexttick中传入的回调会在flushSchedulerQueue执行之后执行,所以我们在回调中是可以获取到最新的DOM的,不同在于如果timerFunc如果是微任务的话,浏览器把DOM更新的操作放在Tick执行microTask的阶段来完成,相比使用宏任务生成的一个macroTask会少一次UI的渲染。

总结

nextTick源码做了什么

nextTick函数其实做了两件事情,一是生成一个timerFunc,把回调作为microTask或macroTask参与到事件循环中来。二是把回调函数放入一个callbacks队列,等待适当的时机执行。(这个时机和timerFunc不同的实现有关)

为什么nextTick可以获取到最新DOM

因为在数据更新的时候会调用nexttick方法,将更新视图的flushSchedulerQueue方法作为第一位放入callbacks中,依次遍历callbacks队列的时候flushSchedulerQueue先执行,所以后序的回调中因为视图已经更新了,所以可以获取最新的DOM

为什么说DOM更新是异步的

因为Vue源码中将视图更新的方法flushSchedulerQueue通过nexttick来调用,所以最后会被包装成宏任务或者微任务,利用事件环的概念,如果宏任务的话会在下一个tick中执行,如果是微任务的话会在当前tick中执行