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

1,307 阅读6分钟

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流程是:

  1. 判断是否是一个有状态组件
  2. 初始化props
  3. 初始化插槽
  4. 设置有状态的组件实例
  5. 返回组件实例
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的执行过程中,流程如下:

  1. 创建渲染代理的属性访问缓存

  2. 创建渲染上下文的代理

  3. 判断处理setup函数

    1. 如果setup函数带有参数,则创建一个setupContext
    2. 执行setup函数,获取结果
    3. 处理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 PublicInstanceProxyHandlersProxyHandler<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(1in 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, {
          enumerabletrue,
          configurabletrue,
          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参考文章

8写在最后

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