1. 前言
本文主要是梳理一下vue中异步更新的机制,其中会涉及到JS的运行机制,如果不太了解的,可以先看一下 JS事件循环。
本篇文章主要从两个问题进行梳理
- Vue的异步更新机制
- $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是异步渲染的
this.msg = '1234'。因为在初始化的时候已经做了数据劫持,当对数据进行更新操作时,就会触发 setter 的拦截(vue中会检测新值和旧值是否相等,如果相等就不更新),如果不相等,就会触发更新。由dep通知watcher进行更新,也就是调用了dep.notify()- Vue 官方文档也有提到:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和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. 总结
- Vue是异步更新的,只要侦听到数据发生变化,vue将开启一个队列,并缓冲同一个事件循环中发生的所有数据的变化,最后再统一更新视图,这是为了避免不必要的计算和DOM操作
- 异步更新机制的核心是利用了浏览器的异步任务队列来实现。Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替 - 当多次更新数据,调用update时,先将watcher缓存起来,放在一个队列中。也就是
调用了 queueWatcher,执行queue.push(watcher)。然后通过flushSchedulerQueue方法统一处理需要更新的 watcher - 通过
nextTick方法将flushSchedulerQueue放入一个 callbacks 数组,在一个事件循环处理所有的回调