vue高效的一个很重要的原因是vue的批量、异步更新策略。
要理解批量、异步更新需要先了解一个概念:事件循环。
事件循环
事件循环:因为js是一个单线程的语言,特定的时间内只能特定的代码执行。要等待上一段代码执行完成后再执行一下段代码,如果上一段代码需要很长时间才能执行完成,那么就必须等待吗?答案显然是否定的,因为js除了单线程外还有一个叫任务队列的东西。它会把一些需要等待一定时间的操作放在任务队列中执行。 任务队列有两种 :macro-task(宏任务) 、micro-task(微任务) macro-task(宏任务)
- setTimeout/setInterval
- setImmediate
- I/O操作
- 主文档对象、解析HTML
- 事件
- 页面加载、输入
micro-task(微任务)
- process.nextTick
- Promise
- MutationObserver
注意:以上方法的回调函数会被放到任务队列中,他们自身会直接执行。例:Promise会直接执行,但是then()会被加入到执行队列中。 javascript的执行机制是:首先执行调用栈中的函数,当调用栈中的执行上下文全部被弹出,只剩全局上下文的时候,就开始执行micro-task(微任务)的执行队列,微任务执行完成就开始执行宏任务(macro-task)中的。先进入的先执行,后进入的后执行。宏任务执行完成之后,js代码会检查是否有微任务需要执行,这样就形成了宏任务-微任务-宏任务-微任务的循环。这就形成了event loop。
vue就是借用了事件循环机制,在一次页面加载或事件循环中一次性把要更新的全部更新,而不是变一个属性进行一次更新,因此会非常高效。
vue中的具体实现
- 异步:只要监听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
- 批量:如果一个Watcher被多次触发,只会被推入到队列中一次。去重对于不必要的计算和DOM操作是非常重要的,然后,在下一个事件循环‘tick’中,Vue刷新队列执行更新
- 异步策略,vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver、setImmediate,如果执行环境不支持(IE),则使用setTimeout(fn,0)代替。
update() 执行入队操作
src/core/observer/watcher.js
dep.notify()之后Watcher执行更新,执行入队操作。
//更新函数
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
//加入watcher队列 执行入队操作******
queueWatcher(this)
}
}
queueWatcher() 执行Watcher入队操作
src/core/observer/scheduler.js
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
//去重,不存在Watcher才加入,非常高效
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 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)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
//从这里进入next-tick.js
nextTick(flushSchedulerQueue)
}
}
}
nextTick(flushSchedulerQueue) 异步策略
src/core/util/next-tick.js
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
//异步策略
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
为了验证vue的异步更新,下面的小例子拿去调试可以深入去理解一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src='../../dist/vue.js'></script>
</head>
<body>
<div id="app">
<h2>异步更新</h2>
<p id="p1">{{obj.bar}}</p>
</div>
<script>
//创建实例
/*
会有几个Observer ? 几个Dep ? 几个watcher
2个:一个对象会有一个Observer,obj是一个对象,bar是一个对象。 2个Dep :有几个key就会有几个dep,obj是一个key,bar是一个key 1个Watcher,因为一个组件只有一个watcher
*/
new Vue({
el: '#app',
data: {
foo: 'ready'
},
mounted() {
setInterval(() => {
this.foo = Math.random()
this.foo = Math.random()
this.foo = Math.random()
//异步行为,此时foo的内容没变
console.log(p1.innerHTML)
this.$nextTick(() => {
//这里才是最新的值,值 是第三个,为什么? 第一次入队了,第二次进入队列的时候,发现已经进了不再进队列了,但是值 是会变的
console.log(p1.innerHTML);
})
}, 2000);
},
})
</script>
</body>
</html>