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 启动函数执行的时机。

应用开发
上一篇:VMware 与 NVIDIA 为企业开启生成式 AI 时代
下一篇:强强联手促应用落地!摩尔线程与长城超云完成服务器产品兼容互认证