Vue3 源码解析计划之Setup,组件渲染前的初始化过程是怎样的?
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. Its 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 启动函数执行的时机。