Vue source code learning - asynchronous update queue and nextTick principle

foreword

In the process of using Vue, basically most of watcherthe updates need to be 异步更新processed. And nextTickis the core of the asynchronous update.

Official definition of it:

Delayed callback to execute after the next DOM update cycle ends. Use this method immediately after modifying the data to get the updated DOM.

1. Vue asynchronously updates the queue

Vue can do it 数据驱动视图更新, let's simply write a case to achieve it:

<template>
  <h1 style="text-align:center" @click="handleCount">{
   
   { value }}</h1>
</template>

<script>
export default {
      
      
  data () {
      
      
    return {
      
      
      value: 0
    }
  },
  methods: {
      
      
    handleCount () {
      
      
      for (let i = 0; i <= 10; i++) {
      
      
        this.value = i
        console.log(this.value)
      }
    }
  }
}
</script>

Vue updates dom asynchronously

valueWhen we fire this event, there must be some changes in the view .
Here you can think about how Vue manages this change process? For example, in the above case, valueit was looped 10 times, so will Vue render the dom view 10 times? Obviously not, after all, the performance cost is too high. Actually we only need valuethe last assignment.

In fact, Vue updates the view asynchronously, that is to say, after handleCount()the event is executed, it is found that it only needs to be updated value, and then the data and Dom are updated at one time to avoid invalid updates.

In short, Vue's data update and DOM update are asynchronous. Vue will add data changes to the queue, perform batch updates in the next event loop, and then apply the changes to the actual DOM elements asynchronously to keep the view and data synchronization.

The official Vue documentation also confirms our thoughts, as follows:

异步Vue is executed when updating the DOM . As long as it listens 数据变化, Vue will open a queue and buffer all data changes that occur in the same event loop. If the same watcher is triggered multiple times, it will only be pushed into the queue once. This deduplication while buffering is very important to avoid unnecessary calculations and DOM manipulations. Then, on the next event loop "tick", Vue flushes the queue and performs the actual (deduplicated) work.

For details, see: Vue Official Documentation - Asynchronous Update Queue

Two, nextTick usage

Look at the example, for example, when the DOM content changes, we need to get the latest element height.

<template>
  <div>{
   
   { name }}</div>
</template>

<script>
export default {
      
      
  data () {
      
      
    return {
      
      
      name: ''
    }
  },
  methods: {
      
      },
  mounted () {
      
      
    console.log(this.$el.clientHeight)
    this.name = '铁锤妹妹'
    console.log(this.name, 'name')
    console.log(this.$el.clientHeight)
    this.$nextTick(() => {
      
      
      console.log(this.$el.clientHeight)
    })
  }
}
</script>

insert image description here

It can be seen from the printing results that although the name data has been updated, the height of the first two elements is 0, and the updated Dom value can only be obtained in nextTick. What is the specific reason? Let's analyze its principle below.

This example can also refer to learning: Watch monitoring and $nextTick are used in combination to process the operation method after data rendering is completed

3. Principle analysis

When executing this.name = '铁锤妹妹', an update will be triggered Watcher, and the watcher will put itself into a queue.

// src/core/observer/watcher.ts
update () {
    
    
    if (this.lazy) {
    
    
        // 如果是计算属性
        this.dirty = true
    } else if (this.sync) {
    
    
        // 如果要同步更新
        this.run()
    } else {
    
    
        // 将 Watcher 对象添加到调度器队列中,以便在适当的时机执行其更新操作。
        queueWatcher(this)
    }
}

The reason for using the queue is that, for example, multiple data changes, if the view is updated multiple times directly, the performance will be reduced, so an asynchronous update queue is made for the view update to avoid unnecessary calculations and DOM operations. In the next round of event loop, the queue is refreshed and the deduplicated work (the callback function of nextTick) is executed, the component is re-rendered, and the view is updated.

Then call nextTick(), the source code of responsive distribution update is as follows:

// src/core/observer/scheduler.ts

export function queueWatcher(watcher: Watcher) {
    
    
    // ...
    
   // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
    nextTick(flushSchedulerQueue)
}

function flushSchedulerQueue () {
    
    
    queue.sort(sortCompareFn)
    for (index = 0; index < queue.length; index++) {
    
    
        watcher = queue[index]
        watcher.run()
        // ...省略细节代码
    }
}

The parameter method here flushSchedulerQueuewill be put into the event loop. After the main thread task is executed, this function will be executed, and the method corresponding to the watcher will be executed for the watcher queue 排序, , and then render and update the view.遍历run()

That is to say this.name = '铁锤妹妹', when , the task queue is simply understood as this [flushSchedulerQueue].

The next line console.log(this.name, 'name')checks whether the name data is updated.

Then the next line console.log(this.$el.clientHeight), because the update view task flushSchedulerQueuehas not been executed in the task queue, so the updated view cannot be obtained.

Then this.$nextTick(fn)when executing, add an asynchronous task, and the task queue is simply understood like this [flushSchedulerQueue, fn].

Then 同步任务all are executed, and then the tasks in the task queue are executed in order 异步任务. The first task execution will update the view, and the updated view will naturally be obtained later.

Fourth, nextTick source code analysis

1) Environmental judgment

Mainly judge which macro task or micro task to use, because the macro task takes more time than the micro task , so it is used first 微任务, and the judgment order is as follows:
Promise =》 MutationObserver =》 setImmediate =》 setTimeout

// src/core/util/next-tick.ts

export let isUsingMicroTask = false  // 是否启用微任务开关

const callbacks: Array<Function> = [] //回调队列
let pending = false  // 异步控制开关,标记是否正在执行回调函数

// 该方法负责执行队列中的全部回调
function flushCallbacks() {
    
    
  // 重置异步开关
  pending = false
  // 防止nextTick里有nextTick出现的问题
  // 所以执行之前先备份并清空回调队列
  const copies = callbacks.slice(0)
  callbacks.length = 0
   // 执行任务队列
  for (let i = 0; i < copies.length; i++) {
    
    
    copies[i]()
  }
}

// timerFunc就是nextTick传进来的回调等... 细节不展开
let timerFunc
// 判断当前环境是否支持原生 Promise
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) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
    
    
  // 当原生 Promise 不可用时,timerFunc 使用原生 MutationObserver
  // MutationObserver不要在意它的功能,其实就是个可以达到微任务效果的备胎
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    
    
    characterData: true
  })
  timerFunc = () => {
    
    
  // 使用 MutationObserver
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    
    
  // 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
  timerFunc = () => {
    
    
    setImmediate(flushCallbacks)
  }
} else {
    
    
  // 最后的倔强,timerFunc 使用 setTimeout
  timerFunc = () => {
    
    
    setTimeout(flushCallbacks, 0)
  }
}

Then enter the core nextTick.

2)nextTick()

There is not much code here, the main logic is:

  • Put the incoming callback function into the callback queue callbacks.
  • Executing the saved asynchronous task timeFuncwill traverse callbacksand execute the corresponding callback function.
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
    
    
  let _resolve
  // 把回调函数放入回调队列
  callbacks.push(() => {
    
    
    if (cb) {
    
    
      try {
    
    
        cb.call(ctx)
      } catch (e: any) {
    
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    
    // 如果异步开关是开的,就关上,表示正在执行回调函数,然后执行回调函数
    pending = true
    timerFunc()
  }
  // 如果没有提供回调,并且支持 Promise,就返回一个 Promise
  if (!cb && typeof Promise !== 'undefined') {
    
    
    return new Promise(resolve => {
    
    
      _resolve = resolve
    })
  }
}

You can see that there is one returned at the end Promise, which can be used when we do not pass parameters, as follows

this.$nextTick().then(()=>{
    
     ... })

5. Supplement

  • In the vue life cycle, if created()the DOM operation is performed in the hook, it must also be placed nextTick()in the callback function of .
  • Because in created()the hook function, the page is DOMstill 未渲染, and there is no way to operate the DOM at this time, so if you want to operate the DOM at this time, you must put the operation code in the nextTick()callback function of

This is the end of this article, I hope it will be helpful to everyone.

Guess you like

Origin blog.csdn.net/weixin_45811256/article/details/131814963