Vue0.11版本源码阅读系列五:批量更新是怎么做的

简介: Vue0.11版本源码阅读系列五:批量更新是怎么做的

在第三篇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 = true
window.vm.show = false


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


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


image.png


进行了批量异步更新:


image.png


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


接下来看一下具体实现,首先是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里,然后如果没有扔过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 = true
window.vm.show = false


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


image.png


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



相关文章
|
6月前
|
JavaScript 前端开发 算法
用vue想要拿20k,面试题要这样回答(源码版)
用vue想要拿20k,面试题要这样回答(源码版)
|
3月前
|
JavaScript 前端开发 API
【Vue面试题三十一】、你是怎么处理vue项目中的错误的?
这篇文章讨论了Vue项目中错误的处理方式,包括后端接口错误和代码逻辑错误的处理策略。文章详细介绍了如何使用axios的拦截器处理后端接口错误,以及Vue提供的全局错误处理函数`errorHandler`和生命周期钩子`errorCaptured`来处理代码中的逻辑错误。此外,还分析了Vue错误处理的源码,解释了`handleError`、`globalHandleError`、`invokeWithErrorHandling`和`logError`函数的作用和处理流程。
【Vue面试题三十一】、你是怎么处理vue项目中的错误的?
|
JavaScript
Vue项目常见问题解决方案
Vue项目常见问题解决方案
86 0
|
前端开发 JavaScript
【React工作记录四十九】dva的简单使用流程
【React工作记录四十九】dva的简单使用流程
114 0
|
前端开发 JavaScript
【React工作记录三十六】react开发规范参考
【React工作记录三十六】react开发规范参考
224 0
|
前端开发 JavaScript
#yyds干货盘点 【React工作记录三十六】react开发规范参考
#yyds干货盘点 【React工作记录三十六】react开发规范参考
91 0
|
JavaScript 测试技术 API
深入解析 Vue 的热更新原理,尤大是如何巧用源码中的细节?
大家都用过 Vue-CLI 创建 vue 应用,在开发的时候我们修改了 vue 文件,保存了文件,浏览器上就自动更新出我们写的组件内容,非常的顺滑流畅,大大提高了开发效率。想知道这背后是怎么实现的吗,其实代码并不复杂。
|
前端开发 JavaScript 测试技术
Vue3原理实战运用,我用40行代码把他装进了React做状态管理
vue-next是Vue3的源码仓库,Vue3采用lerna做package的划分,而响应式能力@vue/reactivity被划分到了单独的一个package中。 如果我们想把它集成到React中,可行吗?来试一试吧。
|
存储 JavaScript 前端开发
Vue3官方出的Playground你都用了吗?没有没关系,直接原理讲给你听
相比`Vue2`,`Vue3`的官方文档中新增了一个在线`Playground`,本文会带领各位从头探索一下它的实现原理
772 0
|
JavaScript
Vue 2 阅读理解(九)之 mergeOptions 配置合并
Vue 2 阅读理解(九)之 mergeOptions 配置合并
307 0
下一篇
无影云桌面