在Vue 3.0的使用中我們可以不使用data
、props
火窒、methods
硼补、computed
等Option函數(shù),可以只下在setup
函數(shù)中進行編寫代碼邏輯熏矿。當然為了和Vue 2.0兼容已骇,也可以繼續(xù)使用Option函數(shù)。
先提出兩個個問題:
-
setup
函數(shù)的執(zhí)行時機是什么票编? -
setup
函數(shù)的返回結(jié)果為何與模板的渲染建立聯(lián)系的褪储?
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
)
}
掛載組件分為三個步驟:
- 創(chuàng)建組件實例
- 設(shè)置組件實例
- 創(chuàng)建帶副作用的渲染函數(shù)
我們本文分析的主角setup
函數(shù)是在第二個階段進行處理的,但是鑒于這三個步驟具有關(guān)聯(lián)性慧域,我們也會將第一個和第三個步驟進行和第二個步驟的關(guān)聯(lián)進行分析鲤竹。
createComponentInstance
創(chuàng)建組件實例
- 我們先來看看組件實例接口
ComponentInternalInstance
定義的一些屬性的含義:
export interface ComponentInternalInstance {
// 組件的id
uid: number
// 組件類型(options對象 或者 函數(shù))
type: ConcreteComponent
// 父組件實例
parent: ComponentInternalInstance | null
// 根組件實例
root: ComponentInternalInstance
// app上下文
appContext: AppContext
// 組件VNode
vnode: VNode
// 要更新到的VNode
next: VNode | null
// 子樹VNode
subTree: VNode
// 帶副作用的渲染函數(shù)
update: SchedulerJob
// 渲染函數(shù)
render: InternalRenderFunction | null
// SSR渲染函數(shù)
ssrRender?: Function | null
// 依賴注入的數(shù)據(jù)
provides: Data
// 收集響應(yīng)式依賴的作用域
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)相關(guān)方法
ceReload?: () => void
// the rest are only for stateful components ---------------------------------
// 渲染上下文代理對象,當使用`this`時就是指的這個對象
proxy: ComponentPublicInstance | null
// 組件暴露的對象
exposed: Record<string, any> | null
// 組件暴露對象的代理對象
exposeProxy: Record<string, any> | null
// 帶有with區(qū)塊(block)的渲染上下文代理對象
withProxy: ComponentPublicInstance | null
// 渲染上下文---即組件對象的信息 { _: instance }
ctx: Data
// data數(shù)據(jù)
data: Data
// props數(shù)據(jù)
props: Data
// attrs數(shù)據(jù)
attrs: Data
// slot數(shù)據(jù)
slots: InternalSlots
// 組件或者DOM的ref引用
refs: Data
// emit函數(shù)
emit: EmitFn
// 記錄被v-once修飾已經(jīng)觸發(fā)的事件
emitted: Record<string, boolean> | null
// 工廠函數(shù)生成的默認props數(shù)據(jù)
propsDefaults: Data
// setup函數(shù)返回的響應(yīng)式結(jié)果
setupState: Data
// setup函數(shù)上下文數(shù)據(jù)
setupContext: SetupContext | null
// 異步組件
suspense: SuspenseBoundary | null
// 異步組件ID
suspenseId: number
// setup函數(shù)返回的異步函數(shù)結(jié)果
asyncDep: Promise<any> | null
// 異步函數(shù)調(diào)用已完成
asyncResolved: boolean
// 是否已掛載
isMounted: boolean
// 是否已卸載
isUnmounted: boolean
// 是否已去激活
isDeactivated: boolean
// 各種鉤子函數(shù)
// 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>>
}
- 我們接下來看看創(chuàng)建組件實例時主要設(shè)置了哪些屬性值:
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
}
我們看到創(chuàng)建組件實例的時候主要設(shè)置了uid
、vnode
昔榴、appContext
辛藻、provides
、propsOptions
互订、emitsOptions
和ctx
等這些屬性吱肌。
setupComponent
設(shè)置組件實例流程
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
(后續(xù)章節(jié)介紹)仰禽;- 調(diào)用
setupStatefulComponent
方法設(shè)置有狀態(tài)組件實例(本文分析的主要內(nèi)容)氮墨。
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
的屬性時會觸發(fā)PublicInstanceProxyHandlers
方法温峭,而此方法的操作對象是ctx
猛铅。
這里先提出一個問題:為什么設(shè)置代理?
我們來看看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
中獲取祥款,并且設(shè)置accessCache
。如果
key
以, data, attrs, refs, root, options, nextTick, $watch等則通過對應(yīng)的方法進行獲取刃跛,如果不是上述key值,則依次從ctx
和appContext.config.globalProperties
苛萎,最后如果找不到就獲取失敗桨昙。set 獲取方法
- 只允許對
setupState
和data
的key進行賦值,且優(yōu)先給setupState
賦值腌歉,如果前兩者都沒有對應(yīng)的key直接賦值在ctx
上蛙酪。
has判斷是否有值的方法
- 判斷
accessCache
,data
,setupState
,propsOptions
,ctx
,publicPropertiesMap
和appContext.config.globalProperties
有沒有對應(yīng)的key。
前面問題的答案:方便用戶的使用翘盖,只需要訪問
instance.proxy
就能訪問和修改data
,setupState
,props
桂塞,appContext.config.globalProperties
等屬性中的值。
- 如果
setup
參數(shù)大于1馍驯,則創(chuàng)建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
函數(shù)的第二個參數(shù),從方法來看我們就知道了setupContext包括attrs
,slots
,emit
和expose
汰瘫,這就解釋了為什么我們能在setup
函數(shù)中拿到對應(yīng)的這些值了狂打。第四個參數(shù)可能比較陌生,表示的是組件需要對外暴露的值混弥。
- 執(zhí)行
setup
函數(shù)趴乡,第一個參數(shù)是props
,第二個參數(shù)是setupConstext
蝗拿。(用callWithErrorHandling
封裝了一層晾捏,可以捕獲執(zhí)行錯誤)
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
函數(shù)的返回結(jié)果;
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)
}
如果返回結(jié)果是函數(shù)蛹磺,則將其作為渲染函數(shù)
render
粟瞬,這個函數(shù)就是用來生成subTree
VNode的函數(shù)。如果返回結(jié)果是對象萤捆,則變成響應(yīng)式然后賦值給
setupState
屬性,這里就解釋了ctx
的代理對象proxy
中的setupState
是如何得到的俗批。
- 完成組件實例的設(shè)置
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
函數(shù)俗或,將render
函數(shù)賦值給instance.render屬性。- 兼容2.0 Options API, 3.0 兼容 2.0 就是在這里實現(xiàn)的岁忘。
這里解釋下render
函數(shù):
我們常見的使用方式是使用SFC (Single File Components)去編寫組件辛慰,我們知道瀏覽器是無法識別Vue文件的,在編譯階段使用Vue loader將Vue文件的代碼轉(zhuǎn)換成JS對象干像,其中會將template模板轉(zhuǎn)換成render
函數(shù)帅腌。所以我們幾乎不太會自己去實現(xiàn)render
函數(shù)驰弄。當然前面也提到了,可以在setup
函數(shù)中返回函數(shù)結(jié)果作為render
函數(shù)速客。
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
}
- 在副作用渲染函數(shù)中的
renderComponentRoot
就是用render
生成subTree
VNode戚篙,然后繼續(xù)遞歸patch
進行掛載和更新。