关于数据响应化,问一个常见的问题:html
下面示例代码中的两个输出console.log(p1.innerHTML)
,分别是什么?为何?html5
<!DOCTYPE html>
<html>
<body>
<div id="demo">
<h1>异步更新</h1>
<p id="p1">{{foo}}</p>
</div>
<script>
const app = new Vue({
el: '#demo',
data: { foo: '' },
mounted() {
setInterval(() => {
this.foo = 't1'
this.foo = 't2'
this.foo = 't3'
console.log(p1.innerHTML) //此时,页面的展现值?
this.$nextTick(() => {
console.log(p1.innerHTML) //此时,页面的展现值?
})
}, 1000);
}
});
</script>
</body>
</html>
复制代码
这个问题的第一问“是什么”,并不复杂。难的是"为何"。该问题的本质涉及到 Vue 的异步更新问题。算法
首先,须要明确的是:Vue 的更新 DOM 的操做是异步的,批量的。之因此这么作的原因也很简单:更新 DOM 的操做是昂贵的,消耗较大。如上面的展现例子所示,Vue 内部会连续更新三次 DOM 么?那显然是不合理的。批量、异步的操做才更优雅。express
咱们想要去源码看看,Vue 更新 DOM 的批量与异步操做,究竟是如何作的呢?bash
首先界定一个界限:咱们不会立马深刻到虚拟 DOM 的生成与页面更新的 patch 算法中去,只是想要看看这个批量与异步的过程,解决刚刚提到的问题。app
从以前的笔记内容可知:数据响应的核心方法defineReactive()
中,当数据发生变化的时候,会调用Dep.notify()
方法,通知对应的Watcher执行updateComponent()
操做,继而从新渲染执行更新页面。异步
让咱们从Dep的notify()
方法提及。async
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
...//省略
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
复制代码
可知,其内部是执行的是相关联的 Watcher 的update()
方法。函数
import { queueWatcher } from './scheduler'
export default class Watcher {
...//省略
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {//若是是同步
this.run()
} else {
queueWatcher(this) //Watcher的入队操做
}
}
//实际执行的更新方法,会被scheduler调用
run () {
if (this.active) {
//this.get()是挂载时传入的updateComponent()方法
const value = this.get()
//若是是组件的Watcher,不会有返回值value,不会执行下一步
//只有用户自定义Watcher才会进入if
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
复制代码
看到这里,提问一哈:若是在同一时刻,组件实例中的 data 修改了屡次,其对应的 Watcher 也会执行queueWatcher(this)
屡次,那么是否会在当前队列中存在多个一样的Watcher呢?oop
带着这个问题,查看同一文件夹下schedule.js
的queueWatcher()
方法:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//去重
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
//异步刷新队列
nextTick(flushSchedulerQueue)
}
}
}
复制代码
代码中看到:每一个 Watcher 都会有一个 id 标识,只有全新的 Watcher 才会入队。批量的过程咱们看到了,将是将 Watcher 放入到队列里面去,而后批量操做更新。
看了这个批量更新的操做,有人会问:屡次数据响应化,只有第一次更新的 Watcher 才会进入队列,是否是意味着只有第一次的数据响应化才生效,然后几回的数据响应化无效了呢?
回答:并非这样的,数据响应化一直都在进行,变化的数据也一直在变。须要明确其和批量更新队列之间的关联,发生在 Watcher 的 run() 方法上。当执行 run() 方法的时候,其获取的 data 是最新的 data。
讲了批量,那么异步的过程是怎样的呢?让咱们来看看nextTick()
函数内部,了解一些关于异步操做的知识点:
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
//关于timerFunc的选取过程
let timerFunc
//优先选择Promise,由于Promise是基于微任务的
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
//次优选择MutationObserver,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)
}
} else {
// 最无奈的选择,选择setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
//nextTick: 按照特定异步策略timerFunc() 执行队列操做
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()
}
}
复制代码
关于宏任务与微任务,能够查看更多有意思的页面: