Vue3 源码解析计划之Setup,组件渲染前的初始化过程是怎样的?

开发 前端
本文中主要分析了组件的初始化过程,主要包括创建组件实例和设置组件实例,通过进一步细节的深入,了解渲染上下文的代理过程,了解了Composition API中的setup 启动函数执行的时机。

[[439970]]

1写在前面

Vue3允许在编写组件的时候添加一个setup启动函数,作为Composition API逻辑组织的入口。那么渲染前的初始化过程是怎样的呢?

2setup启动函数

在setup函数内部,定义了一个响应式对象state,通过reactive API创建。state对象有name和age两个属性,模板中引用到的变量state和函数变量add包含在setup函数的返回对象中。

  1. <template> 
  2.     <div> 
  3.     <h1>我的名字:{{state.name}}</h1> 
  4.     <h1>我的年龄:{{state.age}}</h1> 
  5.     <button>过年了,又长了一岁</button> 
  6.   </div> 
  7. </template> 
  8. <script> 
  9. import {reactive} from "vue"
  10.    
  11. export default define{ 
  12.     setup(){ 
  13.     const state = reactive({ 
  14.         name:"yichuan"
  15.       age:18 
  16.     }); 
  17.     function add(){ 
  18.         state.age++; 
  19.     } 
  20.      
  21.     return
  22.         state, 
  23.       add 
  24.     } 
  25.   } 
  26. </script> 

 

我们在vue2中知道是在props、data、methods、computed等options中定义一些变量,在组件初始化阶段,vue2内部会处理这些options,即把定义的变量添加到组件实例上,等模板变异成render函数时,内部通过with(this){}的语法去访问在组件实例中的变量。

3创建和设置组件实例

组件实例的设置函数setupComponent流程是:

  • 判断是否是一个有状态组件
  • 初始化props
  • 初始化插槽
  • 设置有状态的组件实例
  • 返回组件实例
  1. function setupComponent(instance,isSSR=false){ 
  2.  const {props,children,shapeFlag}= instance.vnode; 
  3.   //判断是否是一个有状态的组件 
  4.   const isStateful = shapeFlag & 4; 
  5.   //初始化 props 
  6.   initProps(instance,props,isStateful,isSSR); 
  7.   //初始化 插槽 
  8.   initSlots(instance,children); 
  9.   //设置有状态的组件实例 
  10.   const setupResult = isStateful  
  11.      ? setupStatefulComponent(instance,isSSR)  
  12.      : undefined; 
  13.    
  14.   return setupResult; 

在函数setupStatefulComponent的执行过程中,流程如下:

  • 创建渲染代理的属性访问缓存
  • 创建渲染上下文的代理
  • 判断处理setup函数
    • 如果setup函数带有参数,则创建一个setupContext
    • 执行setup函数,获取结果
    • 处理setup执行结果
  1. function setupStatefulComponent(instance,isSSR){ 
  2.  const Component = instance.type; 
  3.   //创建渲染代理的属性访问缓存 
  4.   instance.accessCache = {}; 
  5.   //创建渲染上下文的代理 
  6.   instance.proxy = new Proxy(instance.ctx,PublicInstanceProxyHandlers); 
  7.   //判断处理setup函数 
  8.   const {setup} = Component; 
  9.   if(setup){ 
  10.    //如果setup函数带有参数,则创建一个setupContext 
  11.    const setupContext = ( 
  12.       instance.setupContext = setup.length > 1  
  13.       ? createSetupContext(instance)  
  14.       : null
  15.      
  16.     //执行setup函数,获取结果 
  17.     const setupResult = callWithErrorHandling( 
  18.       setup, 
  19.       instance, 
  20.       0,/*SETUP_FUNCTION*/ 
  21.      [instance.props,setupContext] 
  22.     ) 
  23.      
  24.     //处理setup执行结果 
  25.     handleSetupResult(instance,setupResult); 
  26.      
  27.   }else
  28.    //完成组件实例的设置 
  29.     finishComponentSetup(instance); 
  30.   } 
  31.    

在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去判断。

  1. get({ _: instance }: ComponentRenderContext, key: string) { 
  2.     const { ctx, setupState, data, props, accessCache, type, appContext } = 
  3.       instance 
  4.  
  5.     // for internal formatters to know that this is a Vue instance 
  6.     if (__DEV__ && key === '__isVue') { 
  7.       return true 
  8.     } 
  9.  
  10.     // prioritize <script setup> bindings during dev. 
  11.     // this allows even properties that start with _ or $ to be used - so that 
  12.     // it aligns with the production behavior where the render fn is inlined and 
  13.     // indeed has access to all declared variables. 
  14.     if ( 
  15.       __DEV__ && 
  16.       setupState !== EMPTY_OBJ && 
  17.       setupState.__isScriptSetup && 
  18.       hasOwn(setupState, key
  19.     ) { 
  20.       return setupState[key
  21.     } 
  22.  
  23.     // data / props / ctx 
  24.     // This getter gets called for every property access on the render context 
  25.     // during render and is a major hotspot. The most expensive part of this 
  26.     // is the multiple hasOwn() calls. It's much faster to do a simple property 
  27.     // access on a plain object, so we use an accessCache object (with null 
  28.     // prototype) to memoize what access type a key corresponds to
  29.     let normalizedProps 
  30.     if (key[0] !== '$') { 
  31.       // data / props / ctx / setupState 
  32.       // 渲染代理的属性访问缓存中 
  33.       const n = accessCache![key
  34.       if (n !== undefined) { 
  35.         //从缓存中获取 
  36.         switch (n) { 
  37.           case AccessTypes.SETUP:    
  38.             return setupState[key
  39.           case AccessTypes.DATA: 
  40.             return data[key
  41.           case AccessTypes.CONTEXT: 
  42.             return ctx[key
  43.           case AccessTypes.PROPS: 
  44.             return props![key
  45.           // default: just fallthrough 
  46.         } 
  47.       } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { 
  48.         //从setupState中获取数据 
  49.         accessCache![key] = AccessTypes.SETUP 
  50.         return setupState[key
  51.       } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { 
  52.         //从data中获取数据 
  53.         accessCache![key] = AccessTypes.DATA 
  54.         return data[key
  55.       } else if ( 
  56.         // only cache other properties when instance has declared (thus stable) 
  57.         // props 
  58.         (normalizedProps = instance.propsOptions[0]) && 
  59.         hasOwn(normalizedProps, key
  60.       ) { 
  61.         accessCache![key] = AccessTypes.PROPS 
  62.         return props![key
  63.       } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { 
  64.         //从ctx中获取数据 
  65.         accessCache![key] = AccessTypes.CONTEXT 
  66.         return ctx[key
  67.       } else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) { 
  68.         accessCache![key] = AccessTypes.OTHER 
  69.       } 
  70.     } 
  71.  
  72.     const publicGetter = publicPropertiesMap[key
  73.     let cssModule, globalProperties 
  74.     // public $xxx properties 
  75.     if (publicGetter) { 
  76.       if (key === '$attrs') { 
  77.         track(instance, TrackOpTypes.GET, key
  78.         __DEV__ && markAttrsAccessed() 
  79.       } 
  80.       return publicGetter(instance) 
  81.     } else if ( 
  82.       // css module (injected by vue-loader) 
  83.       (cssModule = type.__cssModules) && 
  84.       (cssModule = cssModule[key]) 
  85.     ) { 
  86.       return cssModule 
  87.     } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { 
  88.       // user may set custom properties to `this` that start with `$` 
  89.       accessCache![key] = AccessTypes.CONTEXT 
  90.       return ctx[key
  91.     } else if ( 
  92.       // global properties 
  93.       ((globalProperties = appContext.config.globalProperties), 
  94.       hasOwn(globalProperties, key)) 
  95.     ) { 
  96.       if (__COMPAT__) { 
  97.         const desc = Object.getOwnPropertyDescriptor(globalProperties, key)! 
  98.         if (desc.get) { 
  99.           return desc.get.call(instance.proxy) 
  100.         } else { 
  101.           const val = globalProperties[key
  102.           return isFunction(val) ? val.bind(instance.proxy) : val 
  103.         } 
  104.       } else { 
  105.         return globalProperties[key
  106.       } 
  107.     } else if ( 
  108.       __DEV__ && 
  109.       currentRenderingInstance && 
  110.       (!isString(key) || 
  111.         // #1091 avoid internal isRef/isVNode checks on component instance leading 
  112.         // to infinite warning loop 
  113.         key.indexOf('__v') !== 0) 
  114.     ) { 
  115.       if ( 
  116.         data !== EMPTY_OBJ && 
  117.         (key[0] === '$' || key[0] === '_') && 
  118.         hasOwn(data, key
  119.       ) { 
  120.         warn( 
  121.           `Property ${JSON.stringify( 
  122.             key 
  123.           )} must be accessed via $data because it starts with a reserved ` + 
  124.             `character ("$" or "_"and is not proxied on the render context.` 
  125.         ) 
  126.       } else if (instance === currentRenderingInstance) { 
  127.         warn( 
  128.           `Property ${JSON.stringify(key)} was accessed during render ` + 
  129.             `but is not defined on instance.` 
  130.         ) 
  131.       } 
  132.     } 
  133.   } 

注意:如果我们直接给props中的数据赋值,在非生产环境中收到一条警告,因为直接修改props不符合数据单向流动的设计思想。

set函数的实现:

  1. export const PublicInstanceProxyHandlers: ProxyHandler<any> = { 
  2.  set
  3.     { _: instance }: ComponentRenderContext, 
  4.     key: string, 
  5.     value: any 
  6.   ): boolean { 
  7.     const { data, setupState, ctx } = instance 
  8.     if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { 
  9.       //给setupState赋值 
  10.       setupState[key] = value 
  11.     } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { 
  12.       //给data赋值 
  13.       data[key] = value 
  14.     } else if (hasOwn(instance.props, key)) { 
  15.       //不能直接给props赋值 
  16.       __DEV__ && 
  17.         warn( 
  18.           `Attempting to mutate prop "${key}". Props are readonly.`, 
  19.           instance 
  20.         ) 
  21.       return false 
  22.     } 
  23.     if (key[0] === '$' && key.slice(1) in instance) { 
  24.       //不能给vue内部以$开头的保留属性赋值 
  25.        
  26.       __DEV__ && 
  27.         warn( 
  28.           `Attempting to mutate public property "${key}". ` + 
  29.             `Properties starting with $ are reserved and readonly.`, 
  30.           instance 
  31.         ) 
  32.       return false 
  33.     } else { 
  34.       if (__DEV__ && key in instance.appContext.config.globalProperties) { 
  35.         Object.defineProperty(ctx, key, { 
  36.           enumerable: true
  37.           configurable: true
  38.           value 
  39.         }) 
  40.       } else { 
  41.         ctx[key] = value 
  42.       } 
  43.     } 
  44.     return true 
  45.   } 

has函数的实现:

  1. has( 
  2.     { 
  3.       _: { data, setupState, accessCache, ctx, appContext, propsOptions } 
  4.     }: ComponentRenderContext, 
  5.     key: string 
  6.   ) { 
  7.     let normalizedProps 
  8.     //依次判断 
  9.     return ( 
  10.       !!accessCache![key] || 
  11.       (data !== EMPTY_OBJ && hasOwn(data, key)) || 
  12.       (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || 
  13.       ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) || 
  14.       hasOwn(ctx, key) || 
  15.       hasOwn(publicPropertiesMap, key) || 
  16.       hasOwn(appContext.config.globalProperties, key
  17.     ) 
  18.   } 

5判断处理setup函数

  1. //判断处理setup函数 
  2. const { setup } = Component 
  3. if (setup) { 
  4.   //如果setup函数带参数,则创建了一个setupContext 
  5.   const setupContext = (instance.setupContext = 
  6.                         setup.length > 1 ? createSetupContext(instance) : null
  7.  
  8.   setCurrentInstance(instance) 
  9.   pauseTracking() 
  10.   //执行setup函数获取结果 
  11.   const setupResult = callWithErrorHandling( 
  12.     setup, 
  13.     instance, 
  14.     ErrorCodes.SETUP_FUNCTION, 
  15.     [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] 
  16.   ) 
  17.   resetTracking() 
  18.   unsetCurrentInstance() 
  19.  
  20.   if (isPromise(setupResult)) { 
  21.     setupResult.then(unsetCurrentInstance, unsetCurrentInstance) 
  22.  
  23.     if (isSSR) { 
  24.       // return the promise so server-renderer can wait on it 
  25.       return setupResult 
  26.         .then((resolvedResult: unknown) => { 
  27.         handleSetupResult(instance, resolvedResult, isSSR) 
  28.       }) 
  29.         .catch(e => { 
  30.         handleError(e, instance, ErrorCodes.SETUP_FUNCTION) 
  31.       }) 
  32.     } else if (__FEATURE_SUSPENSE__) { 
  33.       // async setup returned Promise. 
  34.       // bail here and wait for re-entry. 
  35.       instance.asyncDep = setupResult 
  36.     } else if (__DEV__) { 
  37.       warn( 
  38.         `setup() returned a Promise, but the version of Vue you are using ` + 
  39.         `does not support it yet.` 
  40.       ) 
  41.     } 
  42.   } else { 
  43.     //处理setup执行结果 
  44.     handleSetupResult(instance, setupResult, isSSR) 
  45.   } 
  46. else { 
  47.   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方法是通过外部注册的:

  1. let compile; 
  2. function registerRuntimeCompiler(_compile){ 
  3.  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 启动函数执行的时机。

 

责任编辑:武晓燕 来源: 前端万有引力
相关推荐

2021-12-12 18:31:35

VNode组件Vue3

2022-01-26 11:00:58

源码层面Vue3

2023-04-27 11:07:24

Setup语法糖Vue3

2021-12-01 08:11:44

Vue3 插件Vue应用

2021-12-02 05:50:35

Vue3 插件Vue应用

2012-03-13 13:38:42

Java

2023-05-29 09:37:17

Vue3Vite

2021-07-07 05:00:17

初始化源码

2022-02-18 09:39:51

Vue3.0Vue2.0Script Set

2009-06-04 09:26:51

struts 源码struts 资源文件

2023-11-29 08:49:31

Vue.jsData 函数

2024-08-13 09:26:07

2021-12-14 21:43:13

Vue3函数computed

2023-08-28 07:25:58

DDE服务器管理器

2023-10-06 20:57:52

C++聚合成员

2023-11-27 22:55:17

DNS域名解析

2020-12-01 08:34:31

Vue3组件实践

2021-05-12 10:25:53

组件验证漏洞

2016-09-14 22:22:03

Android Vue性能优化

2009-08-28 11:09:35

C#数组初始化
点赞
收藏

51CTO技术栈公众号