Vue的批量更新原理

前端精髓

共 3498字,需浏览 7分钟

 · 2021-08-12


简单的代码开始:

var app = new Vue({  el: '#app',  data: {    message: 'Hello Vue!'  },  watch: {    message (val) {      console.log('mutation')    }  },  mounted () {    this.message = 1    this.message = 2    this.message = 3  }})


首先看这个例子中,连续3次触发了mutation,那么watch中的cb会被执行几次呢?


答案是一次。


那么为什么会是一次呢?本文会围绕着这个问题的解释来粗浅地讨论一下Vue中批量更新的原理。


首先要知道,msg这个key,是通过Object.defineProperty被监听了的,Vue通过这个api实现在key被set的时候(也就是this.msg = xxx这种操作),触发所有订阅了这个key的Watcher的update方法。


这里引入了一个Watcher的概念,那么这个Watcher是什么呢?


从语义上理解,Wathcer其实类似于一个key的观察者,当key被set的时候,Watcher会调用自身的update方法。


在 mountComponent 也就是加载组件的时候会调用 Watcher 创建观察者。


new Watcher(vm, updateComponent, noop, {  before () {    if (vm._isMounted && !vm._isDestroyed) {      callHook(vm, 'beforeUpdate')    }  }}, true /* isRenderWatcher */)


监听到一个set引起的mutation就立即同步执行一次cb吗?

显然是不可以的。


举个例子:

for (let i = 0; i < 1000; i++) {   this.msg = `循环了${i}次`;}


假如对于这种操作,1000次里面每次Watcher都要响应执行一次update,那可能是有很大的性能开销的。像本文的这个例子中update其实就是一个console.log,但是如果cb是一个开销比较大的方法,那么就可能会引起性能问题了。


所以update操作一定是异步的。


虽然知道要通过异步来解决,但具体是如何解决的呢?Vue的做法是把调用cb放到了一个micro task或者macro task队列中,具体放到微任务队列还是宏任务队列要看当前的运行环境是否支持Promise、MutationObserver、setImmediate这几个相当于放入微任务队列的api,支持就会放在微任务队列,不支持则使用setTimeout这个api把调用cb放到宏任务队列里。


不管放到微任务队列还是宏任务队列,调用cb都会在所有的同步代码执行完毕后执行。这一点涉及到event loop的知识,因为总是先执行所有的同步代码,然后从微任务队列中按顺序执行,微任务队列空了才会从宏任务队列中取出一条执行。如果此时微任务队列还有任务,那么就会继续按照这个循环执行,这个就是event loop。


通俗地理解Vue的行为就是在监听到key的mutation之后key的Watcher都会触发update,想要调用自身的cb属性,但是Vue仅仅是答应会在未来的某个时刻执行Watcher的这个update请求也就是调用它的cb,并且在调用之前都不会再受理该Watcher的update的请求。


下面看看源码

function queueWatcher (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) } }}

重点是 nextTick 源码

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true} 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} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) }} else { timerFunc = () => { setTimeout(flushCallbacks, 0) }}
function nextTick (cb, ctx) { 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() } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }}


浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报