Vue2源码解析:从nextTick与异步更新说起

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

Vue为什么要设计nextTick

vue或react这种框架,最大的性能消耗来自于diff计算,而修改data数据,或者setState,这些触发更新的操作,都意味着一次diff的计算,如果没有任何的优化,频繁地进行数据的修改,很容易造成页面的卡顿。

因此,两个框架都不约而同地设计了更新合并。曾有一阵子,react中关于setState是同步还是异步的面试题几乎成为了一道必问的问题,而vue中nextTick相关的面试题也屡见不鲜。

本期将从nextTick开始,探究Vue中如何进行更新合并。

nextTick做了什么

export function nextTick (cb?: Function, ctx?: Object) {
  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()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

复制代码

nextTick接收一个函数参数,并且将这个函数放入了callbacks,之后调用了 timerFunc(),前者是一个空数组,后者其实就是一个微任务。

const callbacks = []
...
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
}
复制代码

timerFunc会调用flushCallbacks,这个函数会浅拷贝callbacks,并依次执行里面的函数

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
复制代码

因此nextTick其实就是将回调函数放入在当前EventLoop的微任务队列。

那为什么要这么设计呢?

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

我们可以推测,修改数据和dom的更新是异步的,因此要获取更新后的dom,也需要等待异步。那么这个异步是如何实现的呢?

我们先来看Vue从beforeCreate到created做了什么。

Vue init执行顺序

flowchart LR
    new["new Vue()"] --> _init 
   

 subgraph _init
     initLifecycle-->initEvents-->initRender-->callHook1["callHook(vm, 'beforeCreate')"]-->initInjections-->initState-->initProvide-->callHook2["callHook(vm, 'created')"]
     end
      subgraph initState
      obdata["observe(data, true)"] --> newOb["ob = new Observer(value)"] --> walk["this.walk(value)"] --> defineReactive
    end

这里需要关注的是initState中的defineReactive

defineReactive

这个方法里进行了大家熟知的Object.defineProperty操作:

export function defineReactive (
  obj: Object, key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {
    ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
     ...
    },
    set: function reactiveSetter (newVal) {
      ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

Vue更新执行顺序

修改data数据后,触发这里的set,set进行了数据的更改,然后调用了dep.notify()

notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
复制代码

这些subs便是一个个Watcher,经过依赖收集,这些watcher会被加入subs数组

 class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
}
复制代码

Watcher

watcher有一个重要的成员vm,代表的是组件实例,它的update调用的是queueWatcher

class Watcher {
    vm: Component;
    ...
    update () {
        ...
         queueWatcher(this)
  }
}
复制代码

queueWatcher

function queueWatcher (watcher: 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)
    }
    // queue the flush
    if (!waiting) {
        ...
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

在最后又遇到了nextTick,可以看到从这里开始了异步更新。

异步更新的得与失

要做性能优化就必定要做更新合并,要做更新合并,首要的问题是如何确定更新已经结束了?

异步更新的思路是认为:对data的修改是在宏任务队列中完成的,当进入微任务队列时,这些修改操作必定已经完成了,接下来只要一次性完成更新即可。

这种设计思路清晰,代码也比较简单,缺点是会造成数据和dom的不一致(毕竟数据改完后,dom还得等微任务后才会更新完),因此不得不借助nextTick

那么有没有同步更新完成更新合并呢?有,比如React16

点击后修改数据,并触发更新:

flowchart TD
subgraph onClick
    s1[setState data1] -->
    s2[setState data2] -->log["console.log(state)"]
end

触发onClick --> 委托事件 --> 进入更新生命周期 

subgraph 委托事件
 s11[执行setState data1]--> s111["得到state副本,开启更新流程"] --> s22[执行setState data1] --> s222["得到state副本,已在更新流程中"] --> s33["执行console.log(state)"]
 
end

subgraph 进入更新生命周期
   合并多次setState生成的副本得到完整的state -->
   根据这个新state执行一次性更新
end

同步的方式下,state和dom是保持统一的,dom不更新,state也不变。

但也有几个缺点,比如反直觉,setState的命名与逻辑不一致了,还不如叫requestStateChange。另外状态的修改,必须是在能够监控到的地方发生,比如说生命周期,自定义的委托事件。

当然这两种设计都能达到更新合并这一主要目标,只有取舍,没有好坏。

Vue是如何收集依赖的?

当然再回到上面,有一个问题还没有解释:subs数组是何时放入了watcher。后续文章将从这个问题继续解析源码。

猜你喜欢

转载自juejin.im/post/7033306469378293791