blog icon indicating copy to clipboard operation
blog copied to clipboard

Vue的nextTick的是怎么实现的?

Open rudyxu1102 opened this issue 4 years ago • 0 comments

Vue.nextTick的作用

在下次DOM更新结束之后执行延迟回调。通常在修改数据之后调用这个方法,便于获取更新之后的DOM。

下面题目的输出是什么

最近在看nextTick相关的文章的时候,看到一些很有意思的题目,可以结合下面的题目了解nextTick源码和watcher源码。

题目一

<template>
  <div>
    <div ref="test">{{test}}</div>
    <button @click="handleClick">click me</button>
  </div>
</template>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText)
        }
    }
}

题目二

<div id="example">
    <div ref="test">{{test}}</div>
    <button @click="handleClick">click me</button>
</div>
var vm = new Vue({
    el: '#example',
    data: {
        test: 'begin',
    },
    methods: {
        handleClick() {
            this.test = 'end';
            console.log('1')
            Promise.resolve().then(function promiseFn () { 
                console.log('promise!')
            })
            this.$nextTick(function nextTickFn () {
                console.log('2')
            })
        }
    }
})

题目三

<div id="example">
    <div ref="test">{{test}}</div>
    <button @click="handleClick">click me</button>
</div>
var vm = new Vue({
    el: '#example',
    data: {
        test: 'begin',
    },
    methods: {
        handleClick() {
            // this.test = 'end';  // 相对于题目二删除了这行代码
            console.log('1')
            Promise.resolve().then(function promiseFn () { //microTask
                console.log('promise!')
            })
            this.$nextTick(function nextTickFn () {
                console.log('2')
            })
        }
    }
})

可以进入blog/vue-demo/nextTick目录运行上面的示例,

下面是Vue2.6在chrome的输出结果:

# 题目一
begin

# 题目二
1 
2 
promise 

# 题目三
1 
promise
2

Vue.nextTick的原理

Vue.nextTick源码地址

看了nextTick源码可以发现,nextTick会把回调函数放入微任务队列或者宏任务队列,如果浏览器支持promise或者MutationObserver的话,就把回调函数放入微任务队列,否则就用setImmediate或者setTimeout放入宏任务队列。

在事件循环中,每进行一次事件循环操作称为tick,nextTick的含义是希望在下一次事件循环执行回调函数。

从nextTick的源码来看,如果放入微任务队列的话,是在本次事件循环中的微任务队列尾部插入回调函数;如果放入宏任务队列的话,则是在下一次的事件循环中去执行。

结合nextTick源码看下面的示例

this.$nextTick(function test01 () {
    console.log('test01')
})
// 上面的代码做了什么?
// 1. 把函数test01做了一层异常捕获,再放入callbacks数组。现在callbacks: [ test01 ]
// 2. pending设置为true
// 3. 执行timerFunc,将flushCallbacks放入微任务队列或者宏任务队列,flushCallbacks是一个执行callbacks数组的函数。

this.$nextTick(function test02 () {
    console.log('test02')
})
// 上面的代码做了什么?
// 1. 把函数test01做了一层异常捕获,再放入callbacks数组。现在callbacks: [ test01, test02 ]
// 2. 因为pending已经为true,就不会执行timerFunc

上面两个nextTick的作用就是把 test01和test02放入callbacks数组,再把flushCallbacks放入微任务队列或者宏任务队列,在事件循环中执行flushCallbacks方法,就是执行callbacks里面的两个回调数组test01和test02,最后通过 callbacks.length = 0将callbacks置为空数组。

异步更新dom是如何实现的

Vue的观察者watcher源码地址

从题目一的输出,我们也可以看到Vue的dom更新默认是异步的

// 题目一
this.test = 'end'
console.log(this.$refs.test.innerText)

更新test的值,会触发test的setter,在setter里面会触发所有依赖test的观察者,触发每个watcher的update方法,然后在update方法里面执行queueWatcher

  // https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L164 

  update () {
    if (this.lazy) { // lazy是跟computed相关的
      this.dirty = true
    } else if (this.sync) { // 当数据变化时,是否同步执行回调。默认是异步
      this.run()
    } else {
      queueWatcher(this)
    }
  }

下面是queueWatcher的主要源码

// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L164
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) { // 是否当前正在执行queue队列里面的函数
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) { // 当前quene是否为空
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher方法会将watcher放到一个queue中,如果queue中已经有相同id的watcher则不会放入queue,然后将flushSchedulerQueue方法通过nextTick放入callbacks数组中。

flushSchedulerQueue会将queue中的watcher根据id升序排序,id较小的先执行,确保①从父组件到子组件的更新顺序,②确保render watcher后执行,③确保在父组件的watcher中销毁子组件,子组件的watcher会被跳过,因为子组件watcher的active会被置为false。

执行flushSchedulerQueue会执行所有的watcher的run方法,然后触发updated钩子和activated钩子,最后通过执行resetSchedulerState,将queue置为空数组。

下面是flushSchedulerQueue的源码

// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L71
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
   
  }
  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

}

题目解析

在了解watcher源码和nextTick源码之后,可以了解到watcher内部也是使用了nextTick方法,nextTick内部维护了一个callbacks队列,使用flushCallbacks方法执行所有callbacks中的回调函数。watcher执行内部也维护了一个queue队列,使用flushSchedulerQueue执行所有watcher的run方法。

掌握了这些知识点之后,我们再来回顾一下最初的题目。

题目一

queue:[test的渲染watcher]
callbacks: [flushSchedulerQueue方法]

微任务队列或者宏任务队列:[flushCallbacks]

题目二

queue:[test的渲染watcher]
callbacks: [flushSchedulerQueue,nextTickFn]

微任务队列或者宏任务队列:[flushCallbacks, promiseFn]

题目三

queue:[]
callbacks: [nextTickFn]

微任务队列或者宏任务队列:[ promiseFn,flushCallbacks]

参考链接

rudyxu1102 avatar Oct 31 '20 08:10 rudyxu1102