共计 3297 个字符,预计需要花费 9 分钟才能阅读完成。
在第三篇 vue0.11 版本源码浏览系列三:指令编译里咱们晓得如果某个属性的值变动了,会调用依赖该属性的 watcher
的update
办法:
p.update = function () {if (!config.async || config.debug) {this.run()
} else {batcher.push(this)
}
}
它没有间接调用指令的 update
办法,而是交给了 batcher
,本篇来看一下这个batcher
做了什么。
顾名思义,batcher
是批量的意思,所以就是批量更新,为什么要批量更新呢,先看一下上面的状况:
<div v-if="show"> 我进去了 </div>
<div v-if="show && true"> 我也是 </div>
window.vm.show = true
window.vm.show = false
比方有两个指令依赖同一个属性或者间断批改某个属性,如果不进行批量异步更新,那么就会屡次批改 dom,这显然是没必要的,看上面两个动图能更直观的感触到:
没有进行批量异步更新的时候:
进行了批量异步更新:
能清晰的发现通过异步更新能跳过两头不必要的渲染以达到优化性能的成果。
接下来看一下具体实现,首先是 push
函数:
// 定义了两个队列,一个用来寄存用户的 watcher,一个用来寄存指令更新的 watcher
var queue = []
var userQueue = []
var has = {}
var waiting = false
var flushing = false
exports.push = function (job) {
// job 就是 watcher 实例
var id = job.id
// 在没有 flushing 的状况下 has[id]用来跳过同一个 watcher 的反复增加
if (!id || !has[id] || flushing) {has[id] = 1
// 首先要阐明的是通过 $watch 办法或者 watch 选项生成的 watcher 代表是用户的,user 属性为 true
// 这里正文说在执行工作中用户的 watcher 可能会触发非 user 的指令更新,所以要立刻更新这个被触发的指令,否则 flushing 这个变量是不须要的
if (flushing && !job.user) {job.run()
return
}
// 依据指令的类型增加到不同的队列里
;(job.user ? userQueue : queue).push(job)
// 上个队列未被清空前不会创立新队列
if (!waiting) {
waiting = true
_.nextTick(flush)
}
}
}
push
办法做的事件是把 watcher
增加到队列 quene
里,而后如果没有扔过 flush
给nextTick
或者上次扔给 nextTick
的flush
办法曾经被执行了,就再给它一个。
flush
办法用来遍历队列里的 watcher
并调用其 run
办法,run
办法最终会调用指令的 update
办法来更新页面。
function flush () {
flushing = true
run(queue)
run(userQueue)
// 清空队列和复位变量
reset()}
function run (queue) {
// 循环执行 watcher 实例的 run 办法,run 办法里会遍历该 watcher 实例的指令队列并执行指令的 update 办法
for (var i = 0; i < queue.length; i++) {queue[i].run()}
}
接下来就是 nextTick
办法了:
exports.nextTick = (function () {var callbacks = []
var pending = false
var timerFunc
function handle () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {copies[i]()}
}
// 反对 MutationObserver 接口的话应用 MutationObserver
if (typeof MutationObserver !== 'undefined') {
var counter = 1
var observer = new MutationObserver(handle)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {characterData: true// 设为 true 示意监督指定指标节点或子节点树中节点所蕴含的字符数据的变动})
timerFunc = function () {counter = (counter + 1) % 2// counter 会在 0 和 1 两者循环变动
textNode.data = counter// 节点变动会触发回调 handle,}
} else {// 否则应用定时器
timerFunc = setTimeout
}
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(handle, 0)
}
})()
这是个自执行函数,个别用来定义并保留一些局部变量,返回了一个函数,就是 nextTick
办法本法了,flush
办法会被 push
到callbacks
数组里,咱们罕用的办法 this.$nextTick(() => {xxxx})
也会把回调增加到这个数组里,这里也有一个变量 pending
来管制反复增加的问题,最初增加到事件循环的队列里的是 handle
办法。
批量很容易了解,都放到一个队列里,最初一起执行就是批量执行了,然而要了解 MutationObserver
的回调或者 setTimeout
的回调为什么能异步调用就须要先来理解一下 JavaScript
语言里的事件循环 Event Loop
的原理了。
简略的说就是因为 JavaScript
是单线程的,所以工作须要排队进行执行,前一个执行完了能力执行前面一个,但有些工作比拟耗时而且没必要等着,所以能够先放一边,先执行前面的,等到了能够执行了再去执行它,比方有些 IO
操作,像常见的鼠标键盘事件注册、Ajax
申请、settimeout
定时器、Promise
回调等。所以会存在两个队列,一个是同步队列,也就是主线程,另一个是异步队列,方才提到的那些事件的回调如果能够被执行了都会被放在异步队列里,当主线程上的工作执行结束后会把异步队列的工作取过去进行执行,所以同步代码总是在异步代码之前执行,执行完了后又会去查看异步队列,这样一直循环就是Event Loop
。
然而异步工作里其实还是分两种,一种叫宏工作,常见的为:setTimeout
、setInterval
,另一种叫微工作,常见的如:Promise
、MutationObserver
。微工作会在宏工作之前执行,即便宏工作的回调先被增加到队列里。
当初能够来剖析一下异步更新的原理,就以结尾提到的例子来说:
<div v-if="show"> 我进去了 </div>
<div v-if="show && true"> 我也是 </div>
window.vm.show = true
window.vm.show = false
因为有两个指令都依赖了 show
,表达式不一样,所以会有两个watcher
,这两个watcher
都会被 show
属性的 dep
收集,所以每批改一次 show
的值都会触发这两个 watcher
的更新,也就是会调两次 batcher.push(this)
办法,第一次调用后会执行 _.nextTick(flush)
注册一个回调,间断两次批改 show
的值,会调用四次上述提到的 batcher.push(this)
办法,因为反复增加的被过滤掉了,所以最初会有两个 watcher
被增加到队列里,以上这些操作都是同步工作,所以是间断被执行完的,等这些同步工作都被执行完了后就会把方才注册的回调 handle
拿过去执行,也就是会一次性执行方才增加的两个watcher
:
以上就是 vue
异步更新的全部内容。