欢迎大家来到IT世界,在知识的湖畔探索吧!
引言
<<往期回顾>>
- vue3源码分析——rollup打包monorepo
- vue3源码分析——实现组件的挂载流程
- vue3源码分析——实现props,emit,事件处理等
本期来实现, slot——插槽,分为普通插槽,具名插槽,作用域插槽,所有的源码请查看
正文
在 模板中使用插槽的方式如下:
<todo-button> Add todo </todo-button> 复制代码
欢迎大家来到IT世界,在知识的湖畔探索吧!
在template中的内容最终会被complie成render函数,render函数里面会调用h函数转化成vnode,在vnode的使用方法如下:
欢迎大家来到IT世界,在知识的湖畔探索吧!render() { return h(TodoButton, {}, this.$slots.default) }, 复制代码
看完slots的基本用法,一起来实现个slots,方便自己理解slots的原理哦!
实现基本的用法
使用slots的地方是this.slots,并且调用的属性是default,那么slots,并且调用的属性是default,那么slots则是一个对象,对象里面有插槽的名称,如果使用者没有传递,则可以通过default来进行访问。
测试用例
attention!!! 由于测试的是dom,需要先写入html等,在这里需要先创建对应的节点
let appElement: Element; beforeEach(() => { appElement = document.createElement('div'); appElement.id = 'app'; document.body.appendChild(appElement); }) afterEach(() => { document.body.innerHTML = ''; }) 复制代码
本案例的测试正式开始
欢迎大家来到IT世界,在知识的湖畔探索吧! test('test basic slots', () => { // 子组件Foo const Foo = { name: 'Foo', render() { return h('div', { class: 'foo' }, [h('p', {}, this.count), renderSlots(this.$slots)]); } } const app = createApp({ render() { return h('div', { class: 'container' }, [ h(Foo, { count: 1 }, { default: h('div', { class: 'slot' }, 'slot1') }), h(Foo, { count: 2 }, { default: [h('p', { class: 'slot' }, 'slot2'), h('p', { class: 'slot' }, 'slot2')] }), ]) } }) const appDoc = document.querySelector('#app') app.mount(appDoc); // 测试挂载的内容是否正确 const container = document.querySelector('.container') as HTMLElement; expect(container.innerHTML).toBe('<div class="foo"><p>1</p><div><div class="slot">slot1</div></div></div><div class="foo"><p>2</p><div><p class="slot">slot2</p><p class="slot">slot2</p></div></div>' ) }) 复制代码
需求分析
通过上面的测试用例,可以分析以下内容:
- 父组件使用子组件传入插槽的方式是在h的第三个参数,并且传入的是一个对象,value的值可以是对象,或者是数组
- 子组件中使用插槽的时候,是在this.$slots中获取的
- 并且还实现了一个renderSlot的方法,renderSlot是将this.$slots调用h转变为vnode
问题解决:
- 需要在绑定在this上面,那就在setupStatefulComponent函数代理中加入判断,传入的$slots ;
- 判断$slot是否在组件的代理中,然后代理需要把slots绑定在instance上面并且绑定值的时候需要把传入的对象统一转成数组;
- renderSlot方法调用了h函数,把一个数据转成vnode
编码实现
// 需要把$slots绑定在this上面,那就需要在代理里面在加入一个判断即可 function setupStatefulComponent(instance: any) { // 代理组件的上下文 instance.proxy = new Proxy({ }, { get(target,key){ // 省略其他 else if(key in instance.slots){ return instance.slots[key] } } }) } // 接下里在instance上面加上slots属性 export function setupComponent(instance) { // 获取props和children const { props, children } = instance.vnode // 处理props const slots = {} for (const key in children) { slots[key] = Array.isArray(children[key]) ? children[key] : [children[key]] } instance.slots = slots // ……省略其他 } // 最后还需要使用renderSlot函数 export function renderSlots(slots) { const slot = slots['default'] if (slot) { return createVNode('div', {}, slot) } } 复制代码
通过上面的编码,测试用例就可以完美通关啦
具名插槽
具名插槽就是,插槽除了可以有多个,并且除了default外,可以加入其他的名字,具体请看测试用例
测试用例
test('测试具名插槽', () => { const Foo = { name: 'Foo', render() { return h('div', { class: 'foo' }, [ renderSlots(this.$slots, 'header'), h('div', { class: 'default' }, 'default'), renderSlots(this.$slots, 'footer') ] ); } } const app = createApp({ name: 'App', render() { return h('div', { class: 'container' }, [h(Foo, {}, { header: h('h1', {}, 'header'), footer: h('p', {}, 'footer') })]) } }) const appDoc = document.querySelector('#app') app.mount(appDoc); const container = document.querySelector('.container') as HTMLElement expect(container.innerHTML).toBe('<div class=\"foo\"><div><h1>header</h1></div><div class=\"default\">default</div><div><p>footer</p></div></div>') }) 复制代码
分析
通过上面测试用例,发现以下内容:
- renderSlot传入第二个参数,然后可以获取对于的slots
问题解决
直接在renderSlot里面传入第二个参数即可
编码
// 最后还需要使用renderSlot函数 export function renderSlots(slots, name = 'default') { const slot = slots[name] if (slot) { return createVNode('div', {}, slot) } } 复制代码
这一步是不是比较简单,相对起前面来说,正所谓,前面考虑好了,后面就舒服,接下来实现作用域插槽
作用域插槽
作用域插槽是,每个slot里面可以传入数据,数据只在当前的slot有效,具体请看测试用例
测试用例
test('测试作用域插槽', () => { const Foo = { name: 'Foo', render() { return h('div', { class: 'foo' }, [ renderSlots(this.$slots, 'header', { children: 'foo' }), h('div', { class: 'default' }, 'default'), renderSlots(this.$slots, 'footer') ] ); } } const app = createApp({ name: 'App', render() { return h('div', { class: 'container' }, [h(Foo, {}, { header: ({ children }) => h('h1', {}, 'header ' + children), footer: h('p', {}, 'footer') })]) } }) const appDoc = document.querySelector('#app') app.mount(appDoc); const container = document.querySelector('.container') as HTMLElement expect(container.innerHTML).toBe('<div class=\"foo\"><div><h1>header foo</h1></div><div class=\"default\">default</div><div><p>footer</p></div></div>') }) 复制代码
需求分析
通过上面的测试用例,分析出以下内容:
- 传入插槽的时候,传入一个函数,函数可以拿到子组件传过来的参数
- renderSlots可以传入第三个参数props, 用于接收子组件往父组件传入的参数
问题解决:
- 问题1: 只需要在传入插槽的时候进行一下判断,如果是函数的话,需要进行函数执行,并且传入参数
- 问题2: 也是对传入的内容进行判断,函数做传入参数处理
编码
// 在renderSlot里面传入第三个参数 export function renderSlots(slots, name = 'default', props = {}) { const slot = slots[name]; if (slot) { if (isFunction(slot)) { return createVNode('div', {}, slot(props)) } return createVNode('div', {}, slot) } } // initSlot时候,需要进行函数判断 const slots = {} // 遍历children for (const key in children) { // 判断传入的是否是函数,如果是函数的话,需要进行执行,并且传入参数 if (isFunction(children[key])) { slots[key] = (props) => Array.isArray(children[key](props)) ? children[key](props) : [children[key](props)] } else { slots[key] = Array.isArray(children[key]) ? children[key] : [children[key]] } } instance.slots = slots 复制代码
到此,整个测试用例就可以完美通过啦!
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/73193.html