Vue中的$nextTick原理

952 阅读1分钟

1. 前言

本文主要是梳理一下vue中异步更新的机制,其中会涉及到JS的运行机制,如果不太了解的,可以先看一下 JS事件循环

本篇文章主要从两个问题进行梳理

  1. Vue的异步更新机制
  2. $nextTick的实现原理

2. 基本使用

<div id="app">{{ msg }}</div>

new Vue({
  el: '#app',
  data(){
    return {
      msg: 'hello vue'
    }
  },
  mounted() {
    this.msg = '1234'
    console.log(this.msg) // 1234
    // 虽然msg更新了,但是直接这样获取到的还是hello vue
    // console.log(document.getElementById('app').innerText) // hello vue
    
    // 如果是使用了$nextTick,获取到的是最新的值
    this.$nextTick(() => {
      console.log(document.getElementById('app').innerText) // 1234
    })
  }

vue实现响应式并不是说数据一改变,DOM就立即变化。Vue在修改数据后,视图并不是立即更新,而是等同一事件循环中的所有数据变化完成后,再统一进行视图的更新。也就是说vue是异步渲染的

  1. this.msg = '1234'。因为在初始化的时候已经做了数据劫持,当对数据进行更新操作时,就会触发 setter 的拦截(vue中会检测新值和旧值是否相等,如果相等就不更新),如果不相等,就会触发更新。由 dep 通知 watcher 进行更新,也就是调用了dep.notify()
  2. Vue 官方文档也有提到:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替

3. nextTick的实现

3.1 异步更新的入口

数据更新时,触发 setter 后调用dep.notify()

// src/core/observer/dep.js

/**
* 通知 dep 中的所有 watcher,执行 watcher.update() 方法
*/
notify () {
  const subs = this.subs.slice()
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    subs.sort((a, b) => a.id - b.id)
  }
  // 遍历 dep 中存储的 watcher,执行 watcher.update()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

3.2 watcher.update():更新

调用 dep.notify() 后,会将需要更新的 watcher 中的 update 执行。如果此时有很多个watcher,并不是多次更新,而是先将watcher缓存起来,等会一起更新

// src/core/observer/watcher.js

class Watcher {
    ...
    update() {
        ...
        
        // 当多次更新数据,调用update时,先将watcher缓存起来,等会一起更新
        /*
           如:this.msg = 1
              this.msg = 2
              this.msg = 3
              this.name = 'vue'
        */
        queueWatcher(this)
    }
}

3.4 queueWatcher

let queue = []
let has = {} // 用于维护存放了哪些 watcher
let pending = false // 判断当前事件环中的所有的 watcher 全部更新完成

function flushSchedulerQueue() {
    for (let i = 0; i < queue.length; i++) {
        queue[i].run()
    }
    queue = []
    has = {}
    pending = false
}

function queueWatcher(watcher) {
    const id = watcher.id
    if (has[id] == null) {
        queue.push(watcher)
        has[id] = true
        
        // 开启一次更新操作,批量处理需要更新的 watcher
        if (!pending) {
            nextTick(flushSchedulerQueue) // this.$nextTick也是调用了这个方法
            pending = true
        }
    }
}

3.5 nextTick的实现

$nextTick 内部也是调用 nextTick 函数

Vue.prototype.$nextTick = function (fn) { 
    return nextTick(fn, this) 
}
// nextTick 方法简单实现
const callbacks = []
let waiting = false

// 在一个事件循环处理所有的回调
function flushCallbacks() {
    callbacks.forEach(cb => cb())
    waiting = false
}

// vue2为了考虑兼容性,Vue3不再考虑兼容性问题
// 依次对 Promise,MutationObserver,setImmediate,setTimeout 进行判断
function timer(flushCallbacks) {
    let timerFn = () => {}
    if (Promise) {
        timerFn = () => {
            Peomise.resolve().then(flushCallbacks)
        }
    } else if (MutationObserver) {
        let textNode = document.createTextNode(1)
        let observer = new MutationObserver(flushCallbacks)
        observer.observe(textNode, {
            characterData: true
        })
        timerFn = () => {
            textNode.textContent = 3
        }
    } else if (setImmediate) {
        timerFn = () => {
            setImmediate(flushCallbacks)
        }
    } else {
        timerFn = () => {
            setTimeout(flushCallbacks)
        }
    }
    
    timerFn()
}

function nextTick(cb, ctx = null) {
    callbacks.push(cb)
    
    if (!waiting) {
        timer(flushCallbacks)
        waiting = true
    }
}

4. 总结

  1. Vue是异步更新的,只要侦听到数据发生变化,vue将开启一个队列,并缓冲同一个事件循环中发生的所有数据的变化,最后再统一更新视图,这是为了避免不必要的计算和DOM操作
  2. 异步更新机制的核心是利用了浏览器的异步任务队列来实现。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替
  3. 当多次更新数据,调用update时,先将watcher缓存起来,放在一个队列中。也就是调用了 queueWatcher,执行queue.push(watcher)。然后通过 flushSchedulerQueue方法统一处理需要更新的 watcher
  4. 通过 nextTick方法flushSchedulerQueue 放入一个 callbacks 数组,在一个事件循环处理所有的回调