vue2的异步更新原理以及nextTick解析

480 阅读3分钟

vue2异步更新原理

简述

通过上一篇文章,我们已经知道了,vue2的基本响应式原理。

在依赖收集结束之后,当响应式数据发生变化 -> 触发 setter 执行 dep.notify -> 让 dep 通知 自己收集的所有 watcher 执行 update 方法,渲染更新。

实际上这是一种同步的更新方式,每次数据更新都会导致一次渲染更新,这样其实会存在很严重的性能问题。于是vue中便使用了批处理异步更新的方法将多次数据变动,合并为一次渲染更新。

大概原理:依赖收集结束之后,当响应式数据发生变化 -> 触发 setter 执行 dep.notify -> 让 dep 通知 自己收集的所有 watcher 执行 update 方法 -> watch.update 调用 queueWatcher 将自己放到 watcher 队列 ->新建刷新 watcher 队列的方法flushSchedulerQueue用于集中处理watcher更新 -> 接下来调用 nextTick 方法将flushSchedulerQueue方法放到 callbacks 数组 -> 然后新建刷新 callbacks 数组的方法并将其放到浏览器的异步任务队列(微任务或者宏任务) -> 待将来执行时最终触发 watcher.run 方法,执行 watcher.get 方法。

原理实现

改造Watcher

let uid = 0
class Watcher {
     constructor(vm, exprOrFn, cb, options) {
         // 唯一标识,实际上首次渲染时父组件的渲染watcher一定先于子组件的创建,所以id可以保证在异步更新时,父子组件的渲染顺序
         this.id = uid++ 
        // ...省略之前的代码
    }
    update() {
      if (this.lazy) {
          this.dirty = true;
      } else {
          // 删除 this.run()
          // 将 watcher 放入异步 watcher 队列
          queueWatcher(this)
      }  
    }
}

watcher更新队列

查看源码:

// core/observer/scheduler.js
// 以下代码省略了,生命周期钩子调用和开发环境错误提示相关代码const queue = [] // watcher更新队列
let has = {} // 存储更新队列中存在的watcher的id,用来去重
let waiting = false // 标识,nextTick中是否已经插入了队列执行函数
let flushing = false // 标识,判断现在是否正在执行更新队列
let index = 0 // 标识,保存当前正在执行的是更新队列的哪一个watcher, 用于调度排序// 添加watcher至 更新队列
function queueWatcher (watcher: Watcher) {
    const id = watcher.id
    // 队列中没有当前的watche才处理
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            // 队列未开始执行时,直接将watcher添加进队列
            queue.push(watcher)
        } else {
            // 如果队列已经开始执行, 那么就根据watcher的id将其插入到对应的位置
            // 如果该watcher对应的位置已经过去了,那么该watche将立即执行
            // if already flushing, splice the watcher based on its id
            // if already past its id, it will be run next immediately.
            let i = queue.length - 1
            while (i > index && queue[i].id > watcher.id) {
                i--
            }
            queue.splice(i + 1, 0, watcher)
        }
        // waiting保证只会向nextTick中插入一次flushSchedulerQueue,flushSchedulerQueue内部之间使用了全局的queue,所以插入多次也是执行的同一个queue,那就没必要了
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}
​
// 批量执行更新对列
function flushSchedulerQueue () {
   
  flushing = true
  let watcher, id
​
  // 依次执行前先排序,确保 父子组件执行顺序
  queue.sort((a, b) => a.id - b.id)
​
  // 依次执行watcher的run方法更新
  // 直接用queue.length,是因为flushing时,可能还有watcher被添加进来
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
   
    id = watcher.id
    has[id] = null
    watcher.run()
  }
    
}

nextTick

// core/util/next-tick.js
// 以下代码省略了部分开发环境错误提示代码const callbacks = [] // 下一个微任务中 需要处理的执行队列
let pending = false // 是否已经将callbacks放进了下一个微任务// 依次执行
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
​
// 根据当前运行环境一步步降级使用,优先使用 微任务 其次 宏任务 处理
let timerFunc
​
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  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)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
​
export function nextTick (cb, ctx) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        // cb 存在为用户代码的情况所以需要try catch容错处理
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  
  // 已经将 flushCallbacks 放进了 下一个 微任务,就不在放了
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 兼容 Promise 用法
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
​

至此,vue2的异步更新功能完成