新特性
- 组合式API
setup:
setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口
在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data 、computed 或 methods 被解析之前,所以它们无法在 setup 中被获取。
使用示例
import{ fetchUserRepositories}from'@/api/repositories'import{ ref}from'vue'exportdefault{components:{ RepositoriesFilters, RepositoriesSortBy, RepositoriesList},props:{user:{type: String,required:true}},setup(props){const repositories=ref([])// 创建一个响应式的repositories变量constgetUserRepositories=async()=>{
repositories.value=awaitfetchUserRepositories(props.user)}return{
repositories,
getUserRepositories}},data(){return{filters:{...},// 3searchQuery:''// 2}},computed:{filteredRepositories(){...},// 3repositoriesMatchingSearchQuery(){...},// 2},watch:{user:'getUserRepositories'// 1},methods:{updateFilters(){...},// 3},mounted(){this.getUserRepositories()// 1}}
源码分析
源码位置: vue/runtime-core/src/component.ts
- 判断是否是一个有状态组件
- 初始化props
- 初始化插槽
- 设置有状态的组件实例
- 返回组件实例
setupComponent
exportfunctionsetupComponent(instance: ComponentInternalInstance,
isSSR=false){
isInSSRComponentSetup= isSSRconst{ props, children}= instance.vnode// 判断是否是一个有状态的组件const isStateful=isStatefulComponent(instance)// 初始化propsinitProps(instance, props, isStateful, isSSR)// 初始化插槽initSlots(instance, children)//设置有状态的组件实例const setupResult= isStateful?setupStatefulComponent(instance, isSSR):undefined
isInSSRComponentSetup=falsereturn setupResult}
setupStatefulComponent
- 创建渲染代理的属性访问缓存
如果是 DEV 环境,则会开始检测组件中的各种选项的命名,比如 name、components、directives 等,如果检测有问题,就会在开发环境报出警告。
- 创建渲染上下文的代理
会在实例上创建一个 accessCache 的属性,该属性用以缓存渲染器代理属性,以减少读取次数。之后会在组件实例上初始化一个代理属性,这个代理属性代理了组件的上下文,并且将它设置为观察原始值,这样这个代理对象将不会被追踪
- 判断处理setup函数
从组件中取出 setup 函数,这里判断是否存在 setup 函数,如果不存在,则直接跳转到底部逻辑,执行 finishComponentSetup,完成组件初始化。否则就会进入 if (setup) 之后的分支条件中
- 如果setup函数带有参数,则创建一个setupContext
生成 setup 的上下文对象
在 function 函数对象上调用 length 时,返回值是这个函数的形参数量。ru: setup(props) => setup.length === 1
- 执行setup函数,获取结果
callWithErrorHandling函数获取setupResult结果
resetTracking重置依赖收集状态
- 处理setup执行结果
判断 setupResult 的返回值类型
如果不是 promise 类型返回值,则会通过 handleSetupResult 函数来处理返回结果
functionsetupStatefulComponent(instance: ComponentInternalInstance,isSSR: boolean){const Component= instance.typeas ComponentOptionsif(__DEV__){/* 检测组件名称、指令、编译选项等等,有错误则报警 */// ...}// 0. 创建一个渲染代理的属性的访问缓存
instance.accessCache= Object.create(null)// 1. 创建一个公共的示例或渲染器代理// 它将被标记为 raw,所以它不会被追踪
instance.proxy=markRaw(newProxy(instance.ctx, PublicInstanceProxyHandlers))if(__DEV__){exposePropsOnRenderContext(instance)}// 2. 调用setup()const{ setup}= Componentif(setup){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).../}else{// 捕获 Setup 执行结果handleSetupResult(instance, setupResult, isSSR)}}else{// 完成组件初始化finishComponentSetup(instance, isSSR)}}
createSetupContext
setup 上下文(setupContext)中就如文档中描述的一样,有 attrs、slots、emit 这三种熟悉的属性,另外还有expose属性
exportfunctioncreateSetupContext(instance: ComponentInternalInstance): SetupContext{constexpose: SetupContext['expose']=exposed=>{if(__DEV__&& instance.exposed){warn(`expose() should be called only once per setup().`)}
instance.exposed= exposed||{}// 暴露组件api给组件使用者}letattrs: Dataif(__DEV__){/* DEV 逻辑忽略,对上下文选项设置 getter */}else{return{getattrs(){return attrs||(attrs=createAttrsProxy(instance))},slots: instance.slots,emit: instance.emit,
expose}}}
- expose
用该 API 来清除地控制哪些内容会明确地公开暴露给组件使用者
import{ ref}from'vue'exportdefault{setup(_,{ expose}){const count=ref(0)functionincrement(){
count.value++}// 仅仅暴露 increment 给父组件expose({
increment})return{ increment, count}}}
像上方代码一样使用 expose 时,父组件获取的 ref 对象里只会有 increment 属性,而 count 属性将不会暴露出去
handleSetupResult
exportfunctionhandleSetupResult(instance: ComponentInternalInstance,setupResult: unknown,isSSR: boolean){if(isFunction(setupResult)){// setup 返回了一个行内渲染函数if(__NODE_JS__&&(instance.typeas ComponentOptions).__ssrInlineRender){// 当这个函数的名字是 ssrRender (通过 SFC 的行内模式编译)// 将函数作为服务端渲染函数
instance.ssrRender= setupResult}else{// 否则将函数作为渲染函数
instance.render= setupResultas InternalRenderFunction}}elseif(isObject(setupResult)){// 将返回对象转换为响应式对象,并设置为实例的 setupState 属性
instance.setupState=proxyRefs(setupResult)}finishComponentSetup(instance, isSSR)}
finishComponentSetup
完成组件设置
exportfunctionfinishComponentSetup(instance: ComponentInternalInstance,isSSR: boolean,
skipOptions?: boolean){const Component= instance.typeas ComponentOptions// 模板 / 渲染函数的规范行为// 1、渲染函数可能已经存在,通过 setup 返回// 2、除此之外尝试使用 `Component.render` 当做渲染函数// 3、如果这个函数没有渲染函数,设置 `instance.render` 为空函数,以便它能从 mixins/extend 中获得渲染函数if(__NODE_JS__&& isSSR){
instance.render=(instance.render||
Component.render||NOOP)as InternalRenderFunction}elseif(!instance.render){// 可以在 setup() 中设置if(compile&&!Component.render){const template= Component.templateif(template){const{ isCustomElement, compilerOptions}= instance.appContext.configconst{
delimiters,compilerOptions: componentCompilerOptions}= ComponentconstfinalCompilerOptions: CompilerOptions=extend(extend({
isCustomElement,
delimiters},
compilerOptions),
componentCompilerOptions)
Component.render=compile(template, finalCompilerOptions)}}
instance.render=(Component.render||NOOP)as InternalRenderFunction// 对于使用 `with` 块的运行时编译的渲染函数,这个渲染代理需要不一样的 `has` handler 陷阱,它有更好的// 性能表现并且只允许白名单内的 globals 属性通过。if(instance.render._rc){
instance.withProxy=newProxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers)}}}
在vue2中也有代理模式:
props求值后的数据存储在this._props中
data定义的数据存储在this._data中
在vue3中,为了维护方便,把组件中不通用状态的数据存储到不同的属性中,比如:存储到setupState、ctx、data、props中。在执行组件渲染函数的时候,直接访问渲染上下文instance.ctx中的属性,做一层proxy对渲染上下文instance.ctx属性的访问和修改,代理到setupState、ctx、data、props中数据的访问和修改。