在第三篇vue0.11版本源码浏览系列三:指令编译里咱们晓得如果某个属性的值变动了,会调用依赖该属性的watcherupdate办法:

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 = truewindow.vm.show = false

比方有两个指令依赖同一个属性或者间断批改某个属性,如果不进行批量异步更新,那么就会屡次批改dom,这显然是没必要的,看上面两个动图能更直观的感触到:

没有进行批量异步更新的时候:

进行了批量异步更新:

能清晰的发现通过异步更新能跳过两头不必要的渲染以达到优化性能的成果。

接下来看一下具体实现,首先是push函数:

// 定义了两个队列,一个用来寄存用户的watcher,一个用来寄存指令更新的watchervar queue = []var userQueue = []var has = {}var waiting = falsevar flushing = falseexports.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里,而后如果没有扔过flushnextTick或者上次扔给nextTickflush办法曾经被执行了,就再给它一个。

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办法会被pushcallbacks数组里,咱们罕用的办法this.$nextTick(() => {xxxx})也会把回调增加到这个数组里,这里也有一个变量pending来管制反复增加的问题,最初增加到事件循环的队列里的是handle办法。

批量很容易了解,都放到一个队列里,最初一起执行就是批量执行了,然而要了解MutationObserver的回调或者setTimeout的回调为什么能异步调用就须要先来理解一下JavaScript语言里的事件循环Event Loop的原理了。

简略的说就是因为JavaScript是单线程的,所以工作须要排队进行执行,前一个执行完了能力执行前面一个,但有些工作比拟耗时而且没必要等着,所以能够先放一边,先执行前面的,等到了能够执行了再去执行它,比方有些IO操作,像常见的鼠标键盘事件注册、Ajax申请、settimeout定时器、Promise回调等。所以会存在两个队列,一个是同步队列,也就是主线程,另一个是异步队列,方才提到的那些事件的回调如果能够被执行了都会被放在异步队列里,当主线程上的工作执行结束后会把异步队列的工作取过去进行执行,所以同步代码总是在异步代码之前执行,执行完了后又会去查看异步队列,这样一直循环就是Event Loop

然而异步工作里其实还是分两种,一种叫宏工作,常见的为:setTimeoutsetInterval,另一种叫微工作,常见的如:PromiseMutationObserver。微工作会在宏工作之前执行,即便宏工作的回调先被增加到队列里。

当初能够来剖析一下异步更新的原理,就以结尾提到的例子来说:

<div v-if="show">我进去了</div><div v-if="show && true">我也是</div>
window.vm.show = truewindow.vm.show = false

因为有两个指令都依赖了show,表达式不一样,所以会有两个watcher,这两个watcher都会被show属性的dep收集,所以每批改一次show的值都会触发这两个watcher的更新,也就是会调两次batcher.push(this)办法,第一次调用后会执行_.nextTick(flush)注册一个回调,间断两次批改show的值,会调用四次上述提到的batcher.push(this)办法,因为反复增加的被过滤掉了,所以最初会有两个watcher被增加到队列里,以上这些操作都是同步工作,所以是间断被执行完的,等这些同步工作都被执行完了后就会把方才注册的回调handle拿过去执行,也就是会一次性执行方才增加的两个watcher

以上就是vue异步更新的全部内容。