引言
<<往期回顧>>
- vue3源碼分析——rollup打包monorepo
- vue3源碼分析——實現(xiàn)組件的掛載流程
- vue3源碼分析——實現(xiàn)props,emit,事件處理等
本期來實現(xiàn), slot——插槽,分為普通插槽,具名插槽,作用域插槽,所有的源碼請查看
正文
在 模板中使用插槽的方式如下:
<todo-button>
Add todo
</todo-button>
復(fù)制代碼
在template中的內(nèi)容最終會被complie成render函數(shù),render函數(shù)里面會調(diào)用h函數(shù)轉(zhuǎn)化成vnode,在vnode的使用方法如下:
render() {
return h(TodoButton, {}, this.$slots.default)
},
復(fù)制代碼
看完slots的基本用法,一起來實現(xiàn)個slots,方便自己理解slots的原理哦!
實現(xiàn)基本的用法
使用slots的地方是this.slots,并且調(diào)用的屬性是default,那么slots,并且調(diào)用的屬性是default,那么slots則是一個對象,對象里面有插槽的名稱,如果使用者沒有傳遞,則可以通過default來進(jìn)行訪問。
測試用例
attention!!! 由于測試的是dom,需要先寫入html等,在這里需要先創(chuàng)建對應(yīng)的節(jié)點
let AppElement: Element;
beforeEach(() => {
appElement = document.createElement('div');
appElement.id = 'app';
document.body.appendChild(appElement);
})
afterEach(() => {
document.body.innerHTML = '';
})
復(fù)制代碼
本案例的測試正式開始
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);
// 測試掛載的內(nèi)容是否正確
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>'
)
})
復(fù)制代碼
需求分析
通過上面的測試用例,可以分析以下內(nèi)容:
- 父組件使用子組件傳入插槽的方式是在h的第三個參數(shù),并且傳入的是一個對象,value的值可以是對象,或者是數(shù)組
- 子組件中使用插槽的時候,是在this.$slots中獲取的
- 并且還實現(xiàn)了一個renderSlot的方法,renderSlot是將this.$slots調(diào)用h轉(zhuǎn)變?yōu)関node
問題解決:
- 需要在綁定在this上面,那就在setupStatefulComponent函數(shù)代理中加入判斷,傳入的$slots ;
- 判斷$slot是否在組件的代理中,然后代理需要把slots綁定在instance上面并且綁定值的時候需要把傳入的對象統(tǒng)一轉(zhuǎn)成數(shù)組;
- renderSlot方法調(diào)用了h函數(shù),把一個數(shù)據(jù)轉(zhuǎn)成vnode
編碼實現(xiàn)
// 需要把$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函數(shù)
export function renderSlots(slots) {
const slot = slots['default']
if (slot) {
return createVNode('div', {}, slot)
}
}
復(fù)制代碼
通過上面的編碼,測試用例就可以完美通關(guān)啦
具名插槽
具名插槽就是,插槽除了可以有多個,并且除了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>')
})
復(fù)制代碼
分析
通過上面測試用例,發(fā)現(xiàn)以下內(nèi)容:
- renderSlot傳入第二個參數(shù),然后可以獲取對于的slots
問題解決
直接在renderSlot里面?zhèn)魅氲诙€參數(shù)即可
編碼
// 最后還需要使用renderSlot函數(shù)
export function renderSlots(slots, name = 'default') {
const slot = slots[name]
if (slot) {
return createVNode('div', {}, slot)
}
}
復(fù)制代碼
這一步是不是比較簡單,相對起前面來說,正所謂,前面考慮好了,后面就舒服,接下來實現(xiàn)作用域插槽
作用域插槽
作用域插槽是,每個slot里面可以傳入數(shù)據(jù),數(shù)據(jù)只在當(dāng)前的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>')
})
復(fù)制代碼
需求分析
通過上面的測試用例,分析出以下內(nèi)容:
- 傳入插槽的時候,傳入一個函數(shù),函數(shù)可以拿到子組件傳過來的參數(shù)
- renderSlots可以傳入第三個參數(shù)props, 用于接收子組件往父組件傳入的參數(shù)
問題解決:
- 問題1: 只需要在傳入插槽的時候進(jìn)行一下判斷,如果是函數(shù)的話,需要進(jìn)行函數(shù)執(zhí)行,并且傳入?yún)?shù)
- 問題2: 也是對傳入的內(nèi)容進(jìn)行判斷,函數(shù)做傳入?yún)?shù)處理
編碼
// 在renderSlot里面?zhèn)魅氲谌齻€參數(shù)
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時候,需要進(jìn)行函數(shù)判斷
const slots = {}
// 遍歷children
for (const key in children) {
// 判斷傳入的是否是函數(shù),如果是函數(shù)的話,需要進(jìn)行執(zhí)行,并且傳入?yún)?shù)
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
復(fù)制代碼
到此,整個測試用例就可以完美通過啦!