Vue3.2x源码解析(二):组件初始化

402 阅读13分钟

系列文章:

本节将深入理解vue3的组件初始化过程。首先接着上节看一下patch方法的内容:

1,组件加载

patch
// packages/runtime-core/src/Renderer.ts
# patch函数,非常重要
const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
   
  // n1: 旧vnode; n2:新vnode
  # 如果新旧vnode相等,直接return,
  if (n1 === n2) {
    return
  }
​
  // patching & not same type, unmount old tree
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
​
  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    n2.dynamicChildren = null
  }
  
  # 从当前新的组件vnode中取出patch类型标记,不同的类型标记执行不同的逻辑
  // 比如常见的组件类型和片段类型
  const { type, ref, shapeFlag } = n2
  switch (type) {
    // 文本
    case Text:
      processText(n1, n2, container, anchor)
      break
    // 注释
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    // 静态节点
    case Static:
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      } else if (__DEV__) {
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    # 片段Fragment;重点,组件创建的subtree为Fragment类型,会走这个分支
    case Fragment:
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    # 默认执行类型
    default:
      // dom元素
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      
      # 组件
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      // vue3内置组件:Teleport
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        ;(type as typeof TeleportImpl).process(
          n1 as TeleportVNode,
          n2 as TeleportVNode,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized,
          internals
        )
      // vue3内置组件:Suspense
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        ;(type as typeof SuspenseImpl).process(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized,
          internals
        )
      // 无效类型
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }
​
  // set ref
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

根据源码可以看出patch函数的作用非常重要:主要就是根据不同的vnode对象类型,执行不同的逻辑处理。所有组件与元素的渲染都需要通过patch函数,执行对应的逻辑加载。

而且也可以看见不仅有常规的组件处理,也有Vue3新增的内置组件Teleport、Suspense处理。

在Vue应用初始化的时候传入的是一个根组件,所以我们继续深入对常规组件的处理逻辑。

查看processComponent源码:

// packages/runtime-core/src/Renderer.ts# 组件进程处理
const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  n2.slotScopeIds = slotScopeIds
  # 旧的vnode为null时,会执行组件加载逻辑,即为第一次组件的初始化
  if (n1 == null) {
    // keep-alive组件处理
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      # 组件挂载,第一次都会走挂载
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else {
    # n1有值时:执行组件更新逻辑
    updateComponent(n1, n2, optimized)
  }
}

根据旧vnode的值是否存在来决定执行:组件加载/组件更新

mountComponent
// packages/runtime-core/src/Renderer.ts
​
# 组件加载
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 2.x compat may pre-create the component instance before actually
  // mounting
  // 兼容2.x版本组件实例
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  # 组件实例 
  const instance: ComponentInternalInstance =
    compatMountInstance ||
    # 创建组件实例 【当前会创建App根组件实例】
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
​
  // inject renderer internals for keepAlive
  if (isKeepAlive(initialVNode)) {
    ;(instance.ctx as KeepAliveContext).renderer = internals
  }
​
  // resolve props and slots for setup context
  if (!(__COMPAT__ && compatMountInstance)) {
    # 调用setup初始化组件
    setupComponent(instance)
  }
​
  // setup() is async. This component relies on async logic to be resolved
  // before proceeding
  if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
    parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
​
    // Give it a placeholder if this is not hydration
    // TODO handle self-defined fallback
    if (!initialVNode.el) {
      const placeholder = (instance.subTree = createVNode(Comment))
      processCommentNode(null, placeholder, container!, anchor)
    }
    return
  }
  
  # 设置组件渲染的renderEffect
  // 类似vue2的renderWatcher
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

注意:COMPAT常量表示对vue2功能的兼容处理。

mountComponent方法里面有三个重点处理逻辑:

  • 创建组件实例。
  • 调用setupComponent初始化组件。
  • 调用setupRenderEffect设置组件渲逻辑。

下面我们逐个分析每个逻辑处理过程:

(一)创建组件实例

createComponentInstance
// packages/runtime-core/src/component.ts
# 创建组件实例
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  // 这个type是最初编译后的组件对象,拥有setup/render函数
  const type = vnode.type as ConcreteComponent
  // inherit parent app context - or - if root, adopt from root vnode
  // 继承父级组件的应用上下文
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  # 创建组件实例对象
  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    root: null!, // to be immediately set
    next: null,
    subTree: null!, # 重点,subtree 存储的是组件调用render渲染函数后生成的虚拟dom树
    effect: null!, # 组件渲染的renderEffect
    update: null!, // will be set synchronously right after creation
    scope: new EffectScope(true /* detached */),
    render: null,
    proxy: null,
    exposed: null,
    exposeProxy: null,
    withProxy: null,
    provides: parent ? parent.provides : Object.create(appContext.provides),
    accessCache: null!,
    renderCache: [],

    // local resolved assets
    components: null, // 子组件列表
    directives: null, // 指令列表

    // resolved props and emits options
        
    # 格式化props/emits
    propsOptions: normalizePropsOptions(type, appContext),
    emitsOptions: normalizeEmitsOptions(type, appContext),

    // emit
    emit: null!, // to be set immediately
    emitted: null,

    // props default value
    propsDefaults: EMPTY_OBJ,

    // inheritAttrs
    inheritAttrs: type.inheritAttrs,

    // state 组件自身状态
    ctx: EMPTY_OBJ,
    data: EMPTY_OBJ,
    props: EMPTY_OBJ,
    attrs: EMPTY_OBJ,
    slots: EMPTY_OBJ,
    refs: EMPTY_OBJ,
    setupState: EMPTY_OBJ,
    setupContext: null,

    // suspense related
    suspense,
    suspenseId: suspense ? suspense.pendingId : 0,
    asyncDep: null,
    asyncResolved: false,

    // lifecycle hooks
    // not using enums here because it results in computed properties
    // 生命周期钩子
    isMounted: false,
    isUnmounted: false,
    isDeactivated: false,
    bc: null,
    c: null,
    bm: null,
    m: null,
    bu: null,
    u: null,
    um: null,
    bum: null,
    da: null,
    a: null,
    rtg: null,
    rtc: null,
    ec: null,
    sp: null
  }
  if (__DEV__) {
    instance.ctx = createDevRenderContext(instance)
  } else {
    instance.ctx = { _: instance }
  }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  // apply custom element special handling
  if (vnode.ce) {
    vnode.ce(instance)
  }

  # 返回组件实例对象
  return instance
}

createComponentInstance方法只有一个作用:创建组件实例对象。可以看见组件实例对象定义了非常多的数据属性,存储了组件需要的各种数据,在这里我们不需要知道每个属性的作用,后面需要的时候再回头来查看。

(二)初始化组件

setupComponent
// packages/runtime-core/src/component.ts# 初始化组件
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  // 判断是否为ssr环境组件初始化
  isInSSRComponentSetup = isSSR
​
  const { props, children } = instance.vnode
  # 判断是否为有状态组件
  const isStateful = isStatefulComponent(instance)
  # 初始化props 
  // 通过传递的真实props和声明的props 分离组件参数
  // 组件参数放入props中 其余放入instance.attrs
  initProps(instance, props, isStateful, isSSR)
  // 初始化插槽 
  initSlots(instance, children)
​
  # 初始化组件: 重点
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
​
  # 返回初始化结果,即组件暴露内容
  return setupResult
}

在初始化组件这里有一个非常重要的逻辑处理:isStatefulComponent(instance)判断当前组件是否为有状态组件。其实在我们的Vue项目中,Vue单文件组件为对象组件,都是有状态组件,因为我们可以在组件内部声明很多的数据即状态State,而一般的无状态组件为函数式组件。

函数式组件只需要初始化propsslots

我们常用的单文件组件即有状态组件需要继续初始化。

setupStatefulComponent
// packages/runtime-core/src/component.ts# 初始化有状态组件
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 组件对象
  const Component = instance.type as ComponentOptions
​
  // 0. create render proxy property access cache
  // 初始化渲染缓存
  instance.accessCache = Object.create(null)
  // 1.初始化组件代理对象【非响应式】
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
      
  // 2. call setup()
  // 从组件中取出setup选项
  const { setup } = Component
  # 存在setup选项
  if (setup) {
    // 创建setup上下文   这个length属性是干啥的?
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)
​
    // 设置为当前实例
    setCurrentInstance(instance)
    // 暂停收集依赖
    pauseTracking()
    # 调用setup, 初始化组件
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    // 恢复依赖收集
    resetTracking()
    // 卸载当前实例
    unsetCurrentInstance()
​
    # setup调用结果处理
    if (isPromise(setupResult)) {
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
      ...
    } else {
      // setup返回值:默认是一个对象,会走else分支
      # handleSetupResult方法作用:暴露值处理
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 不存在setup选项
    finishComponentSetup(instance, isSSR)
  }
}

可以把组件初始化理解为:一个更新数据的过程。我们定义了一些初始数据,然后通过接口查询来更新这些数据,最后执行render渲染函数,render函数会根据组件内最新的数据渲染页面。

这里判断了setup选项的存在,因为**setup选项是组合式 API 的入口**。

注意: <script setup>是编译宏语法糖,最终的内容都会被编译到setup选项。

如果setup选项存在:则说明组件使用的是组合式API,组件的初始化就是调用setup函数

如果setup选项不存在:则直接执行finishComponentSetup方法,开始处理选项式API

扩展;我们都知道使用组合式API,我们定义的响应式数据,计算属性,监听器都在setup选项里,所以这些内容的初始化都是在setup函数执行过程中完成的。对于响应式数据比较简单就是创建对应的proxy代理,了解更多可以查看《Vue3.2x源码解析(三):深入响应式原理》,对于computed和watch监听器的初始化可以看【3,扩展】。

在解析finishComponentSetup之前,我们还得了解一下setupResult的处理:

handleSetupResult(instance, setupResult, isSSR)
handleSetupResult
// packages/runtime-core/src/component.ts# setup返回值处理
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
    
  # 返回值为函数时
  if (isFunction(setupResult)) {
    // setup returned an inline render function
    # setup也可以返回一个渲染函数
    instance.render = setupResult as InternalRenderFunction
    
  } else if (isObject(setupResult)) {
    # 默认返回值是一个对象,这是最常见的
    // 将组件setup的返回数据对象:使用proxyRefs脱ref【其实就是为对象第一层每个属性定义了一个对应ref访问代理】
    // 如果是针对ref数据的处理
    instance.setupState = proxyRefs(setupResult)
  }
  
  # 完成组件初始化
  finishComponentSetup(instance, isSSR)
}

setup选项最常见的返回值就是一个数据对象,对它的处理其实最主要的作用就是:在模板中访问ref类型数据时,自动脱.value。

在这里,setupResult处理完成后,也会调用finishComponentSetup

finishComponentSetup
// packages/runtime-core/src/component.ts
​
# 初始化完成处理
export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  // 保存组件
  const Component = instance.type as ComponentOptions
​
  if (__COMPAT__) {
    convertLegacyRenderFn(instance)
  }
​
  // template / render function normalization
  // could be already set when returned from setup()
  # 如果render函数不存在:开启即时编译,生成render函数
  if (!instance.render) {
    // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
    // is done by server-renderer
    if (!isSSR && compile && !Component.render) {
      const template =
        (__COMPAT__ &&
          instance.vnode.props &&
          instance.vnode.props['inline-template']) ||
        Component.template ||
        resolveMergedOptions(instance).template
      // 如果模板存在,开始模板编译生成render函数
      if (template) {
        if (__DEV__) {
          startMeasure(instance, `compile`)
        }
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const { delimiters, compilerOptions: componentCompilerOptions } = Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        if (__COMPAT__) {
          // pass runtime compat config into the compiler
          finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
          if (Component.compatConfig) {
            // @ts-expect-error types are not compatible
            extend(finalCompilerOptions.compatConfig, Component.compatConfig)
          }
        }
        Component.render = compile(template, finalCompilerOptions)
        if (__DEV__) {
          endMeasure(instance, `compile`)
        }
      }
    }
​
    instance.render = (Component.render || NOOP) as InternalRenderFunction
​
    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (installWithProxy) {
      installWithProxy(instance)
    }
  }
​
  // support for 2.x options
  # 支持vue2的选项式API
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    setCurrentInstance(instance)
    pauseTracking()
    # 初始化选项式API
    # 和vue2组件初始化一样,感兴趣的可以去详细看一下
    applyOptions(instance)
    resetTracking()
    unsetCurrentInstance()
  }
}

finishComponentSetup主要是在组件初始化完成后,对一些边界情况的处理:

  • 没有render情况下,即时编译,生成render渲染函数。
  • 支持vue2的选项式API。【需要处理】

从这里我们也可以看出:

  • setup选项是在所有选项式API初始化之前执行的,所以setup无法使用选项式API的内容,
  • 但是选项式API可以使用setup返回的数据。

以上就是Vue3组件初始化的过程:初始化核心就是setup选项的调用及结果处理。其实比vue2的选项式API的初始化更简单。

组件初始化完成后,下面开始组件的渲染逻辑解析。

(三)渲染组件

setupRenderEffect

前面在mountComponent函数最后调用了一个setupRenderEffect方法,这个方法的作用是:设置组件渲染的renderEffect,即确定组件具体的挂载与更新逻辑。

# 初始化组件渲染的effect
setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )

我们继续查看setupRenderEffect源码:

// packages/runtime-core/src/renderer.ts# 初始化组件渲染的effect
const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
    
  # 1,定义了一个组件挂载/更新的钩子函数
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
        // 挂载组件
        ...
    } else {
       // 更新组件
       ...
    }
  }
​
  // create reactive effect for rendering
  # 2,创建组件渲染的renderEffect【类似于vue2的renderWatcher】
  const effect = (instance.effect = new ReactiveEffect(
    // 传入的回调为组件更新fn
    componentUpdateFn,
    // 调度函数
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))
  
  # 3,定义组件更新的调度任务
  const update: SchedulerJob = (instance.update = () => effect.run())
  // 任务id为组件实例id
  update.id = instance.uid
  // allowRecurse
  // #1801, #2043 component render effects should allow recursive updates
  toggleRecurse(instance, true)
​
  # 4,执行update方法: 即第一次组件的挂载
  update()
}

setupRenderEffect方法里面的内容比较多,其中大部分的代码都是componentUpdateFn函数的内容,从这个函数的名字就可以看出它是专门用于处理组件更新的方法,但是我们在这里先不看它的具体内容,先折叠。

// 先折叠
const componentUpdateFn = () => {...}

我们先折叠这个方法的内容,然后再看setupRenderEffect的逻辑就非常简单了,可以划分为四点内容:

  • 定义了一个组件挂载/更新的钩子函数。
  • 创建组件渲染的renderEffect。
  • 定义更新的调度任务。
  • 执行更新任务【第一次挂载】。

我们先看创建组件渲染的Effect,这个明显和vue2创建组件的renderWatcher写法一致,所以我们也很容易它的功能就是负责组件渲染。在组件内容变化后,就会触发effect.fn钩子函数即componentUpdateFn的调用,重新渲染组件:

// 关于ReactiveEffect的详细内容会响应式章节里面介绍
const effect = (instance.effect = new ReactiveEffect(
    // 传入的回调为组件更新fn
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
))

然后我们再看看这个更新方法的定义:

# 更新方法
const update: SchedulerJob = (instance.update = () => effect.run())

这里定义了一个update方法,同时也将这个方法存储到了instance.update属性中:

# 两个方法的用途区别:
// 第一次组件加载使用
const update: SchedulerJob = () => effect.run()
// 后续的组件更新使用
instance.update = () => effect.run()

第一次组件加载:

# 执行组件挂载
update();

最后我们再回头来看看componentUpdateFn钩子函数:

componentUpdateFn
# 处理组件挂载与更新的钩子函数
const componentUpdateFn = () => {
  if (!instance.isMounted) {
     # 挂载组件逻辑
    let vnodeHook: VNodeHook | null | undefined
    const { el, props } = initialVNode
    const { bm, m, parent } = instance
    const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
​
    toggleRecurse(instance, false)
    // beforeMount hook
    # 触发beforeMount钩子函数
    if (bm) {
      // 组合式API的生命周期钩子函数可以多次调用,所以是以数组方式处理
      invokeArrayFns(bm)
    }
    // onVnodeBeforeMount
    if (
      !isAsyncWrapperVNode &&
      (vnodeHook = props && props.onVnodeBeforeMount)
    ) {
      invokeVNodeHook(vnodeHook, parent, initialVNode)
    }
    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
    ) {
      // 也会触发选项式生命周期钩子 beforeMount
      instance.emit('hook:beforeMount')
    }
    toggleRecurse(instance, true)
    
    # 开始渲染,直接看patch
    if (el && hydrateNode) {
      // vnode has adopted host node - perform hydration instead of mount.
      // ssr相关省略...
    } else {
      # 重点:非常重要,renderComponentRoot是调用组件的render渲染函数,生成虚拟dom树
      // 同时将虚拟dom存储到组件实例的subTree属性上,用于组件更新时做差异比较
      const subTree = (instance.subTree = renderComponentRoot(instance))
      # 这里再次调用patch,子组件递归渲染
      patch(
        null,
        subTree,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG
      )
      initialVNode.el = subTree.el
    }
      
    // mounted hook
    # 渲染完成:触发mounted钩子函数
    # mounted钩子函数并非同步执行的,而是执行queuePostRenderEffect方法,将mounted钩子回调传入到了一个任务队列,异步执行
    # updated同理
    if (m) {
      queuePostRenderEffect(m, parentSuspense)
    }
    // onVnodeMounted
    if (
      !isAsyncWrapperVNode &&
      (vnodeHook = props && props.onVnodeMounted)
    ) {
      const scopedInitialVNode = initialVNode
      queuePostRenderEffect(
        () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
        parentSuspense
      )
    }
    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
    ) {
      queuePostRenderEffect(
        () => instance.emit('hook:mounted'),
        parentSuspense
      )
    }
​
    // activated hook for keep-alive roots.
    // #1742 activated hook must be accessed after first render
    // since the hook may be injected by a child keep-alive
      
    # 触发activated钩子函数
    if (
      initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
      (parent &&
        isAsyncWrapper(parent.vnode) &&
        parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
    ) {
      instance.a && queuePostRenderEffect(instance.a, parentSuspense)
      if (
        __COMPAT__ &&
        isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
      ) {
        queuePostRenderEffect(
          () => instance.emit('hook:activated'),
          parentSuspense
        )
      }
    }
    # 设置组件状态为已挂载
    instance.isMounted = true
​
    // #2458: deference mount-only object parameters to prevent memleaks
    // 清空数据,以防内存泄漏
    initialVNode = container = anchor = null as any
  } else {
​
    # 更新组件逻辑
    # 有两种情况:组件自身数据更改触发/父组件调用processComponent进程方法
    // updateComponent
    // This is triggered by mutation of component's own state (next: null)
    // OR parent calling processComponent (next: VNode)
    let { next, bu, u, parent, vnode } = instance
    let originNext = next
    let vnodeHook: VNodeHook | null | undefined
​
    // Disallow component effect recursion during pre-lifecycle hooks.
    toggleRecurse(instance, false)
    if (next) {
      # 父组件引起的更新,会在子组件更新之前,更新props/solts以及冲刷pre队列
      next.el = vnode.el
      updateComponentPreRender(instance, next, optimized)
    } else {
     // 组件自身变化的更新
      next = vnode
    }
​
    // beforeUpdate hook
    # 触发beforeUpdate钩子函数
    if (bu) {
      invokeArrayFns(bu)
    }
    // onVnodeBeforeUpdate
    if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
      invokeVNodeHook(vnodeHook, parent, next, vnode)
    }
    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
    ) {
      // 触发选项式生命周期钩子
      instance.emit('hook:beforeUpdate')
    }
    toggleRecurse(instance, true)
​
    // render
    # 渲染组件
    # 组件更新时:调用组件的render渲染函数,生成最新的虚拟dom树
    const nextTree = renderComponentRoot(instance)
    // 取出旧的虚拟dom树
    const prevTree = instance.subTree
    // 同时保存新的tree
    instance.subTree = nextTree
    # 开始更新渲染,会做差异比较
    patch(
      prevTree,
      nextTree,
      // parent may have changed if it's in a teleport
      hostParentNode(prevTree.el!)!,
      // anchor may have changed if it's in a fragment
      getNextHostNode(prevTree),
      instance,
      parentSuspense,
      isSVG
    )
​
    next.el = nextTree.el
    if (originNext === null) {
      // self-triggered update. In case of HOC, update parent component
      // vnode el. HOC is indicated by parent instance's subTree pointing
      // to child component's vnode
      updateHOCHostEl(instance, nextTree.el)
    }
    
    // updated hook
    # 触发updated钩子函数 :更新完成
    if (u) {
      queuePostRenderEffect(u, parentSuspense)
    }
    // onVnodeUpdated
    if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
      queuePostRenderEffect(
        () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
        parentSuspense
      )
    }
    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
    ) {
      queuePostRenderEffect(
        () => instance.emit('hook:updated'),
        parentSuspense
      )
    }
  }
}

这里以instance.isMounted属性值进行判断,组件是否已经挂载。未挂载执行挂载逻辑,已挂载执行更新逻辑。

在创建组件实例的时候,组件实例的isMounted属性默认为false,即未挂载状态,所以上面第一次触发componentUpdateFn函数的时候会执行组件的挂载逻辑,组件的渲染核心是调用patch函数,所以组件的渲染实际上:会重复执行前面我们解析的组件加载逻辑。从根组件开始一直递归渲染整个组件树,直到整个应用加载完成。

componentUpdateFn函数中有两个最重要的逻辑处理:

  1. 调用renderComponentRoot(instance)生成虚拟dom树。
  2. 执行patch函数,使用虚拟dom渲染组件。

patch函数前面我们已经有了一定的了解,这里我们主要看renderComponentRoot方法,这个方法非常重要,它接收的参数是一个组件实例,这个方法内部最重要的作用就是调用了传入的这个组件的render渲染函数,生成了虚拟dom树并返回,然后patch函数才能根据这些虚拟dom开始具体的渲染。

下面查看renderComponentRoot源码:

# 调用组件的render渲染函数
export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
    
  # 取出组件上的数据
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    props,
    propsOptions: [propsOptions],
    slots,
    attrs, // 属性
    emit,
    render, // 组件的render
    renderCache,
    data,
    setupState,
    ctx,
    inheritAttrs
  } = instance
​
  # 存储渲染虚拟dom树
  let result
  let fallthroughAttrs
  // prev 是什么
  const prev = setCurrentRenderingInstance(instance)
  if (__DEV__) {
    accessedAttrs = false
  }
​
  try {
    # 有状态组件,
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      // withProxy is a proxy with a different `has` trap only for
      // runtime-compiled render functions using `with` block.
      const proxyToUse = withProxy || proxy
      // 格式化vnode
      result = normalizeVNode(
        # 重点:调用组件的render渲染函数,生成虚拟dom树
        render!.call(
          proxyToUse,
          proxyToUse!,
          renderCache,
          props,
          setupState,
          data,
          ctx
        )
      )
      fallthroughAttrs = attrs
    } else {
      // functional
      # 函数式组件
      const render = Component as FunctionalComponent
      // in dev, mark attrs accessed if optional props (attrs === props)
      if (__DEV__ && attrs === props) {
        markAttrsAccessed()
      }
      result = normalizeVNode(
        render.length > 1
          ? render(
              props,
              __DEV__
                ? {
                    get attrs() {
                      markAttrsAccessed()
                      return attrs
                    },
                    slots,
                    emit
                  }
                : { attrs, slots, emit }
            )
          : render(props, null as any /* we know it doesn't need it */)
      )
      fallthroughAttrs = Component.props
        ? attrs
        : getFunctionalFallthrough(attrs)
    }
  } catch (err) {
    blockStack.length = 0
    handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
    result = createVNode(Comment)
  }
​
  // attr merging
  // in dev mode, comments are preserved, and it's possible for a template
  // to have comments along side the root element which makes it a fragment
  // 存储虚拟dom树
  let root = result
  let setRoot: SetRootFn = undefined
  if (
    __DEV__ &&
    result.patchFlag > 0 &&
    result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
  ) {
    ;[root, setRoot] = getChildRoot(result)
  }
​
  if (fallthroughAttrs && inheritAttrs !== false) {
    const keys = Object.keys(fallthroughAttrs)
    const { shapeFlag } = root
    if (keys.length) {
      if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {
        if (propsOptions && keys.some(isModelListener)) {
          // If a v-model listener (onUpdate:xxx) has a corresponding declared
          // prop, it indicates this component expects to handle v-model and
          // it should not fallthrough.
          // related: #1543, #1643, #1989
          fallthroughAttrs = filterModelListeners(
            fallthroughAttrs,
            propsOptions
          )
        }
        root = cloneVNode(root, fallthroughAttrs)
      } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
        const allAttrs = Object.keys(attrs)
        const eventAttrs: string[] = []
        const extraAttrs: string[] = []
        for (let i = 0, l = allAttrs.length; i < l; i++) {
          const key = allAttrs[i]
          if (isOn(key)) {
            // ignore v-model handlers when they fail to fallthrough
            if (!isModelListener(key)) {
              // remove `on`, lowercase first letter to reflect event casing
              // accurately
              eventAttrs.push(key[2].toLowerCase() + key.slice(3))
            }
          } else {
            extraAttrs.push(key)
          }
        }
        if (extraAttrs.length) {
          warn(
            `Extraneous non-props attributes (` +
              `${extraAttrs.join(', ')}) ` +
              `were passed to component but could not be automatically inherited ` +
              `because component renders fragment or text root nodes.`
          )
        }
        if (eventAttrs.length) {
          warn(
            `Extraneous non-emits event listeners (` +
              `${eventAttrs.join(', ')}) ` +
              `were passed to component but could not be automatically inherited ` +
              `because component renders fragment or text root nodes. ` +
              `If the listener is intended to be a component custom event listener only, ` +
              `declare it using the "emits" option.`
          )
        }
      }
    }
  }
​
  if (
    __COMPAT__ &&
    isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&
    vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
    root.shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)
  ) {
    const { class: cls, style } = vnode.props || {}
    if (cls || style) {
      if (__DEV__ && inheritAttrs === false) {
        warnDeprecation(
          DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
          instance,
          getComponentName(instance.type)
        )
      }
      root = cloneVNode(root, {
        class: cls,
        style: style
      })
    }
  }
​
  // inherit directives
  if (vnode.dirs) {
    if (__DEV__ && !isElementRoot(root)) {
      warn(
        `Runtime directive used on component with non-element root node. ` +
          `The directives will not function as intended.`
      )
    }
    // clone before mutating since the root may be a hoisted vnode
    root = cloneVNode(root)
    root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
  }
  // inherit transition data
  if (vnode.transition) {
    if (__DEV__ && !isElementRoot(root)) {
      warn(
        `Component inside <Transition> renders non-element root node ` +
          `that cannot be animated.`
      )
    }
    root.transition = vnode.transition
  }
​
  if (__DEV__ && setRoot) {
    setRoot(root)
  } else {
    result = root
  }
​
  setCurrentRenderingInstance(prev)
  return result
}

可以看出renderComponentRoot方法里面的内容也是比较多,虽然我们不能把每一行代码都理解清楚,但是我们只需要拆解出最核心的内容:

result = normalizeVNode(
   # 核心:调用组件的render渲染函数,生成虚拟dom树
   render!.call(
      proxyToUse,
      proxyToUse!,
      renderCache,
      props,
      setupState,
      data,
      ctx
   )
)
return result

renderComponentRoot方法最核心的内容就是:取出了组件的render函数,然后调用render渲染函数生成了该组件的虚拟dom树,最后对vnode做一些格式上的处理并返回。

然后patch函数使用最新生成subTree即虚拟dom树来渲染该组件的内容。

如果组件内还有子组件内容,就会继续循环上面的组件初始化加载过程。Vue3每一个应用的加载,都是以根组件的vnode对象执行patch函数开始,进行整个组件树的递归渲染,直到最终的应用渲染完成。

2,组件更新

关于组件的加载前面已经解析的比较清楚了,其实组件的更新的内容比较简单,我们首先回到processComponent里面:

const processComponent = () {
    if (n1 == null) { 
        // 挂载组件
    } else {
        # n1有值时:执行组件更新逻辑
        updateComponent(n1, n2, optimized)
    }
}
updateComponent

继续查看updateComponent

// packages/runtime-core/src/Renderer.ts# 更新组件
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!
  if (shouldUpdateComponent(n1, n2, optimized)) {
      // normal update
      # 正常更新逻辑
      instance.next = n2
      // in case the child component is also queued, remove it to avoid
      // double updating the same child component in the same flush.
      invalidateJob(instance.update)
      // instance.update is the reactive effect.
      # 重点:调用组件更新方法
      instance.update()
    }
  } else {
    // no update needed. just copy over properties
    n2.el = n1.el
    instance.vnode = n2
  }
}

更新组件就是调用组件的更新方法:

instance.update()

通过前面我们已经知道,组件在第一次挂载时就会确定update更新方法。

instance.update = () => effect.run()
// run方法就是执行effect实例上的fn回调函数
run() {
   ... 
   return this.fn()
}

最终执行的就是renderEffect实例上的fn回调函数即:componentUpdateFn钩子函数。

Vue3组件初始化的内容就解析到这里了,下节我们开始深入理解Vue3的响应式原理。

3,扩展

computed

计算属性:接受一个 getter 函数,返回一个只读的响应式ref对象

在了解computed函数之前,我们先看看计算属性类ComputedRefImpl的定义:

// packages/reactivity/src/computed.ts# 计算属性Class
export class ComputedRefImpl<T> {
  // 依赖容器
  public dep?: Dep = undefined
  // 值
  private _value!: T
  # computedEffect 计算属性effect
  public readonly effect: ReactiveEffect<T>
  # 计算属性也是ref类型数据
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false
  // 脏检测:重新计算
  public _dirty = true
  // 缓存
  public _cacheable: boolean
​
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    
    # 创建属于计算属性的effect实例
    // getter就是传入的fn,最终执行的回调
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        // 触发依赖
        triggerRefValue(this)
      }
    })
    // 重要:定义effect实例的computed属性:等于自身计算属性实例
    this.effect.computed = this
    # 非ssr环境的计算属性:设置effect为激活状态,设置计算属性可缓存
    this.effect.active = this._cacheable = !isSSR
    // 设置只读
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  
  # 和ref一样:定义了一个value访问器属性
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    // 收集依赖
    trackRefValue(self)
    # 每次获取计算属性值时,都会判断_dirty属性值的变化,来决定是否要重新调用getter计算最新的值
    // dirty为true的时候,需要重新计算getter。在第一次执行后,就会将dirty设置为false,后续再访问get就不会再次执行计算
    // 需要有内部的依赖变量变化,触发调度程序更新,才会重新计算,重置dirty为true
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }
    
  set value(newValue: T) {
    this._setter(newValue)
  }
}

根据ComputedRefImpl的定义,可以看见计算属性实例也是ref类型数据:

__v_isRef = true

同时计算属性实例和ref实例非常类似,都是围绕一个访问器属性value进行逻辑处理。

并且计算属性有一个effect属性,这个属性值是一个effect实例对象,每个计算属性创建时都会绑定一个computedEffect实例,这个effect实例对象就是专门作用于计算属性的结果更新以及触发依赖。

了解了computed对象的定义,继续查看computed源码:

// packages/reactivity/src/computed.ts# 组合式API:创建计算属性
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
​
  # 如果传入的是Fn,那就是只有getter,否则就是对象
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    // 直接把函数赋值给getter
    getter = getterOrOptions
      
  } else {
    // 对象类型:设置对应的get/set
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  # 重点; 创建一个ComputedRefImpl计算属性实例
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
​
  // 返回计算属性实例
  return cRef as any
}

可以看见computed方法源码非常简洁,就是通过我们传入的getter,创建并返回了一个计算属性实例对象。

watch

在Vue3中有以下几个侦听器API:watchwatchEffectwatchSyncEffectwatchPostEffect

虽然方法比原来多了几个,但是其思想还是和Vue2基本一致,拆分成多个API 也是方便用户使用。并且这几个方法都是基于doWatch函数实现,我们这里以watch为例展开分析。

// packages/runtime-core/src/apiWatch.ts
​
# 组合式API:创建监听器
function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
​
  return doWatch(source as any, cb, options)
}

继续查看doWatch源码:

# 监听器核心实现
function doWatch(
  // 监听源
  source: WatchSource | WatchSource[] | WatchEffect | object,
  // 回调函数
  cb: WatchCallback | null,
  // 配置对象
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
 
  # 无效的参数警告
  const warnInvalidSource = (s: unknown) => {
    warn(
      `Invalid watch source: `,
      s,
      `A watch source can only be a getter/effect function, a ref, ` +
        `a reactive object, or an array of these types.`
    )
  }
​
  const instance = currentInstance
  // 重点:getter回调函数
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false
 
  if (isRef(source)) {
    # 对ref类型数据做处理
    // 脱ref
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    # 对reactive响应式对象数据做处理,会默认进行深度监听
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    # 对数组进行处理
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) {
    # 对函数类型进行处理:
    if (cb) {
      # getter with cb 使用的是watch方法
      getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect 使用的是watchEffect类方法
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }
​
  // 2.x array mutation watch compat
  # 2.x版本兼容
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }
​
  if (cb && deep) {
    // 深度监听
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
​
  // 清除函数:
  let cleanup: () => void
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }
​
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  
  # 重点1:定义了一个监听器的任务job
  const job: SchedulerJob = () => {
    // 如果effect没有被激活,这个任务不会执行任何逻辑,effect默认是激活有效状态
    if (!effect.active) {
      return
    }
    # 重点: 回调函数存在,即使用的是watch方法,调度任务的作用是执行getter及cb
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE 
            ? undefined
            : (isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE)
              ? []
              : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      # 回调函数不存在,则使用的watchEffect类方法,作用是调用run执行getter
      effect.run()
    }
  }
​
  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb
  
  # 重点2:定义了一个调度程序
  let scheduler: EffectScheduler
  # 根据flush值对调度程序进行赋值===
  if (flush === 'sync') {
    # 同步立即执行 watchSyncEffect
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    # 将任务推入到post队列,等待组件更新后执行 watchPostEffect
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    # 默认为pre:即组件更新之前执行:watch/watchEffect
    job.pre = true
    // job.id为自身组件实例id
    if (instance) job.id = instance.uid
    # 添加job任务到queue队列
    scheduler = () => queueJob(job)
  }
  # 重点;创建属于监听器的watchEffect实例
  const effect = new ReactiveEffect(getter, scheduler)
​
  // initial run
  # 几种API的不同初始化执行
  if (cb) {
    // cb存在,即使用的watch方法时,必须immediate为true,才会在watch初始化的时候,立即执行一次回调即job任务
    if (immediate) {
      job()
    } else {
      // 默认只是执行一次getter,获取旧的value
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    // 使用的时watchPostEffect方法,立即执行一次,即使用queuePostFlushCb方法将任务推送到post队列
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    // 使用的是watchEffect/watchSyncEffect,立即同步执行一次getter回调
    effect.run()
  }
    
  // 取消监听函数
  const unwatch = () => {
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
​
  return unwatch
}

可以看见doWatch的源码非常多,因为他是几个监听器API的核心实现,watchwatchEffectwatchPostEffectwatchSyncEffect都来依赖于doWatch函数,所以理解这个方法是理解Vue3监听器实现的重点。

虽然doWatch的源码非常多,但是我们可以将他的内容分成几个小点,然后我们一点一点进行分析:

  • 对传入的监听源source进行类型判断,执行不同的初始化。
  • 定义了一个监听器的任务job。
  • 定义了一个调度程序scheduler。
  • 创建了一个属于监听器的watchEffect实例。
  • 监听器回调的初始化执行。

首先看创建的watchEffect实例:

// ReactiveEffect(fn, scheduler, scope?)
const effect = new ReactiveEffect(getter, scheduler)

所以这里的getter就是fn回调函数,scheduler是调度程序

重点: Vue3中只有两类监听器API:watch和watchEffect。一定要明确这样的理解,watchPostEffectwatchSyncEffect只是watchEffect的派生而已。

所以,getter也有两类:

  • 为watch时,getter重点是对值的处理,cb才是我们需要执行的回调逻辑。
handler(val, oldval)
  • 当为watchEffect时,getter就是我们需要执行的回调,重点是回调函数的逻辑执行。

然后我们再看调度程序scheduler:

  let scheduler: EffectScheduler
  # 根据flush值对调度程序进行赋值===
  if (flush === 'sync') {
    # 同步立即执行 watchSyncEffect
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    # 将任务推入到post队列,等待组件更新后执行 watchPostEffect
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    # 默认为pre:即组件更新之前执行 watch/watchEffect
    job.pre = true
    // job.id为自身组件实例id
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
  }

重点: 调度程序在Vue3中非常重要,他决定了job任务是按哪种方式执行的。

在这里调度程序的设置由flush值决定:

  • sync:表明当前使用方法为watchSyncEffect
# 执行调度方法即会同步执行Job任务
scheduler = job
  • post:表明当前使用方法为watchPostEffect
# 执行调度程序会:将job任务推送到post队列,job任务将会在组件更新后执行
scheduler = () => queuePostRenderEffect(job)
  • pre(默认):表明当前使用方法为watch/watchEffect
# 执行调度程序会:将job任务推送到pre队列,job任务将会在组件更新之前执行
job.pre = true
scheduler = () => queueJob(job)

我们再继续查看job任务,因为执行调度程序最终的目的是处理job任务。

# job任务
const job: SchedulerJob = () => {
    // 如果effect没有被激活,这个任务不会执行任何逻辑
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      # 回调函数存在,表明使用的是watch方法
      // 1,执行getter,获取新值,重点是值的处理
      const newValue = effect.run()
      ...
      // 2,执行cb回调函数,重点是cb函数的执行
      allWithAsyncErrorHandling(cb, instance, ...)
      ...
      oldValue = newValue
    } else {
      // watchEffect
      # 回调函数不存在,则使用的watchEffect类方法
      // 1,只需要执行getter
      effect.run()
    }
  }

我们可以看见job任务中最核心就是执行了effect.run(),这个effect实例就是:前面创建的watchEffect实例对象。 而effect的run方法内部的重点也是为了执行fn回调函数:

# 这个fn回调函数就是我们之前传入的getter
run() {
    ...
    return this.fn()
}

而根据job任务中cb回调函数的存在判断,还是分成了两类逻辑:

  • cb存在:为watch,执行job任务同时执行了getter和cb回调函数。
  • cb不存在:为watchEffect类,执行job任务只执行了getter【回调函数】。

解析到这里,我相信大家对Vue3的侦听器已经有了明确的认知。

最后我们再说一下侦听器的立即执行,即在watch初始化时立即执行一次回调:

  // initial run
  # 初始化执行
  if (cb) {
    // cb存在,即使用的watch方法时,必须immediate为true,才会在watch初始化的时候,立即执行一次回调即job任务
    if (immediate) {
      job()
    } else {
      // 默认只是执行一次getter,获取旧的value
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    // 使用的时watchPostEffect方法,立即执行一次,即使用queuePostFlushCb方法将任务推送到post队列
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    // 使用的是watchEffect/watchSyncEffect,立即同步执行一次回调
    effect.run()
  }
  • cb存在时:表明当前使用方法为watch,如果配置了immediate:true,就需要立即执行一次job任务,即执行getter和cb;如果没有配置,则只需要执行getter。
  • cb不存在时:表明使用的是watchEffect类,因为watchEffect类的监听器初始化就需要立即执行一次回调。
#这其中又分为两种情况:
// 1,flush === 'post'时:表明使用的是watchPostEffect,执行queuePostFlushCb方法将任务推送到post队列
// 2,其他情况else分支时:表明使用的时watchEffect和watchSyncEffect,立即同步执行一次getter回调

组件的初始化解析就到这里,下节我们解析Vue3的响应式原理。