Vue3-Component解析初次加载流程

712 阅读4分钟

Vue3-Component解析初次加载流程

我正在参与掘金创作者训练营第4期(带链接:juejin.cn/post/706419…

创建组件实例(createComponentInstance)

组件实例,设置vue组件的所有通用属性,包括attr,props,provides这些vue组件自身带有的属性,通过对象的形式接收起来。组件内部也赋予了生命周期的命名,目前都是空值。会将父级的provides继续接收到当前组件中provides: parent ? parent.provides : Object.create(appContext.provides),对于props和emits参数,会进行额外的存储起来,通过normalizePropsOptionsnormalizeEmitsOptions这个两个函数,因为组件允许有mixins和extends功能,所以也需要将功能里面这些属性全部聚集起来。

将当前实例加入热更新(registerHMR)

判断当前实例__hmrId属性有没有被加入map(用于记录当前实例是否被收录),如果没有则要set进去。

开始初始化组件实例(setupComponent)

通过判断当前组件的shapeFlag是否是有状态组件(组件分为有状态组件和无状态组件)。

instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT

initProps初始化props

初始化propsattrs,因为attrs将所有的组件上的属性全部收录起来,props只是用户声明的属性;并且将props属性用proxy代理,变成shallowReactive

initSlots初始化插槽

通过对子节点的操作,获取子节点上面的所有slot,并且最终收集到父节点的slots属性上。没有具名的slot放到default这个函数上面;

// 遍历原始插槽
for (const key in rawSlots) {
  if (isInternalKey(key)) continue
  const value = rawSlots[key]
  // 原始的数据格式需要转成特定的格式
  if (isFunction(value)) {
    slots[key] = normalizeSlot(key, value, ctx)
  } else if (value != null) {
    const normalized = normalizeSlotValue(value)
    slots[key] = () => normalized
  }
}

安装有状态组件(setupStatefulComponent)

给组件实例设置accessCache属性用于缓存内部变量,二次访问会直接读取缓存。开始给实例ctx设置公共代理。

// 创建缓存属性
instance.accessCache = Object.create(null)

// 对ctx进行全局代理
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

处理setup属性,创建setup的上下文环境并且给其传入参数:

return {
  get attrs() { // attrs只有读操作
    return attrs || (attrs = createAttrsProxy(instance))
  },
  slots: instance.slots, // 插槽
  emit: instance.emit, // emit参数
  expose // 暴露公共实例属性
}

设置当前组件实例,暂停了对组件的跟踪,执行setup函数,并且对setup内部的进行错误收集。

setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling( // 执行setup函数,报错了就对报错信息收集
  setup,
  instance,
  ErrorCodes.SETUP_FUNCTION,
)
resetTracking()
unsetCurrentInstance()

// callWithErrorHandling
function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn() // 执行setup函数
  } catch (err) {
    handleError(err, instance, type) // 对错误数据进行收集
  }
  return res
}

因为setup可以用async/await语法糖,所以需要进行是否是promise判断方便进行额外操作;setup的返回值refs在模板中访问时是被自动浅解包的instance.setupState = proxyRefs(setupResult)

function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

// 代理函数,进行解包
const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  }
}

最终进入到了finishComponentSetup函数阶段,这里会将组件进行编译,通过调用compile(),将页面的template内容转换成js形式的ast树,最后生成vnode。

抽取出vue中的script内部的所有选项,组成一个函数,方便统一管理。

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance) // 将组件实例的选项拿出来
  const publicThis = instance.proxy! as any
  const ctx = instance.ctx

  // do not cache property access on public proxy during state initialization
  shouldCacheAccess = false

 // 首先执行beforeCreate函数
  if (options.beforeCreate) {
    callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE) // 绑定实例,执行函数
  }
  // 函数内部选项
  const {
    // state
    data: dataOptions,
    computed: computedOptions,
    methods,
    watch: watchOptions,
    provide: provideOptions,
    inject: injectOptions,
    // lifecycle
    created,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
    activated,
    deactivated,
    beforeDestroy,
    beforeUnmount,
    destroyed,
    unmounted,
    render,
    renderTracked,
    renderTriggered,
    errorCaptured,
    serverPrefetch,
    // public API
    expose,
    inheritAttrs,
    // assets
    components,
    directives,
    filters
  } = options
}

Suspense组件额外处理

因为setu()是异步的,Suspense依赖的逻辑有可能是依赖于setup的内容,所以需要提前执行。

parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

设置渲染效果(setupRenderEffect)

创建渲染效果,开始将组件进行反应处理。

const effect = new ReactiveEffect(
  componentUpdateFn, // 组件内部更新处理函数
  () => queueJob(instance.update), // 加入任务工作队列
  instance.scope
)

对组件进行更新处理,进入componentUpdateFn处理方法,对于没有加载过的组件,从组件实例内部拿出bm(beforemounted),执行生命钩子函数。之后通过const subTree = (instance.subTree = renderComponentRoot(instance))将组件生成以组件为根目录的vnode,并且通过patch方法进行打补丁,进行下一步的shapeFlag的细分操作。

后面就进行组件内部的钩子函数m(mounted)操作,当遇到ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE的时候,说明是缓存组件,则会执行activated钩子函数。