1写在前面
Vue3允许在编写组件的时候添加一个setup启动函数,作为Composition API逻辑组织的入口。那么渲染前的初始化过程是怎样的呢?
2setup启动函数
在setup函数内部,定义了一个响应式对象state,通过reactive API创建。state对象有name和age两个属性,模板中引用到的变量state和函数变量add包含在setup函数的返回对象中。
<template>
<div>
<h1>我的名字:{{state.name}}</h1>
<h1>我的年龄:{{state.age}}</h1>
<button>过年了,又长了一岁</button>
</div>
</template>
<script>
import {reactive} from "vue";
export default define{
setup(){
const state = reactive({
name:"yichuan",
age:18
});
function add(){
state.age++;
}
return{
state,
add
}
}
}
</script>
我们在vue2中知道是在props、data、methods、computed等options中定义一些变量,在组件初始化阶段,vue2内部会处理这些options,即把定义的变量添加到组件实例上,等模板变异成render函数时,内部通过with(this){}的语法去访问在组件实例中的变量。
3创建和设置组件实例
组件实例的设置函数setupComponent
流程是:
- 判断是否是一个有状态组件
- 初始化props
- 初始化插槽
- 设置有状态的组件实例
- 返回组件实例
function setupComponent(instance,isSSR=false){
const {props,children,shapeFlag}= instance.vnode;
//判断是否是一个有状态的组件
const isStateful = shapeFlag & 4;
//初始化 props
initProps(instance,props,isStateful,isSSR);
//初始化 插槽
initSlots(instance,children);
//设置有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance,isSSR)
: undefined;
return setupResult;
}
在函数setupStatefulComponent的执行过程中,流程如下:
-
创建渲染代理的属性访问缓存
-
创建渲染上下文的代理
-
判断处理setup函数
- 如果setup函数带有参数,则创建一个setupContext
- 执行setup函数,获取结果
- 处理setup执行结果
function setupStatefulComponent(instance,isSSR){
const Component = instance.type;
//创建渲染代理的属性访问缓存
instance.accessCache = {};
//创建渲染上下文的代理
instance.proxy = new Proxy(instance.ctx,PublicInstanceProxyHandlers);
//判断处理setup函数
const {setup} = Component;
if(setup){
//如果setup函数带有参数,则创建一个setupContext
const setupContext = (
instance.setupContext = setup.length > 1
? createSetupContext(instance)
: null)
//执行setup函数,获取结果
const setupResult = callWithErrorHandling(
setup,
instance,
0,/*SETUP_FUNCTION*/
[instance.props,setupContext]
)
//处理setup执行结果
handleSetupResult(instance,setupResult);
}else{
//完成组件实例的设置
finishComponentSetup(instance);
}
}
在vue2中也有代理模式:
- props求值后的数据存储在this._props中
- data定义的数据存储在this._data中
在vue3中,为了维护方便,把组件中不通用状态的数据存储到不同的属性中,比如:存储到setupState、ctx、data、props中。在执行组件渲染函数的时候,直接访问渲染上下文instance.ctx中的属性,做一层proxy对渲染上下文instance.ctx属性的访问和修改,代理到setupState、ctx、data、props中数据的访问和修改。
4创建渲染上下文代理
创建渲染上下文代理,使用了proxy的set、get、has三个属性。
我们第一次获取key对应的数据后,利用accessCache[key]去缓存数据。下次再根据key查找数据,直接通过accessCache[key]获取对应的值,不需要依次调用hasOwn去判断。
get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } =
instance
// for internal formatters to know that this is a Vue instance
if (__DEV__ && key === '__isVue') {
return true
}
// prioritize <script setup> bindings during dev.
// this allows even properties that start with _ or $ to be used - so that
// it aligns with the production behavior where the render fn is inlined and
// indeed has access to all declared variables.
if (
__DEV__ &&
setupState !== EMPTY_OBJ &&
setupState.__isScriptSetup &&
hasOwn(setupState, key)
) {
return setupState[key]
}
// data / props / ctx
// This getter gets called for every property access on the render context
// during render and is a major hotspot. The most expensive part of this
// is the multiple hasOwn() calls. It's much faster to do a simple property
// access on a plain object, so we use an accessCache object (with null
// prototype) to memoize what access type a key corresponds to.
let normalizedProps
if (key[0] !== '$') {
// data / props / ctx / setupState
// 渲染代理的属性访问缓存中
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]
// default: just fallthrough
}
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
//从setupState中获取数据
accessCache![key] = AccessTypes.SETUP
return setupState[key]
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
//从data中获取数据
accessCache![key] = AccessTypes.DATA
return data[key]
} else if (
// only cache other properties when instance has declared (thus stable)
// props
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
accessCache![key] = AccessTypes.PROPS
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
//从ctx中获取数据
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]
}
} else if (
__DEV__ &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
key.indexOf('__v') !== 0)
) {
if (
data !== EMPTY_OBJ &&
(key[0] === '$' || key[0] === '_') &&
hasOwn(data, key)
) {
warn(
`Property ${JSON.stringify(
key
)} must be accessed via $data because it starts with a reserved ` +
`character ("$" or "_") and is not proxied on the render context.`
)
} else if (instance === currentRenderingInstance) {
warn(
`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`
)
}
}
}
注意:如果我们直接给props中的数据赋值,在非生产环境中收到一条警告,因为直接修改props不符合数据单向流动的设计思想。
set函数的实现:
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
set(
{ _: instance }: ComponentRenderContext,
key: string,
value: any
): boolean {
const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
//给setupState赋值
setupState[key] = value
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
//给data赋值
data[key] = value
} else if (hasOwn(instance.props, key)) {
//不能直接给props赋值
__DEV__ &&
warn(
`Attempting to mutate prop "${key}". Props are readonly.`,
instance
)
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
//不能给vue内部以$开头的保留属性赋值
__DEV__ &&
warn(
`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`,
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函数的实现:
has(
{
_: { data, setupState, accessCache, ctx, appContext, propsOptions }
}: ComponentRenderContext,
key: string
) {
let normalizedProps
//依次判断
return (
!!accessCache![key] ||
(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)
)
}
5判断处理setup函数
//判断处理setup函数
const { setup } = Component
if (setup) {
//如果setup函数带参数,则创建了一个setupContext
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()
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
//处理setup执行结果
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
6标准化模板或渲染函数
组件会通过 函数渲染成DOM,但是我们很少直接改写render函数。而是通过这两种方式:
- 使用SFC(SIngle File Components)单文件的开发方式来开发组件,通过编写组件的template模板去描述一个组件的DOM结构
- 还可以不借助webpack编译,直接引入vue.js,开箱即用,直接在组件对象template属性中写组件的模板
Vue.js在web端有runtime-only和runtime-compiled两个版本,在不是特殊要求的开发时,推荐使用runtime-only版本,因为它的体积相对更小,而且运行时不用进行编译,耗时少,性能更优秀。对于老旧项目可以使用runtime-compiled,runtime-only和runtime-compiled的区别在于是否注册了compile。
compile方法是通过外部注册的:
let compile;
function registerRuntimeCompiler(_compile){
compile = _compile;
}
compile和组件template属性存在,render方法不存在的情况,runtime-compiled版本会在Javascript运行时进行模板编译,生成render函数。
compile和组件template属性不存在,组件template属性存在的情况,由于没有compile,用的是runtime-only版本,会报警告告诉用户,想要运行时编译得使用runtime-compiled版本的vue.js。
在执行setup函数并获取结果的时候,使用callWithErrorHandling把setup包装了一层,有哪些好处呢?
7参考文章
- 《Vue3核心源码解析》
- 《Vue中文社区》
- 《Vue3中文文档》
8写在最后
本文中主要分析了组件的初始化过程,主要包括创建组件实例和设置组件实例,通过进一步细节的深入,了解渲染上下文的代理过程,了解了Composition API中的setup 启动函数执行的时机。