• 【vue设计与实现】组件的实现原理 4 - setup函数的作用与实现 & 组件事件与emit的实现


    组件的setup函数时Vue.js3新增的组件选项,setup函数主要用来配合组合式API,为用户提供一个地方,用于建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等能力
    在组件的整个生命周期中,setup函数只会在挂载时执行一次,其返回值可以有两种情况:

    1. 返回一个函数,该函数将作为组件的render函数:
      const Comp = {
      	setup(){
      		// setup 函数可以返回一个函数,该函数将作为组件的渲染函数
      		return ()=>{
      			return { type:'div', children:'hello' }
      		}
      	}	
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      这种方式常用于组件不是以模板来表达其渲染内容的情况。如果组件以模板来表达其渲染的内容,那么setup函数不可以再返回函数,否则会与模板编译生成的渲染函数产生冲突
    2. 返回一个对象,该对象中包含的数据将暴露给模板使用
      const Comp = {
      	setup(){
      		const count = ref(0)
      		// 返回一个对象,对象中的数据会暴露到渲染函数中
      		return {
      			count
      		}
      	}return ()=>{
      		// 通过this可以访问setup暴露出来的响应式数据
      		return { type:'div', children:`count is: ${this.count}`}
      	}	
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    另外,setup函数接收两个参数,第一个参数是props,第二个参数也是个对象,通常称为setupContext,如下面的代码所示:

    const Comp = {
    	props:{
    		foo: String
    	},
    	setup(props, setupContext){
    		props.foo //访问传入的props数据
    		// setupContext中包含与组件接口相关的重要数据
    		const {slots, emit, attrs, expose} = setupContext
    		// ...
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中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('不存在')
    			}
    		}
    	})
    
    	// 省略部分代码
    
    	
    
    }
    
    • 1
    • 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
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    上面是setup函数的最小实现,要注意的是:

    1. setupContext是一个对象
    2. 检测setup函数的返回值类型来决定如何处理。
    3. 渲染上下文renderContext应该正确地处理setupState,因为setup函数返回的数据状态也应该暴露到渲染环境

    emit用来发射组件的自定义事件,如下面代码:

    const MyComponent = {
    	name: 'MyComponent',
    	setup(props,{emit}){
    		// 发射change时间,并传递给事件处理函数两个参数
    		emit('change',1,2)
    		return()=>{
    			return //...
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当使用该组件时,可以监听由emit函数发射的自定义事件

    
    
    • 1

    上面这段对应的虚拟DOM

    const CompVNode = {
    	type: MyComponent,
    	props:{
    		onChange: handler
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到,自定义事件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}
    }
    
    • 1
    • 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

    这里要额外注意的是,在介绍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]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 相关阅读:
    【调参】如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras
    个人网站搭建学习笔记
    路平石模具对于项目中建设细节的补充
    clickonce 程序发布到ftp在使用cnd 加速https 支持下载,会不会报错
    大语言模型(LLM)综述(四):如何适应预训练后的大语言模型
    Qt-FFmpeg开发-视频播放【软解码】(1)
    MyBatis是如何执行一条SQL语句的
    基于Python的邯郸地标美食导游平台设计与实现-计算机毕业设计源码+LW文档
    JJJ:python学习笔记
    CountDownLatch源码分析
  • 原文地址:https://blog.csdn.net/loyd3/article/details/127666089