VueStudyNote
VueStudyNote copied to clipboard
24 实现插槽
实现思路
插槽看上去很高级很复杂,其实实现起来并不复杂!
插槽的真正内容是在父组件上的,所以创建子组件之前,子组件里面的数据对应的vnode已经存在了。这里使用,我们把它归类到实例的 $slots
对象上,它是一个数组的对象。
然后再子组件渲染的时候,再把他的数据(就是vnode)拿出来,当到子组件的child里面,snabbdom自动就会渲染出来了!
归类子组件到$slots
再创建子组件的时候,增加代码。把 父组件
里面的vnode根据插槽归类。
/**
* 实现组件功能
*
* 采用snabbdom的hook,在insert和update的时候更新数据。
*
* @param {*} vnode
* @param {*} vm
*/
function setComponentHook(vnode: any, vm: Xiao) {
if (!vnode.sel) {
return
}
// 查看是否组成了组件?
const Comp = Xiao.component(vnode.sel)
if (Comp) {
vnode.data.hook = {
insert: (vnode) => {
log('component vnode', vnode)
// 创建子组件实例
let app = new Comp()
app.$parent = vm
const propsData = vnode.data.props
// 把计算后的props数据代理到当前vue里面
initProps(app, propsData)
// 处理插槽,把插槽归类
resolveSlots(app, vnode.children)
// 绑定事件
if(vnode.data.on){
initEvent(app, vnode.data.on)
}
// 保存到vnode中,更新的时候需要取出来用
vnode.childContext = app
// 渲染
app.$mount(vnode.elm)
},
update: (oldvnode, vnode) => {
const app = oldvnode.childContext
// 更新update属性
updateProps(app, vnode.data.props)
vnode.childContext = app
}
}
}
// 递归
if (vnode.children) {
vnode.children.forEach(function (e) {
setComponentHook(e, vm)
}, this)
}
}
/**
* 归类插槽
*
* @param {*} vm
* @param {*} children
*/
function resolveSlots(vm: Xiao, children: Array<any>){
log('resolveSlots', children)
vm.$slots = {}
children.forEach(vnode =>{
let slotname = 'default'
if(vnode.data.props && vnode.data.props.slot){
slotname = vnode.data.props.slot
delete vnode.data.props.slot
}
(vm.$slots[slotname] || (vm.$slots[slotname] = [])).push(vnode)
})
log('resolveSlots end', vm.$slots)
}
修改渲染函数
/**
* 根据元素AST生成渲染函数。
*
* 如果是插槽,生成 _t(插槽名字, [默认插槽内容])
* 否则生成 h(tag, 属性。。。)
*
* @param {*} node
*/
function createRenderStrElemnet(node: any): string {
log('createRenderStrElemnet', node)
let str: string
// 插槽使用 _t 函数, 参数为插槽名字
if (node.tag == 'slot') {
log('slot node', node)
const slot = node.attrsMap.name || "default"
str = `_t("${slot}",[`
if (node.children && node.children.length > 0) {
// 生成插槽默认的子组件的渲染函数
for (let i = 0; i < node.children.length; i++) {
str += createRenderStr(node.children[i])
if (i != node.children.length - 1) {
str += ','
}
}
}
str += '])'
return str
}
// snabbdom 的语法,类名放在tag上。'div#container.two.classes'
let tagWithIdClass = getTagAndClassName(node)
str = `h(${tagWithIdClass},{`
// 解析指令
str += getDirectiveStr(node)
// 解析属性
str += genAttrStr(node)
str += "}"
if (node.children) {
str += ',['
// 保存上一次if指令,处理只有if没有else的场景
let lastDir
node.children.forEach(child => {
// 如果这里节点有if指令
let dir = getIfElseDirective(child)
console.log('dir:', dir)
if (dir) {
if (dir.name == 'if') {
str += `(${dir.exp})?`
lastDir = dir
} else if (dir.name == 'else') {
str += `:`
}
}
str += createRenderStr(child)
if (dir) {
if (dir.name == 'else') {
str += `,`
lastDir = null
}
}
else if (lastDir) {
str += `:"",`
lastDir = null
}
else {
str += `,`
}
})
if (lastDir) {
str += `:"",`
}
str += ']'
}
str += ')'
return str
}
实现插槽渲染函数
class Xiao{
/**
* 插槽渲染函数
*
* vue里面是 _t = renderSlot
* @param {*} slot
*/
_t(slot: string, child: ?any){
// 如果父节点没有制定插槽内容,那么返回默认值(是个数组)
return this.$slots[slot] || child
}
}
更新组件的时候,把父组件的数据填充到子元素上
渲染函数执行之后,插槽渲染函数 _t
执行返回vnode节点数组。把他打散到原来的数组里面即可。
/**
* 渲染组件
*
* @param {*} vm
*/
function updateComponent(vm: Xiao) {
let proxy = vm
// 虚拟dom里面的创建函数
proxy.h = h
// 新的虚拟节点
// 指令的信息已经自动附带再vnode里面
let vnode = vm.$render.call(proxy, h)
log('before expandSlotArray: ', vnode)
// 插槽后child里面应该为节点的可能变成了数组,所以要单独处理一下
expandSlotArray(vnode)
log('after expandSlotArray: ', vnode)
// 把实例绑定到vnode中,处理指令需要用到
setContext(vnode, vm)
// 处理子组件
setComponentHook(vnode, vm)
// 上一次渲染的虚拟dom
let preNode = vm.$options.oldvnode;
log(`[lifecycle][uid:${vm._uid}] 第${++vm._renderCount}次渲染`)
if (preNode) {
vnode = patch(preNode, vnode)
}
else {
vnode = patch(vm.$el, vnode)
}
log('vnode', vnode)
// 保存起来,下次patch需要用到
vm.$options.oldvnode = vnode;
}
测试代码
<h1>slot测试</h1>
<div id="demo">
<h1>我是父组件的标题</h1>
<my-component>
<p>这是一些初始内容{{message}}</p>
<p>这是更多的初始内容</p>
<h3 slot="header">这是父组件放到子组件头插槽的数据</h3>
<h2 slot="foot2">放到foot2插槽{{message}}</h2>
</my-component>
</div>
<script>
Xiao.component('my-component', {
data: function(){
return {
aa: 'child message'
}
},
template: `
<div>
<slot name="header"></slot>
<h2>我是子组件的标题{{aa}}</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
<slot name="foot"><h2>默认的foot slot:{{aa}}</h2></slot>
<slot name="foot2">默认的foot2 slot:{{aa}}</slot>
</div>
`
})
var app = new Xiao({
el: '#demo',
data:{
message : 'parent message'
}
})
</script>