# setup函数的作用与实现
setup函数主要用于配合组合式API,为用户提供一个地方,用于建立组合逻辑、创建响应式数据、创建通用函数,注册生命周期钩子等能力.
在整个组件的生命周期中,setup函数只会在被挂载的时候执行一次,它的返回值有两种情况
- 返回一个函数,将该函数作为组件的render函数
const Comp = {
setup(){
return () => {
return {type:'div',text:'hello'}
}
}
}
2
3
4
5
6
7
这种方式常用与组件不是以模板来表达其渲染内容的情况,否则会与模板编译生成的渲染函数产生冲突.
- 返回一个对象,该对象中的数据将暴露给模板使用.
const Comp = {
setup(){
const count = ref(0)
return {count}
},
render(){
return {type:'div',text:this.count}
}
}
2
3
4
5
6
7
8
9
10
setup函数暴露的数据可以在渲染函数中通过this来访问.
另外 setup函数接收两个参数: props数据对象和setupContext.
const Comp = {
props:{
foo: String
},
setup(props,setupContext){
props.foo
const {slots,attrs,emit,expose} = setupContext
//....
}
}
2
3
4
5
6
7
8
9
10
setup可以获取外部为组件传递的props数据对象,也能获取与组件接口相关的数据和方法.接下来我们来尝试实现setup组件选项.
function mountComponent(vnode, container,anchor) {
const componentOptions = vnode.type
let {data,render,setup} = componentOptions
beforeCreate && beforeCreate()
const state = data ? reactive(data()) : null
const [props,attrs ] = resolveProps(vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
}
const setupContext = { attrs }
//调用setup函数,将只读版本的props作为第一个参数传入
const setupResult = setup(shallowReadonly(props),setupContext)
let setupState = null
//如果setup的返回值是函数,则将其作为渲染函数
if(typeof setupResult === 'function') {
if(render) {
console.warn('setup返回了一个函数,将作为render函数使用')
render = setupResult
} else {
//setup返回一个非函数,则作为数据状态赋值
setupState = setupResult
}
}
vnode.component = instance
const renderContext = new Proxy(instance,{
get(t,k,r) {
const {props,state} = t
if(state && k in state) {
return state[k]
} else if(k in props) {
return props[k]
} else if(setupState && k in setupState) {
//渲染上下文增加对setupState的支持,将返回的数据暴露
return setupState[k]
}
},
set(t,k,v,r) {
const {state,props} = t
if(state && k in state) {
state[k] = v
} else if(k in props) {
console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)
} else if(setupState && k in setupState) {
setupState[k] = v
} else {
console.error('不存在')
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
以上是setup的最小实现,后续我们会继续完善.
# 组件实现与emit的实现
emit用来发射组件的自定义事件.
const MyComp = {
name: 'MyComp',
setup(props, { emit }) {
//发射change事件并传递参数
emit('change', 1, 2)
return () => {
return //.....
}
}
}
2
3
4
5
6
7
8
9
10
11
使用该组件的时候,我们可以监听由emit函数发射的自定义事件.
<MyComp @change="handler" />
上面这段模板对应的虚拟DOM是
const CompVnode = {
type: MyComp,
props: {
onChange: handler
}
}
2
3
4
5
6
在具体的实现上,发射自定义事件的本质就是根据事件名称去props数据对象中寻找对应的事件处理函数并执行.
function mountComponent(vnode, container,anchor) {
//省略..
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
}
//定义emit函数
function emit (event, ...payload) {
//处理事件名称 比如 onClick -> onclick
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
//根据处理后的事件名称去props数据对象中寻找对应的事件处理函数并执行
const handler = instance.props[eventName]
handler && handler(...payload)
}
//将emit函数暴露给setup函数
const setupContext = { attrs, emit }
//省略..
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
有一点就需要额外注意.在讲解props章节的时候提到,没有显式声明的props会被存在attrs中.也就是说任何事件类型的props(onClick...)都不会出现在props中.为了解决这个问题,我们要在resolveProps函数中做特殊处理.
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for(let key in options ) {
if(key in propsData || ker.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
}
2
3
4
5
6
7
8
9
10
11
对所有以'on'开头的字符串做特殊处理,保证事件被收集到props数据对象中.
# 插槽的工作原理与实现
当使用带有插槽的子组件时,可以根据插槽的名字来插入自定义的内容.
<MyComp>
<template #header>
<h1>我是标题</h1>
</template>
<template #body>
<p>我是内容</p>
</template>
<template #footer>
<p>我是尾部</p>
</template>
</MyComp>
2
3
4
5
6
7
8
9
10
11
上面这端附组件模板会被编译成如下渲染函数
function render() {
return {
type: MyComp,
children: {
header() {
return h('h1', '我是标题')
},
body() {
return h('p', '我是内容')
},
footer() {
return h('p', '我是尾部')
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
组件MyComp的模板则会变异为如下渲染函数
function render() {
return [
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
渲染插槽内容的过程就是调用插槽函数并返回其结果.
在运行时的实现上,插槽依赖于setupContext中的slots对象.
function mountComponent(vnode, container,anchor) {
//省略
const slots = vnode.children || {}
const setupContext = { attrs, emit, slots }
}
2
3
4
5
6
最基本的slots实现非常简单.只要将编译好的vnode.children作为slots对象暴露在setupContext中即可.为了render函数内和生命周期钩子函数内能通过this.$slots来访问插槽内容,我们还要在setupContext中特殊对待一下它.
function mountComponent(vnode, container,anchor) {
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots
}
const setupContext = new Proxy(instance,{
get(t,k,r) {
const {props,state,slots} = t
// 当key为$slots时,返回slots对象
if(k === '$slots') {
return slots
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
写于西13