组件的setup函数时Vue.js3新增的组件选项,setup函数主要用来配合组合式API,为用户提供一个地方,用于建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等能力。
在组件的整个生命周期中,setup函数只会在挂载时执行一次,其返回值可以有两种情况:
- 返回一个函数,该函数将作为组件的render函数:
这种方式常用于组件不是以模板来表达其渲染内容的情况。如果组件以模板来表达其渲染的内容,那么setup函数不可以再返回函数,否则会与模板编译生成的渲染函数产生冲突const Comp = { setup(){ // setup 函数可以返回一个函数,该函数将作为组件的渲染函数 return ()=>{ return { type:'div', children:'hello' } } } }
- 返回一个对象,该对象中包含的数据将暴露给模板使用:
const Comp = { setup(){ const count = ref(0) // 返回一个对象,对象中的数据会暴露到渲染函数中 return { count } }, return ()=>{ // 通过this可以访问setup暴露出来的响应式数据 return { type:'div', children:`count is: ${this.count}`} } }
另外,setup函数接收两个参数,第一个参数是props,第二个参数也是个对象,通常称为setupContext,如下面的代码所示:
const Comp = {
props:{
foo: String
},
setup(props, setupContext){
props.foo //访问传入的props数据
// setupContext中包含与组件接口相关的重要数据
const {slots, emit, attrs, expose} = setupContext
// ...
}
}
其中props为外部为组件传递的props数据对象,setupContext保存着与组件接口相关的数据和方法
通常情况下,不建议将setup与Vue.js2的其他选项混合使用,因为在Vue.js3的场景下,更加提倡组合式API,setup函数就是为组合式API而生的,混合使用会带来语义和理解上的负担。
接下来,就围绕上述的这些呢你尝试实现setup组件选项,如下面代码所示:
function mountComponent(vnode, container, anchor){
const componentOptions = vnode.type
// 从组件选项中取出setup函数
let { render, data, setup, /* 省略其他选项*/ } = componentOptions
beforeCreate && beforeCreate()
const state = data ? reactive(data()):null
const [props, attrs] = resolveProps(propsOption, vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
}
// setupContext,暂时实现attrs
const setupContext = { attrs }
// 调用setup函数
const setupResult = setup(shallowReadOnly(instance.props),setupContext)
// setupState用来存储由setup返回的数据
let setupState = null
// 如果 setup函数的返回值是函数,则将其作为渲染函数
if(typeof setupResult === 'function'){
// 报告冲突
if(render) console.error('setup 函数返回渲染函数,render选项将被忽略')
// 将 setupResult 作为渲染函数
render = setupResult
}else {
// 如果setup的返回值不是函数,则作为数据状态赋值给setupState
setupState = setupContext
}
vnode.component = instance
// 创建渲染上下文对象,本质上是组件实例的代理
const renderContext = new Proxy(instance, {
get(t,k,r){
// 取得组件自身状态与props数据
const {state, props } = t
// 先尝试读取自身状态数据
if(state && k in state){
return state[k]
}else if(k in props){ // 如果组件自身没有改数据,则尝试从props中读取
return props[k]
}else if(setupState && k in setupState){
// 渲染上下文需要增加对setupState的支持
return setupState[k]
}else{
console.error('不存在')
}
},
set(t,k,v,r){
const {state, props} = t
if(state && k in state){
state[k] = v
}else if(k in props){
props[k] = v
}else if(setupState && k in setupState){
// 渲染上下文需要增加对setupState的支持
setupState[k] = v
}else{
console.error('不存在')
}
}
})
// 省略部分代码
}
上面是setup函数的最小实现,要注意的是:
- setupContext是一个对象
- 检测setup函数的返回值类型来决定如何处理。
- 渲染上下文renderContext应该正确地处理setupState,因为setup函数返回的数据状态也应该暴露到渲染环境
emit用来发射组件的自定义事件,如下面代码:
const MyComponent = {
name: 'MyComponent',
setup(props,{emit}){
// 发射change时间,并传递给事件处理函数两个参数
emit('change',1,2)
return()=>{
return //...
}
}
}
当使用该组件时,可以监听由emit函数发射的自定义事件
<MyComponent @change="handler" />
上面这段对应的虚拟DOM是
const CompVNode = {
type: MyComponent,
props:{
onChange: handler
}
}
可以看到,自定义事件change被编译成名为onChange的属性,并存储在props数据对象中,这实际上是一种约定。
在具体的实现上,发射自定义事件的本质就是根据事件名称去props数据对象中寻找对应的时间处理函数并执行,如下面代码所示:
function mountComponent(vnode, container, anchor){
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
}
// 定义emit函数,接收两个参数
// event:事件名称
// payload: 传递给事件处理函数的参数
function emit(event, ...payload){
const eventName = `on${event[0].toUpperCase()+event.slice(1)}`
// 根据处理后的事件名称去props中寻找对应的时间处理函数
const handler = instance.props[eventName]
if(handler){
// 调用时间处理函数并传递参数
handler(...payload)
}else{
console.error('事件不存在')
}
}
// 将emit函数添加到setupContext中,用户可以通过setupContext取得emit函数
const setupContext = {attrs, emit}
}
这里要额外注意的是,在介绍props时提到,任何没有显式声明未props的属性都会存储到attrs中,也就是说,任何事件类型的props,即onXxx类的属性,都不会出现在props中,也就无法根据事件名称在instance.props中找到对应的事件处理函数。
为了解决这个问题,需要再解析props数据的时候对事件类型的props做特殊处理,如下面代码所示:
function resolveProps(options, propsData){
const props = {}
const attrs = {}
for(const key in propsData){
// 以字符串on开头的props, 无论是否显式地声明,都将其添加到props数据中,而不是添加到attrs中
if(key in options || key.startsWith('on')){
props[key] = propsData[key]
}else{
attrs[key] = propsData[key]
}
}
return [props, attrs]
}