Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【源码】nextTick 源码解析 #40

Open
YIngChenIt opened this issue Jul 3, 2020 · 0 comments
Open

【源码】nextTick 源码解析 #40

YIngChenIt opened this issue Jul 3, 2020 · 0 comments

Comments

@YIngChenIt
Copy link
Owner

【源码】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中执行

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant