vue2异步更新原理
简述
通过上一篇文章,我们已经知道了,vue2的基本响应式原理。
在依赖收集结束之后,当响应式数据发生变化 -> 触发 setter 执行 dep.notify -> 让 dep 通知 自己收集的所有 watcher 执行 update 方法,渲染更新。
实际上这是一种同步的更新方式,每次数据更新都会导致一次渲染更新,这样其实会存在很严重的性能问题。于是vue中便使用了批处理异步更新的方法将多次数据变动,合并为一次渲染更新。
大概原理:依赖收集结束之后,当响应式数据发生变化 -> 触发 setter 执行 dep.notify -> 让 dep 通知 自己收集的所有 watcher 执行 update 方法 -> watch.update 调用 queueWatcher 将自己放到 watcher 队列 ->新建刷新 watcher 队列的方法flushSchedulerQueue用于集中处理watcher更新 -> 接下来调用 nextTick 方法将flushSchedulerQueue方法放到 callbacks 数组 -> 然后新建刷新 callbacks 数组的方法并将其放到浏览器的异步任务队列(微任务或者宏任务) -> 待将来执行时最终触发 watcher.run 方法,执行 watcher.get 方法。
原理实现
改造Watcher
let uid = 0
class Watcher {
constructor(vm, exprOrFn, cb, options) {
// 唯一标识,实际上首次渲染时父组件的渲染watcher一定先于子组件的创建,所以id可以保证在异步更新时,父子组件的渲染顺序
this.id = uid++
// ...省略之前的代码
}
update() {
if (this.lazy) {
this.dirty = true;
} else {
// 删除 this.run()
// 将 watcher 放入异步 watcher 队列
queueWatcher(this)
}
}
}
watcher更新队列
查看源码:
// core/observer/scheduler.js
// 以下代码省略了,生命周期钩子调用和开发环境错误提示相关代码
const queue = [] // watcher更新队列
let has = {} // 存储更新队列中存在的watcher的id,用来去重
let waiting = false // 标识,nextTick中是否已经插入了队列执行函数
let flushing = false // 标识,判断现在是否正在执行更新队列
let index = 0 // 标识,保存当前正在执行的是更新队列的哪一个watcher, 用于调度排序
// 添加watcher至 更新队列
function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 队列中没有当前的watche才处理
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 队列未开始执行时,直接将watcher添加进队列
queue.push(watcher)
} else {
// 如果队列已经开始执行, 那么就根据watcher的id将其插入到对应的位置
// 如果该watcher对应的位置已经过去了,那么该watche将立即执行
// 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)
}
// waiting保证只会向nextTick中插入一次flushSchedulerQueue,flushSchedulerQueue内部之间使用了全局的queue,所以插入多次也是执行的同一个queue,那就没必要了
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
// 批量执行更新对列
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 依次执行前先排序,确保 父子组件执行顺序
queue.sort((a, b) => a.id - b.id)
// 依次执行watcher的run方法更新
// 直接用queue.length,是因为flushing时,可能还有watcher被添加进来
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
}
nextTick
// core/util/next-tick.js
// 以下代码省略了部分开发环境错误提示代码
const callbacks = [] // 下一个微任务中 需要处理的执行队列
let pending = false // 是否已经将callbacks放进了下一个微任务
// 依次执行
function flushCallbacks () {
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)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
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)
}
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb, ctx) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
// cb 存在为用户代码的情况所以需要try catch容错处理
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 已经将 flushCallbacks 放进了 下一个 微任务,就不在放了
if (!pending) {
pending = true
timerFunc()
}
// 兼容 Promise 用法
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
至此,vue2的异步更新功能完成