Vue3 源碼解析計劃之Setup,組件渲染前的初始化過程是怎樣的?
1寫在前面
Vue3允許在編寫組件的時候添加一個setup啟動函數(shù),作為Composition API邏輯組織的入口。那么渲染前的初始化過程是怎樣的呢?
2setup啟動函數(shù)
在setup函數(shù)內(nèi)部,定義了一個響應(yīng)式對象state,通過reactive API創(chuàng)建。state對象有name和age兩個屬性,模板中引用到的變量state和函數(shù)變量add包含在setup函數(shù)的返回對象中。
- <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內(nèi)部會處理這些options,即把定義的變量添加到組件實例上,等模板變異成render函數(shù)時,內(nèi)部通過with(this){}的語法去訪問在組件實例中的變量。
3創(chuàng)建和設(shè)置組件實例
組件實例的設(shè)置函數(shù)setupComponent流程是:
- 判斷是否是一個有狀態(tài)組件
- 初始化props
- 初始化插槽
- 設(shè)置有狀態(tài)的組件實例
- 返回組件實例
- function setupComponent(instance,isSSR=false){
- const {props,children,shapeFlag}= instance.vnode;
- //判斷是否是一個有狀態(tài)的組件
- const isStateful = shapeFlag & 4;
- //初始化 props
- initProps(instance,props,isStateful,isSSR);
- //初始化 插槽
- initSlots(instance,children);
- //設(shè)置有狀態(tài)的組件實例
- const setupResult = isStateful
- ? setupStatefulComponent(instance,isSSR)
- : undefined;
- return setupResult;
- }
在函數(shù)setupStatefulComponent的執(zhí)行過程中,流程如下:
- 創(chuàng)建渲染代理的屬性訪問緩存
- 創(chuàng)建渲染上下文的代理
- 判斷處理setup函數(shù)
- 如果setup函數(shù)帶有參數(shù),則創(chuàng)建一個setupContext
- 執(zhí)行setup函數(shù),獲取結(jié)果
- 處理setup執(zhí)行結(jié)果
- function setupStatefulComponent(instance,isSSR){
- const Component = instance.type;
- //創(chuàng)建渲染代理的屬性訪問緩存
- instance.accessCache = {};
- //創(chuàng)建渲染上下文的代理
- instance.proxy = new Proxy(instance.ctx,PublicInstanceProxyHandlers);
- //判斷處理setup函數(shù)
- const {setup} = Component;
- if(setup){
- //如果setup函數(shù)帶有參數(shù),則創(chuàng)建一個setupContext
- const setupContext = (
- instance.setupContext = setup.length > 1
- ? createSetupContext(instance)
- : null)
- //執(zhí)行setup函數(shù),獲取結(jié)果
- const setupResult = callWithErrorHandling(
- setup,
- instance,
- 0,/*SETUP_FUNCTION*/
- [instance.props,setupContext]
- )
- //處理setup執(zhí)行結(jié)果
- handleSetupResult(instance,setupResult);
- }else{
- //完成組件實例的設(shè)置
- finishComponentSetup(instance);
- }
- }
在vue2中也有代理模式:
- props求值后的數(shù)據(jù)存儲在this._props中
- data定義的數(shù)據(jù)存儲在this._data中
在vue3中,為了維護方便,把組件中不通用狀態(tài)的數(shù)據(jù)存儲到不同的屬性中,比如:存儲到setupState、ctx、data、props中。在執(zhí)行組件渲染函數(shù)的時候,直接訪問渲染上下文instance.ctx中的屬性,做一層proxy對渲染上下文instance.ctx屬性的訪問和修改,代理到setupState、ctx、data、props中數(shù)據(jù)的訪問和修改。
4創(chuàng)建渲染上下文代理
創(chuàng)建渲染上下文代理,使用了proxy的set、get、has三個屬性。
我們第一次獲取key對應(yīng)的數(shù)據(jù)后,利用accessCache[key]去緩存數(shù)據(jù)。下次再根據(jù)key查找數(shù)據(jù),直接通過accessCache[key]獲取對應(yīng)的值,不需要依次調(diào)用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中獲取數(shù)據(jù)
- accessCache![key] = AccessTypes.SETUP
- return setupState[key]
- } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
- //從data中獲取數(shù)據(jù)
- 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中獲取數(shù)據(jù)
- 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中的數(shù)據(jù)賦值,在非生產(chǎn)環(huán)境中收到一條警告,因為直接修改props不符合數(shù)據(jù)單向流動的設(shè)計思想。
set函數(shù)的實現(xiàn):
- 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內(nèi)部以$開頭的保留屬性賦值
- __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函數(shù)的實現(xiàn):
- 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函數(shù)
- //判斷處理setup函數(shù)
- const { setup } = Component
- if (setup) {
- //如果setup函數(shù)帶參數(shù),則創(chuàng)建了一個setupContext
- const setupContext = (instance.setupContext =
- setup.length > 1 ? createSetupContext(instance) : null)
- setCurrentInstance(instance)
- pauseTracking()
- //執(zhí)行setup函數(shù)獲取結(jié)果
- 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執(zhí)行結(jié)果
- handleSetupResult(instance, setupResult, isSSR)
- }
- } else {
- finishComponentSetup(instance, isSSR)
- }
6標(biāo)準(zhǔn)化模板或渲染函數(shù)
組件會通過 函數(shù)渲染成DOM,但是我們很少直接改寫render函數(shù)。而是通過這兩種方式:
- 使用SFC(SIngle File Components)單文件的開發(fā)方式來開發(fā)組件,通過編寫組件的template模板去描述一個組件的DOM結(jié)構(gòu)
- 還可以不借助webpack編譯,直接引入vue.js,開箱即用,直接在組件對象template屬性中寫組件的模板
Vue.js在web端有runtime-only和runtime-compiled兩個版本,在不是特殊要求的開發(fā)時,推薦使用runtime-only版本,因為它的體積相對更小,而且運行時不用進行編譯,耗時少,性能更優(yōu)秀。對于老舊項目可以使用runtime-compiled,runtime-only和runtime-compiled的區(qū)別在于是否注冊了compile。
compile方法是通過外部注冊的:
- let compile;
- function registerRuntimeCompiler(_compile){
- compile = _compile;
- }
compile和組件template屬性存在,render方法不存在的情況,runtime-compiled版本會在Javascript運行時進行模板編譯,生成render函數(shù)。
compile和組件template屬性不存在,組件template屬性存在的情況,由于沒有compile,用的是runtime-only版本,會報警告告訴用戶,想要運行時編譯得使用runtime-compiled版本的vue.js。
在執(zhí)行setup函數(shù)并獲取結(jié)果的時候,使用callWithErrorHandling把setup包裝了一層,有哪些好處呢?
7參考文章
《Vue3核心源碼解析》
《Vue中文社區(qū)》
《Vue3中文文檔》
8寫在最后
本文中主要分析了組件的初始化過程,主要包括創(chuàng)建組件實例和設(shè)置組件實例,通過進一步細(xì)節(jié)的深入,了解渲染上下文的代理過程,了解了Composition API中的setup 啟動函數(shù)執(zhí)行的時機。