vue事件列表和组件更新

151 阅读6分钟

宏人物和微任务

原文

JavaScript是一个单线程的脚本语言。但是却不能单纯的同步执行代码,例如接口调用,在接口调用后不可一直等待接口返回而不处理其他代码。 所以js会有异步的概念,可以告诉主程序,等接收到数据通知我,这样也就分宏任务和微任务

微任务与宏任务的区别

这个就像去银行办业务一样,先要取号进行排号。 一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。 因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。 所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。 任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中,就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号 而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。 所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。 也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币? 无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。 这就说明:你大爷永远是你大爷 在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)

1,2,3,4

也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。

宏任务

I/O setTimeout setInterval setImmediate requestAnimationFrame

微任务

process.nextTick MutationObserver Promise.then catch finally

同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现微任务,遂执行之 setTimeout是下一个宏任务,所以等上一个宏任务以及下面的微任务队列完成后,才会执行

事件队列

原文

在响应式、编译和渲染的过程中,都没有触发过微任务。我们在组件渲染instance.update中,创建了effect,并且传递了options,这个options是告诉响应式数据触发trigger的时候,使用options.scheduler(effect)方法,而不会直接effect()。 trigger中触发effect的方法run:

const prodEffectOptions = {
    scheduler: queueJob
}

const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) { // 渲染processComponent的时候在setupRenderEffect方法中,instance.update = effect(()=>{}, prodEffectOptions)
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

// 当effect被trigger的时候,会执行queueJob(effect) 所以不会立刻执行

function queueFlush() {
  // 避免重复调用flushJobs
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs)
  }
}

const queue: (Job | null)[] = []
export function queueJob(job: Job) {
  // 去重 
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

export function queuePostFlushCb(cb: Function | Function[]) {
  if (!isArray(cb)) {
    postFlushCbs.push(cb)
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush()
}
function queueFlush() {
  // 避免重复调用flushJobs
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs)
  }
}

function flushJobs(seen?: CountMap) {
  debugger
  isFlushPending = false
  isFlushing = true
  let job

  queue.sort((a, b) => getId(a!) - getId(b!)) // 小到大 如果为null 则为Infinate effect的id 从小到大执行 因为effect 嵌套 是大到小被track收集的 BFS就是如此

  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }
  flushPostFlushCbs(seen)
  isFlushing = false

  if (queue.length || postFlushCbs.length) { // 免得在postFlushCbs内部生成 所以再次flushJobs 但是seen会保留
    flushJobs(seen) // 传承Map
  }
}

run->queueJob->queueFlush->flushJobs

nextTick

const p = Promise.resolve()
function nextTick(fn?: () => void): Promise<void> { // 卧槽promise 微任务
  return fn ? P.then(fn) : p
}

nextTickvue 中的更新策略,也是性能优化手段,基于JS执行机制实现

vue 中我们改变数据时不会立即触发视图,如果需要实时获取到最新的DOM,这个时候可以手动调用 nextTick

组件更新:

假设我们有父组件A,子组件B,响应式数据R,A与B的effect均被R追踪到,追踪到的顺序是先A的A-effect,后B的B-effect,R变动,使A-effect和B-effect相继进入queue,queueFlush被推进微任务,执行A的A-update,

A-update创建子组件B的新的B-vnode(只是创建),进行patch,patch检测到旧的B-vnode和新的B-vnode不一致,需要进行更新,删除queue中的B-effect,设置旧B-instance.next为新B-vnode,调用B-effect,此时queue中effect的排列将为先A-effect后B-effect,完成A-update的钩子。

接着执行B-effect,检测到旧B.instance.next,调用updateComponentPreRender,去更新旧的B-instance为新的B-instance,更新B的子vnode(假设B的子vnode为普通Element),完成B-update的钩子。

整个更新就完成了。

组件移除:

假设有响应式数据Z,A的A-effect被Z追踪到,当Z.value为true,A组件的子组件为B,如果为false,A组件的子组件将为C。

现在把Z.value设置为false,触发A-effect,创建新C-vnode,patch(B-vnode, C-vnode),对比发现两个vnode不相等,使用unmount卸载B-vnode

// unmount
const unmount: UnmountFn = (
    vnode, // B-vnode
    parentComponent, // 当前例子是传入A-instance
    parentSuspense, // suspense组件的特性,当前并没有涉及
    doRemove = false // true
  ) => {
    const {
      type,
      props,
      ref,
      children,
      dynamicChildren,
      shapeFlag,
      patchFlag,
      dirs
    } = vnode
    const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
    const shouldKeepAlive = shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
    let vnodeHook: VNodeHook | undefined | null

    // 去除ref 想了解ref去ref章节
    if (ref != null && parentComponent) {
      setRef(ref, null, parentComponent, null)
    }

    // keepAlive 就不执行props.beforeUnMount钩子
    if ((vnodeHook = props && props.onVnodeBeforeUnmount) && !shouldKeepAlive) {
      invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }

    if (shapeFlag & ShapeFlags.COMPONENT) { // 组件相关
      if (shouldKeepAlive) {
        ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
      } else { // 移除组件
        unmountComponent(vnode.component!, parentSuspense, doRemove)
      }
    }
  }

// unmountComponet
const unmountComponent = (
    instance: ComponentInternalInstance,
    parentSuspense: SuspenseBoundary | null,
    doRemove?: boolean
  ) => {
    if (__DEV__ && instance.type.__hmrId) {
      unregisterHMR(instance)
    }

    const { bum, effects, update, subTree, um, da, isDeactivated } = instance
    // beforeUnmount hook
    if (bum) {
      invokeArrayFns(bum)
    }
    if (effects) { // 停止被追踪
      for (let i = 0; i < effects.length; i++) {
        stop(effects[i]) // 可以手动用stop停止某个行为的追踪
      }
    }
    // update may be null if a component is unmounted before its async
    // setup has resolved.
    if (update) {
      stop(update) // updtae 是 effect
      unmount(subTree, instance, parentSuspense, doRemove) // 卸载子组件/其他
    }
    // unmounted hook 钩子 unmounted
    if (um) {
      queuePostRenderEffect(um, parentSuspense)
    }
    // deactivated hook 指令
    if (
      da &&
      !isDeactivated &&
      instance.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
    ) {
      queuePostRenderEffect(da, parentSuspense)
    }
    queuePostRenderEffect(() => {
      instance.isUnmounted = true
    }, parentSuspense)

    // A component with async dep inside a pending suspense is unmounted before
    // its async dep resolves. This should remove the dep from the suspense, and
    // cause the suspense to resolve immediately if that was the last dep.
    if (
      __FEATURE_SUSPENSE__ &&
      parentSuspense &&
      !parentSuspense.isResolved &&
      !parentSuspense.isUnmounted &&
      instance.asyncDep &&
      !instance.asyncResolved
    ) {
      parentSuspense.deps--
      if (parentSuspense.deps === 0) {
        parentSuspense.resolve()
      }
    }
  }