Vue异步更新队列源码解析

208 阅读3分钟

Vue异步更新队列源码解析

此文章仅代表个人的阅读理解,如有不对欢迎指出

队列:就简单的理解为一个数组即可,不要想的太复杂

整个异步更新过程大致可以理解为

1、当数据发生改变会先触发defineReactive方法里面定义的set方法

2、set方法里面对调用 dep.notify() (内部就是遍历收集到的观察者的update方法)

3、watcher.update 方法被执行

4、通过queueWatcher方法将观察者推入到queue队列里面

5、queue队列是通过flushSchedulerQueue方法执行的

7、将flushSchedulerQueue方法通过nextTick推入到callbacks中执行

8、最终会由timerFunc方法执行

对应的源码如下:

defineReactive

// defineReactive 是整个响应式系统中最核心的方法
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 被get的时候收集依赖即Dep.target
        dep.depend()
        //...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      // dep.notify 就是通知所有的观察者更新
      dep.notify()
    }
  })
}

// Dep.notify定义
export default class Dep {
  static target: ?Watcher;
  id: number;
  // subs就是当前收集到的依赖
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  //...

  // 通知所有的watcher更新
  notify () {
    // ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// Wacther的update方法定义
class Watcher {
    //...
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 推入到queue队列
      queueWatcher(this)
    }
  }
}

callbacks 异步回调

// 可以看到其实就是个普通的数组
const callbacks = []

// 这个方法主要是用来添加任务
// 包括Vue.nextTick也是调用的这个方法实现
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 这里主要是把回调函数添加到callbacks
  callbacks.push(() => {
    //省略其他代码...
    cb.call(ctx)
  })
  // 这个pending会在 flushCallbacks 方法里面被修改, 即在队列被刷新或执行的时候
  if (!pending) {
    pending = true
    // 这个就是一个异步的函数,详见下面
    timerFunc()
  }
  //省略其他代码 ...
}

// timerFunc 是实现异步的核心方法
// 优先使用微任务,但在某些情况下会使用宏任务
let timerFunc

// 1、优先使用Promise微任务执行回调(这里在2.5版本以前实现略有不同)
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // 这里好像是对移动平台的一些特殊处理,强制刷新队列
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true

// 2、 在不支持Promise的情况下的优先选择
// MutationObserver: https://developer.mozilla.org/zh-cn/docs/web/api/mutationobserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  //...
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true

// 3、这里其实就是利用宏任务队列了
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 4、使用setTimeout进行兜底
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
  • 到这里应该能大概理解vue里面是怎么执行异步任务的了
  • 下面就看vue是如何执行queue队列的

queue 队列定义如下

// 可以看到queue队列就是一个Watcher类型的数组
// Watcher的定义这里就不做过多赘述了,想了解的同学可以看 core/observer/watcher.js
const queue: Array<Watcher> = []

// 这里Watcher主要做两件事
// 1、将watcher压入队列,且重复的只会被压入一次
// 2、例用nextTick将flushSchedulerQueue丢到异步任务里面执行
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 这里has[id]应该是为了防止重复添加,在被执行的时候会被重置为null
  // 也可以理解为等待执行的一个状态
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 推入队列
      queue.push(watcher)
    } else {
      // 如果当前的队列正在被执行,那么会根据id进行顺序插入
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 开始执行队列 waiting会在reset方法里面被重置
    if (!waiting) {
      waiting = true
      // 在开发环境 并且配置了async那么会立即执行 flushSchedulerQueue
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 生产环境会将当前的队列执行函数推入到异步任务中即callbacks
      // nextTick 就是上面提到的nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}