在Vue 3.0的使用中我们可以不使用data
、props
、methods
、computed
等Option函数,可以只下在setup
函数中进行编写代码逻辑。当然为了和Vue 2.0兼容,也可以继续使用Option函数。
先提出两个个问题:
-
setup
函数的执行时机是什么? -
setup
函数的返回结果为何与模板的渲染建立联系的?
mountComponent
挂载组件
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 1.
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 2.
setupComponent(instance)
// 3.
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
挂载组件分为三个步骤:
- 创建组件实例
- 设置组件实例
- 创建带副作用的渲染函数
我们本文分析的主角setup
函数是在第二个阶段进行处理的,但是鉴于这三个步骤具有关联性,我们也会将第一个和第三个步骤进行和第二个步骤的关联进行分析。
createComponentInstance
创建组件实例
- 我们先来看看组件实例接口
ComponentInternalInstance
定义的一些属性的含义:
export interface ComponentInternalInstance {
// 组件的id
uid: number
// 组件类型(options对象 或者 函数)
type: ConcreteComponent
// 父组件实例
parent: ComponentInternalInstance | null
// 根组件实例
root: ComponentInternalInstance
// app上下文
appContext: AppContext
// 组件VNode
vnode: VNode
// 要更新到的VNode
next: VNode | null
// 子树VNode
subTree: VNode
// 带副作用的渲染函数
update: SchedulerJob
// 渲染函数
render: InternalRenderFunction | null
// SSR渲染函数
ssrRender?: Function | null
// 依赖注入的数据
provides: Data
// 收集响应式依赖的作用域
scope: EffectScope
// 读取proxy属性值后的缓存
accessCache: Data | null
// 渲染缓存
renderCache: (Function | VNode)[]
// 注册的组件
components: Record<string, ConcreteComponent> | null
// 注册的指令
directives: Record<string, Directive> | null
// 过滤器
filters?: Record<string, Function>
// props
propsOptions: NormalizedPropsOptions
// emits
emitsOptions: ObjectEmitsOptions | null
// attrs
inheritAttrs?: boolean
// 是否是自定义组件(custom element)
isCE?: boolean
// 自定义组件(custom element)相关方法
ceReload?: () => void
// the rest are only for stateful components ---------------------------------
// 渲染上下文代理对象,当使用`this`时就是指的这个对象
proxy: ComponentPublicInstance | null
// 组件暴露的对象
exposed: Record<string, any> | null
// 组件暴露对象的代理对象
exposeProxy: Record<string, any> | null
// 带有with区块(block)的渲染上下文代理对象
withProxy: ComponentPublicInstance | null
// 渲染上下文---即组件对象的信息 { _: instance }
ctx: Data
// data数据
data: Data
// props数据
props: Data
// attrs数据
attrs: Data
// slot数据
slots: InternalSlots
// 组件或者DOM的ref引用
refs: Data
// emit函数
emit: EmitFn
// 记录被v-once修饰已经触发的事件
emitted: Record<string, boolean> | null
// 工厂函数生成的默认props数据
propsDefaults: Data
// setup函数返回的响应式结果
setupState: Data
// setup函数上下文数据
setupContext: SetupContext | null
// 异步组件
suspense: SuspenseBoundary | null
// 异步组件ID
suspenseId: number
// setup函数返回的异步函数结果
asyncDep: Promise<any> | null
// 异步函数调用已完成
asyncResolved: boolean
// 是否已挂载
isMounted: boolean
// 是否已卸载
isUnmounted: boolean
// 是否已去激活
isDeactivated: boolean
// 各种钩子函数
// bc
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
// c
[LifecycleHooks.CREATED]: LifecycleHook
// bm
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
// m
[LifecycleHooks.MOUNTED]: LifecycleHook
// bu
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
// u
[LifecycleHooks.UPDATED]: LifecycleHook
// bum
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
// um
[LifecycleHooks.UNMOUNTED]: LifecycleHook
// rtc
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
// rtg
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
// a
[LifecycleHooks.ACTIVATED]: LifecycleHook
// da
[LifecycleHooks.DEACTIVATED]: LifecycleHook
// ec
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
// sp
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
}
- 我们接下来看看创建组件实例时主要设置了哪些属性值:
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as ConcreteComponent
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
provides: parent ? parent.provides : Object.create(appContext.provides),
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),
inheritAttrs: type.inheritAttrs,
// 省略...
}
instance.ctx = { _: instance }
instance.root = parent ? parent.root : instance
return instance
}
我们看到创建组件实例的时候主要设置了uid
、vnode
、appContext
、provides
、propsOptions
、emitsOptions
和ctx
等这些属性。
setupComponent
设置组件实例流程
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
// 1.
const { props, children } = instance.vnode
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
// 2.
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
return setupResult
}
该方法主要有两个步骤:
- 从
vnode
中获得到一些属性,然后初始化props
和slots
(后续章节介绍);- 调用
setupStatefulComponent
方法设置有状态组件实例(本文分析的主要内容)。
setupStatefulComponent
方法
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 1. create render proxy property access cache
instance.accessCache = Object.create(null)
// 2. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
const { setup } = Component
if (setup) {
// 3. call setup()
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 4.
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[instance.props, setupContext]
)
// 5.
handleSetupResult(instance, setupResult, isSSR)
} else {
// 6.
finishComponentSetup(instance, isSSR)
}
}
首先初始化了一个
accessCache
对象,用来缓存查找ctx
后得到的值,避免重复查找ctx
中的属性。
后面的每步我们分开来说明。建立了一个
ctx
的代理对象proxy
, 当访问或者修改proxy
的属性时会触发PublicInstanceProxyHandlers
方法,而此方法的操作对象是ctx
。
这里先提出一个问题:为什么设置代理?
我们来看看PublicInstanceProxyHandlers
方法:
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } =
instance
let normalizedProps
if (key[0] !== '$') {
const n = accessCache![key]
if (n !== undefined) {
switch (n) {
case AccessTypes.SETUP:
return setupState[key]
case AccessTypes.DATA:
return data[key]
case AccessTypes.CONTEXT:
return ctx[key]
case AccessTypes.PROPS:
return props![key]
}
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache![key] = AccessTypes.SETUP
return setupState[key]
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache![key] = AccessTypes.DATA
return data[key]
} else if (
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
accessCache![key] = AccessTypes.PROPS
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
accessCache![key] = AccessTypes.OTHER
}
}
const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// public $xxx properties
if (publicGetter) {
if (key === '$attrs') {
track(instance, TrackOpTypes.GET, key)
__DEV__ && markAttrsAccessed()
}
return publicGetter(instance)
} else if (
// css module (injected by vue-loader)
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])
) {
return cssModule
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// user may set custom properties to `this` that start with `$`
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (
// global properties
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))
) {
if (__COMPAT__) {
const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!
if (desc.get) {
return desc.get.call(instance.proxy)
} else {
const val = globalProperties[key]
return isFunction(val) ? val.bind(instance.proxy) : val
}
} else {
return globalProperties[key]
}
}
},
set(
{ _: instance }: ComponentRenderContext,
key: string,
value: any
): boolean {
const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
setupState[key] = value
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
data[key] = value
} else if (hasOwn(instance.props, key)) {
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
return false
} else {
if (__DEV__ && key in instance.appContext.config.globalProperties) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
value
})
} else {
ctx[key] = value
}
}
return true
},
has(
{
_: { data, setupState, accessCache, ctx, appContext, propsOptions }
}: ComponentRenderContext,
key: string
) {
let normalizedProps
return (
accessCache![key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key)
)
}
}
我们分别对三个方法进行讲解:
get 获取方法
如果
key
不以$开头,直接从accessCache
获取,获取到就直接返回,如果获取不到则依次从setupState
,data
,ctx
,props
中获取,并且设置accessCache
。如果
key
以, data, attrs, refs, root, options, nextTick, $watch等则通过对应的方法进行获取,如果不是上述key值,则依次从ctx
和appContext.config.globalProperties
,最后如果找不到就获取失败。set 获取方法
- 只允许对
setupState
和data
的key进行赋值,且优先给setupState
赋值,如果前两者都没有对应的key直接赋值在ctx
上。
has判断是否有值的方法
- 判断
accessCache
,data
,setupState
,propsOptions
,ctx
,publicPropertiesMap
和appContext.config.globalProperties
有没有对应的key。
前面问题的答案:方便用户的使用,只需要访问
instance.proxy
就能访问和修改data
,setupState
,props
,appContext.config.globalProperties
等属性中的值。
- 如果
setup
参数大于1,则创建setupContext;
export function createSetupContext(
instance: ComponentInternalInstance
): SetupContext {
const expose: SetupContext['expose'] = exposed => {
instance.exposed = exposed || {}
}
let attrs: Data
return {
get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
},
slots: instance.slots,
emit: instance.emit,
expose
}
}
setupContext是
setup
函数的第二个参数,从方法来看我们就知道了setupContext包括attrs
,slots
,emit
和expose
,这就解释了为什么我们能在setup
函数中拿到对应的这些值了。第四个参数可能比较陌生,表示的是组件需要对外暴露的值。
- 执行
setup
函数,第一个参数是props
,第二个参数是setupConstext
。(用callWithErrorHandling
封装了一层,可以捕获执行错误)
const setupResult = callWithErrorHandling(
setup,
nstance,
ErrorCodes.SETUP_FUNCTION,
[instance.props, setupContext]
)
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
- 处理
setup
函数的返回结果;
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult)
}
finishComponentSetup(instance, isSSR)
}
如果返回结果是函数,则将其作为渲染函数
render
,这个函数就是用来生成subTree
VNode的函数。如果返回结果是对象,则变成响应式然后赋值给
setupState
属性,这里就解释了ctx
的代理对象proxy
中的setupState
是如何得到的。
- 完成组件实例的设置
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
// template / render function normalization
if (!instance.render) {
// could be set from setup()
if (compile && !Component.render) {
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template
if (template) {
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } =
Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
Component.render = compile(template, finalCompilerOptions)
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
if (installWithProxy) {
installWithProxy(instance)
}
}
// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
setCurrentInstance(instance)
pauseTracking()
applyOptions(instance)
resetTracking()
unsetCurrentInstance()
}
}
- 标准化模板和
render
函数,将render
函数赋值给instance.render属性。- 兼容2.0 Options API, 3.0 兼容 2.0 就是在这里实现的。
这里解释下render
函数:
我们常见的使用方式是使用SFC (Single File Components)去编写组件,我们知道浏览器是无法识别Vue文件的,在编译阶段使用Vue loader将Vue文件的代码转换成JS对象,其中会将template模板转换成render
函数。所以我们几乎不太会自己去实现render
函数。当然前面也提到了,可以在setup
函数中返回函数结果作为render
函数。
renderComponentRoot
生成subTree
VNode
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx,
inheritAttrs
} = instance
let result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
return result
}
- 在副作用渲染函数中的
renderComponentRoot
就是用render
生成subTree
VNode,然后继续递归patch
进行挂载和更新。