美文网首页
Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3侦听器和异步任务调度, 其中有个神秘角色

作者: chonglingliu | 来源:发表于2022-01-19 13:25 被阅读0次

    侦听器的实现逻辑

    我们先来看看一个最简单的使用方式(watch的使用方式非常灵活,我们通过简单的使用方式来了解流程):

    let disabled = ref(false);
    
    let unwatch = watch(disabled, (value, oldValue, oninvalidate) => {
      console.log(oldValue);
      console.log(value);
      nextTick(() => {
          console.log("hoho");
      });
    })
    

    先思考问题:

    1. 参数value是新值,oldValue旧值, 如何实现对disabled进行求值的封装,以及旧值oldValue是如何保存的?
    2. 侦听器也是响应式的API,那disabled的依赖收集和依赖分发是如何实现的(即值的变化是怎么被监听到的)?
    3. unwatch是一个取消监听的函数,内部的实现逻辑是什么?
    对监听数据求值的实现

    逻辑在doWatch方法中:

    所有支持的的watch数据都被封装成了对应的求值函数。

    <!-- doWatch -->
    function doWatch(
      source: WatchSource | WatchSource[] | WatchEffect | object,
      cb: WatchCallback | null,
      { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
    ): WatchStopHandle {
      // 1. 求值函数的封装
      let getter: () => any
      if (isRef(source)) {
        getter = () => source.value
      } else if (isReactive(source)) {
        getter = () => source
      } else if (isArray(source)) {
        // 省略...
      } else if (isFunction(source)) {
        // 省略......
      } else {
        getter = NOOP
      }
    }
    

    函数中有一个oldValue变量,每次求新值后都会保存在它上面,作为下一次求值的旧值。
    第一次监听的时候会调用求新值的函数,这样数据变化后就知道了最开始的值

    <!-- doWatch -->
    let oldValue = isMultiSource ? [] : {}
    
    // 计算新值
    const newValue = effect.run() // 等同于调用getter函数
    oldValue = newValue
    
    数据响应式的实现

    目前只需要看下面的第二段代码,第一段代码后面会介绍

    <!-- doWatch -->
    let scheduler: EffectScheduler
    if (flush === 'sync') {
      scheduler = job as any
    } else if (flush === 'post') {
      scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
    } else {
      // default: 'pre'
      scheduler = () => {
        if (!instance || instance.isMounted) {
          queuePreFlushCb(job)
        } else {
          job()
        }
      }
    }
    
    // 2.
    const effect = new ReactiveEffect(getter, scheduler)
    effect.run()
    
    

    和组件的副作用渲染函数一样,侦听器也是基于ReactiveEffect

    1. ReactiveEffectrun方法执行会调用getter函数,我们的例子中会调用disabledvalue方法从而触发依赖收集,我们的例子中收集的就是effect对象;
    2. 当数据disabled变化后会触发依赖分发,会找到effect对象,执行它里面的scheduler方法,scheduler方法又会调用getter方法计算新值然后返回。这些操作同时进行了又一次收集依赖,等待下一次的数据变化。

    如果对响应式的 依赖收集依赖分发 有疑问的同学可以参考一下其他的文章。

    问题:如果一个数据即被 监听器监听,也被使用在了组件模板中,那 组件的副作用渲染函数监听器函数 哪个会被先执行?

    答案是 监听器函数 ,因为监听器是在setup函数中调用的,所以是先收集的 监听器函数

    取消监听的实现
    <!-- doWatch -->
    return () => {
      effect.stop()
      if (instance && instance.scope) {
        remove(instance.scope.effects!, effect)
      }
    }
    
    1. effect.stop()的主要作用是将effect对象 从 监听数据的 依赖列表中移除,这样监听数据变化后就不会再触发 getter函数了;此外将effect对象置为 未激活,未激活的effect对象也是不能触发getter函数的,所以是双保险;
    2. effect对象从组件作用域中移除;

    任务调度的实现

    在理解任务调度之前我们先来了解一些重要的概念:

    • JS是单线程的,所有的JS代码执行在JS引擎线程中;
    • 浏览器是多线程的,除了JS引擎线程还有UI渲染线程,网络IO线程等;
    • 由于JS执行在一个线程中,所以一次只能执行一个任务,如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务;
    • 同步任务 是指主进程中一个个按顺序执行的任务,如果某个任务执行时间久,那后面的任务就等待执行;
    • 异步任务 先不进入主线程,而先进入任务队列,只有主线程空闲了,且异步任务可以执行了,这些任务才会进入主线程,也是按照先后顺序执行;
    • JS操作DOM是同步任务,浏览器渲染DOM是异步任务(因为js引擎线程GUI渲染线程线程间是互斥的);
    • 异步任务分为 微任务宏任务, 微任务优先执行,所有的微任务执行完成后再执行宏任务
    • 微任务promise等,宏任务setTimeout等;
    触发组件渲染的入口逻辑
    const effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update),
      instance.scope
    )
    
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
    

    前面提到过 组件的副作用渲染函数 是基于ReactiveEffect
    组件模板的数据变化后,会触发() => queueJob(instance.update)函数(本质就是ReactiveEffectrun方法),然后最终会调用componentUpdateFn函数执行组件的挂载或者更新,从而更新DOM。

    • queueJob 的逻辑
    <!-- scheduler.ts -->
    // 组件渲染任务数组
    const queue: SchedulerJob[] = []
    
    // 组件渲染任务队列执行函数
    export function queueJob(job: SchedulerJob) {
      // 省略其他
      if (job.id == null) {
        queue.push(job)
      } else {
        queue.splice(findInsertionIndex(job.id), 0, job)
      }
      queueFlush()
    }
    
    

    Vue 维护了一个queue队列,用于保存需要执行的 副作用渲染函数ReactiveEffectrun方法。
    queueJob 就是将 副作用渲染函数 添加到队列中合适的位置,然后执行 queueFlush方法。

    queueJob

    queueFlush方法我们先忽略,我们回到侦听器的相关逻辑中。

    侦听器的侦测的数据变化后的的逻辑

    我们继续看上面提到的一段代码。

    let scheduler: EffectScheduler
    if (flush === 'sync') {
      scheduler = job as any // the scheduler function gets called directly
    } else if (flush === 'post') {
      scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
    } else {
      // default: 'pre'
      scheduler = () => {
        if (!instance || instance.isMounted) {
          queuePreFlushCb(job)
        } else {
          // with 'pre' option, the first call must happen before
          // the component is mounted so it is called synchronously.
          job()
        }
      }
    }
    

    我们可以通过flush参数来指定侦听器的执行顺序,有sync,postpre(默认) 这三种方式。同步很好理解我们不讨论,我们主要来研究queuePostRenderEffectqueuePreFlushCb这两个方法。

    • queuePostRenderEffect在大多数情况下等同于queuePostFlushCb函数:
    <!-- render.ts -->
    export const queuePostRenderEffect = __FEATURE_SUSPENSE__
      ? queueEffectWithSuspense
      : queuePostFlushCb
    

    所以我们来看看queuePostFlushCbqueuePreFlushCb的逻辑:

    <!-- scheduler.ts -->
    // 更新DOM前的两个callback队列
    const pendingPreFlushCbs: SchedulerJob[] = []
    let activePreFlushCbs: SchedulerJob[] | null = null
    
    // 更新DOM前的两个callback队列
    const pendingPostFlushCbs: SchedulerJob[] = []
    let activePostFlushCbs: SchedulerJob[] | null = null
    
    export function queuePreFlushCb(cb: SchedulerJob) {
      queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
    }
    
    export function queuePostFlushCb(cb: SchedulerJobs) {
      queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
    }
    
    
    function queueCb(
      cb: SchedulerJobs,
      activeQueue: SchedulerJob[] | null,
      pendingQueue: SchedulerJob[],
      index: number
    ) {
      // 省略其他
      pendingQueue.push(cb)
      queueFlush()
    }
    
    1. Vue 维护了DOM更新前需要执行的回调函数执行队列pendingPreFlushCbsactivePreFlushCbs;
    2. Vue 维护了DOM更新后需要执行的回调函数执行队列pendingPostFlushCbsactivePostFlushCbs;
    3. queuePostFlushCbqueuePreFlushCb分别是吧对应的回调方法加到pendingPostFlushCbspendingPreFlushCbs队列中;
    4. 然后执行queueFlush函数。
    `queuePostFlushCb`和`queuePreFlushCb`
    queueFlush执行异步调用

    不管是DOM更新还是监听器的监听到数据后的回调都是进入了queueFlush,我们来看看它的实现逻辑。

    const resolvedPromise: Promise<any> = Promise.resolve()
    
    function queueFlush() {
      if (!isFlushing && !isFlushPending) {
        isFlushPending = true
        currentFlushPromise = resolvedPromise.then(flushJobs)
      }
    }
    

    queueFlush 使用了微任务的Promise执行异步执行flushJobs。且用isFlushPending控制flushJobs的执行时机。

    flushJobs清空所有任务
    function flushJobs(seen?: CountMap) {
    
      isFlushPending = false
      isFlushing = true
        
      // 1. 依次执行所有的所有的回调函数
      flushPreFlushCbs(seen)
    
      // 2. 对副作用渲染函数排序,然后依次执行所有的副作用渲染函数
      queue.sort((a, b) => getId(a) - getId(b))
    
      try {
        for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
          const job = queue[flushIndex]
          callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
        }
      } finally {
        // 3. 依次执行所有的所有的回调函数
        flushPostFlushCbs(seen)
    
        isFlushing = false
        currentFlushPromise = null
        
        // 4. 如果有新的回调函数添加进来,继续一个 1,2,3 的执行流程
        if (
          queue.length ||
          pendingPreFlushCbs.length ||
          pendingPostFlushCbs.length
        ) {
          flushJobs(seen)
        }
      }
    }
    

    flushJobs 的作用是清空所有任务:

    1. flushPreFlushCbs依次清空所有DOM更新前的回调函数:1). 先将pendingPreFlushCbs中的所有数据拷贝到activePreFlushCbs中,pendingPreFlushCbs置空等待新的回调函数加入;2). 依次执行activePreFlushCbs中的回调函数;
    2. callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)依次清空所有更新DOM的副作用渲染函数;
    3. flushPostFlushCbs依次清空所有DOM更新后的回调函数:1). 先将pendingPostFlushCbs中的所有数据拷贝到activePostFlushCbs中,pendingPostFlushCbs置空等待新的回调函数加入;2).依次执行activePostFlushCbs中的回调函数;
    flushJobs

    神秘的nextTick

    nextTick异常神秘,遇到DOM的操作问题可能就想到它了。其实非常简单

    export function nextTick<T = void>(
      this: T,
      fn?: (this: T) => void
    ): Promise<void> {
      const p = currentFlushPromise || resolvedPromise
      return fn ? p.then(this ? fn.bind(this) : fn) : p
    }
    

    异常简单,其实就是对Promise调用then方法。

    Promise.resolve().then(() => {
      // 清空任务(包括更新DOM)
    }).then(() => {
      // 是不是可以获取到更新后的DOM了??
    })
    

    不那么神秘的forceUpdate

    forceUpdate: i => () => queueJob(i.update)
    

    现在也不需要我解释这个方法的作用了。

    相关文章

      网友评论

          本文标题:Vue3侦听器和异步任务调度, 其中有个神秘角色

          本文链接:https://www.haomeiwen.com/subject/tverhrtx.html