vue2源码学习--04统一异步更新方法 nextTick

78 阅读3分钟

开始之前先对上一篇有个小bug进行说明修复,就是在触发数据的get的时候不能简单粗暴的直接进行dep.depend(),触发这个方法会去调用Dep.target.addDep(),如果触发get只是在逻辑里读取数据并没有在视图上读取,就会导致Dep.target为空报错。
其实就是只有在视图上读取的数据才应该进行依赖收集,所以要通过判断Dep.target是否有值来决定是否进行depend,具体改正如下

export function defineReactive(target, key, value) { 
  // 递归判断
  observe(value)
  let dep = new Dep()
  Object.defineProperty(target, key, {
      get() {
          // 可以进行调试
          if(Dep.target) { // 只有视图上使用的数据才进行依赖收集
             dep.depend()
          }
          return value
      },
      set(newValue) { 
          if(newValue === value) return 
          //设置的新值也要进行劫持
          observe(newValue)
          value = newValue 
          dep.notify()
      }
  })
}

ok开始本篇正文
先看我们要解决的问题

image.png

image.png 可见更新数据之后直接获取dom和通过promise.then获取的dom都是滞后的, 原因是当前我们通过setTimeout实现的数据异步更新,同样是异步的微任务会优先于它执行,这就导致我们必须通过setTimeout获取新dom,但延时器对前端开发者来说都是不小的心智负担,所以我们需要规定一个统一的方法去异步更新。


回到watcher.js

function queueWatcher(watcher) {
  const id = watcher.id
  if(!has[id]) {
      queue.push(watcher)
      has[id] = true
      // 不管watcher执行多少次 但是只执行一次刷新操作
      if(!pending) {
          // setTimeout(flushSchedulerQueue,0) // 统一更新方法避免promise.then微任务
          // 改动点 引出nextTick
          nextTick(flushSchedulerQueue)
          pending = true
      }
  }
}

let callbacks = []
let waiting = false
function flushCallbacks() {
    waiting = false
    let cbs = callbacks.slice(0)
    callbacks = []
    cbs.forEach(cb => cb())
} 

export function nextTick(cb) {
  callbacks.push(cb)
  if(!waiting) {
      setTimeout(() => {
          flushCallbacks()
      },0)
      waiting = true
  }
}

会发现跟之前的watcher队列去重执行是一个思路,通过变量锁和一个异步代码,只要中间执行nextTick就会往队列里加等待执行的回调函数,也就是外部我们书写的业务逻辑只要用到nextTick,并且此时也涉及到数据更新的watcher正在等待执行,就会把他们按顺序放进callbacks队列里等待异步执行,开始执行放开锁。
但是看到我们开异步的方法仍然是延时器,跟我们外部自己用延时器没有本质的区别,所以就要优化这个异步的方法了,Promise.resolve.then()肯定是最优解,但是vue2还是对一些低版本的浏览器保证兼容,所以这里采用了一系列的降级策略。

// nextTick 没有直接使用某个api 而是优雅降级
// 先采用promise(ie不兼容) MutationObserve  可以考虑ie专享setImmediate  setTimeout
let timerFunc;
if(Promise){
    timerFunc = () => {
        Promise.resolve().then(flushCallbacks)
    }
} else if(MutationObserver) {
    // 这个api就是实例化的时候传入回调函数,然后他可以监听一个文本节点,当文本节点改变就会执行传入的回调函数
    let observer = new MutationObserver(flushCallbacks)
    let textNode = document.createTextNode(1)
    observer.observe(textNode)
    timerFunc = () => {
        textNode.textContent = 2
    }
}else if(setImmediate) {
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
}else {
    // 最后考虑延时器
    setTimeout(() => {
        flushCallbacks()
    },0)
}

最后,我们还要把nextTick暴露出去供业务逻辑使用

image.png

image.png ok我们验证下效果

image.png

image.png 完美实现。
这块还有个数组的依赖收集和更新还没实现,这个比较复杂先放一放,下一篇先实现mixin和生命周期