众所周知,Vue 基于数据驱动视图,数据更改会触发
data
的setter
函数,通知watcher
进行更新。更新过程也是需要经历很多操作,例如模板编译、dom diff、渲染等,频繁进行更新那么性能也会很差。那么在Vue里面是怎么做的么?在了解之前,我们先了解一下 js 的运行机制。
js运行机制
众所周知,JS是基于事件循环的单线程语言。事件循环的大致步骤如下。
✅(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
✅(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件(回调)。
✅(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
✅(4)主线程不断重复上面的第三步。
任务队列 中的任务(task) 被分为两类,分别是 宏任务(macro task) 和 微任务(micro task)
宏任务:在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务队列,但需要等到下一次事件循环才会执行。常见的宏任务有
setTimeout、setInterval、setImmediate、requestAnimationFrame
微任务:当前事件循环的同步任务执行完毕,任务队列中的任务就会被依次执行。在执行过程中,如果遇到微任务,微任务被加入到当前事件循环的微任务队列中执行直至清空。简单来说,当前事件循环中只要有微任务就会执行直至清空,而不是放到下一个事件循环才执行。常见的微任务有
MutationObserver、Promise.then、process.nextTick。
Vue数据更新机制
只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际 (已去重的) 工作。
例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环 tick 中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用 “数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:
// html
<div id="example">{{message}}</div>
// javascript
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
// dom 没有立即更新
vm.$el.textContent === 'new message' // false
// 在nextTick中 获取到更新后的dom
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
复制代码
为什么nextTick可以访问更新后的dom?
// 当一个 Data 更新时,会执行以下代码
// 1. 触发 setter
// 2. setter 中调用 dep.notify
// 3. Dep notify 会遍历所有相关的 Watcher 执行 update 方法
class Watcher {
// 4. 执行更新操作
update() {
queueWatcher(this)
}
}
const queue = [];
function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] == null) { // 避免添加同一 watcher
has[id] = true
if (!flushing) { // 处理 Watcher 渲染时,可能产生的新 Watcher eg:v-if 触发新watcher
// 5. 将当前 Watcher 添加到异步队列
queue.push(watcher)
} else {
// 新watcher 添加到指定位置
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 保证此次watcher都在同一tick 并且不会重复执行
if (!waiting) {
waiting = true
// 6. 执行异步队列,并传入更新视图回调
nextTick(flushSchedulerQueue)
}
}
}
// 更新视图的具体方法
function flushSchedulerQueue() {
let watcher, id;
// 排序,先渲染父节点,再渲染子节点
// 这样可以避免不必要的子节点渲染,如:父节点中 v-if 为 false 的子节点,就不用渲染了
queue.sort((a, b) => a.id - b.id);
// 遍历所有 Watcher 进行批量更新。
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 更新 DOM
watcher.run();
}
}
复制代码
nextTick
可以看到watcher的update并没有直接更新视图,而是将更新视图的方法传给了nextTick方法,接下来看下nextTick源码
// src\core\util\next-tick.js
const callbacks = [];
let pending = false;
let timerFunc;
export function nextTick (cb?: Function, ctx?: Object) {
// 1.将更新视图方法添加 callbacks中
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
}
})
// 2.pending用来保障同一时间 只会执行一个timerFunc()
if (!pending) {
pending = true
timerFunc()
}
}
复制代码
let timerFunc;
// 判断是否支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
// 判断是否支持 MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
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)
}
isUsingMicroTask = true
}
// 判断是否支持 setImmediate
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
// 最不济 setTimeout
else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
复制代码
可以看出
timerFunc
其实是根据浏览器兼容创建的异步方法,优雅降级,最终调用flushCallbacks
执行flushSchedulerQueue
方法更新dom。
vm.nextTick
可以访问到更新后的dom,是因为异步队列也遵循先来后到,修改 Data 触发的更新异步队列会先执行,执行完成 dom 更新,此时调用 nextTick 的回调可以访问到更新后的dom。
this.$nextTick
this.$nextTick
原理就是nextTick
方法
// src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
复制代码
总结
vue异步更新原理就是触发data的setter最终调用watcher的update方法,update调用queueWatcher将本身推入一个全局的queue队列,并将更新视图的方法传给nextTick,最终通过nextTick方法异步更新视图。