模拟实现
2023年06月11日
一、/src/compiler/parse.js parse()
function processElement() {
// ...
// 处理插槽内容
processSlotContent(curEle)
// 节点处理完以后让其和父节点产生关系
if (stackLen) {
stack[stackLen - 1].children.push(curEle)
curEle.parent = stack[stackLen - 1]
// 如果节点存在 slotName,则说明该节点是组件传递给插槽的内容
// 将插槽信息放到组件节点的 rawAttr.scopedSlots 对象上
// 而这些信息在生成组件插槽的 VNode 时(renderSlot)会用到
if (curEle.slotName) {
const { parent, slotName, scopeSlot, children } = curEle
// 这里关于 children 的操作,只是单纯为了避开 JSON.stringify 的循环引用问题
// 因为生成渲染函数时需要对 attr 执行 JSON.stringify 方法
const slotInfo = {
slotName, scopeSlot, children: children.map(item => {
delete item.parent
return item
})
}
if (parent.rawAttr.scopedSlots) {
parent.rawAttr.scopedSlots[curEle.slotName] = slotInfo
} else {
parent.rawAttr.scopedSlots = { [curEle.slotName]: slotInfo }
}
}
}
}
二、/src/compiler/parse.js processSlotContent()
/**
* 处理插槽
* <scope-slot>
* <template v-slot:default="scopeSlot">
* <div>{{ scopeSlot }}</div>
* </template>
* </scope-slot>
* @param { AST } el 节点的 AST 对象
*/
function processSlotContent(el) {
// 注意,具有 v-slot:xx 属性的 template 只能是组件的根元素,这里不做判断
if (el.tag === 'template') { // 获取插槽信息
// 属性 map 对象
const attrMap = el.rawAttr
// 遍历属性 map 对象,找出其中的 v-slot 指令信息
for (let key in attrMap) {
if (key.match(/v-slot:(.*)/)) { // 说明 template 标签上 v-slot 指令
// 获取指令后的插槽名称和值,比如: v-slot:default=xx
// default
const slotName = el.slotName = RegExp.$1
// xx
el.scopeSlot = attrMap[`v-slot:${slotName}`]
// 直接 return,因为该标签上只可能有一个 v-slot 指令
return
}
}
}
}
三、/src/compiler/generate.js generate()
/**
* 解析 ast 生成 渲染函数
* @param {*} ast 语法树
* @returns {string} 渲染函数的字符串形式
*/
function genElement(ast) {
// ...
// 处理子节点,得到一个所有子节点渲染函数组成的数组
const children = genChildren(ast)
if (tag === 'slot') {
// 生成插槽的处理函数
return `_t(${JSON.stringify(attrs)}, [${children}])`
}
// 生成 VNode 的可执行方法
return `_c('${tag}', ${JSON.stringify(attrs)}, [${children}])`
}
四、/src/compiler/renderHelper.js renderHelper()
/**
* 在 Vue 实例上安装运行时的渲染帮助函数,比如 _c、_v,这些函数会生成 Vnode
* @param {VueContructor} target Vue 实例
*/
export default function renderHelper(target) {
// ...
target._t = renderSlot
}
五、/src/compiler/renderHelper.js renderSlot()
/**
* 插槽的原理其实很简单,难点在于实现
* 其原理就是生成 VNode,难点在于生成 VNode 之前的各种解析,也就是数据准备阶段
* 生成插槽的的 VNode
* @param {*} attrs 插槽的属性
* @param {*} children 插槽所有子节点的 ast 组成的数组
*/
function renderSlot(attrs, children) {
// 父组件 VNode 的 attr 信息
const parentAttr = this._parentVnode.attr
let vnode = null
if (parentAttr.scopedSlots) { // 说明给当前组件的插槽传递了内容
// 获取插槽信息
const slotName = attrs.name
const slotInfo = parentAttr.scopedSlots[slotName]
// 这里的逻辑稍微有点绕,建议打开调试,查看一下数据结构,理清对应的思路
// 这里比较绕的逻辑完全是为了实现插槽这个功能,和插槽本身的原理没关系
this[slotInfo.scopeSlot] = this[Object.keys(attrs.vBind)[0]]
vnode = genVNode(slotInfo.children, this)
} else { // 插槽默认内容
// 将 children 变成 vnode 数组
vnode = genVNode(children, this)
}
// 如果 children 长度为 1,则说明插槽只有一个子节点
if (children.length === 1) return vnode[0]
return createElement.call(this, 'div', {}, vnode)
}
六、/src/compiler/renderHelper.js genVNode()
/**
* 将一批 ast 节点(数组)转换成 vnode 数组
* @param {Array<Ast>} childs 节点数组
* @param {*} vm 组件实例
* @returns vnode 数组
*/
function genVNode(childs, vm) {
const vnode = []
for (let i = 0, len = childs.length; i < len; i++) {
const { tag, attr, children, text } = childs[i]
if (text) { // 文本节点
if (typeof text === 'string') { // text 为字符串
// 构造文本节点的 AST 对象
const textAst = {
type: 3,
text,
}
if (text.match(/{{(.*)}}/)) {
// 说明是表达式
textAst.expression = RegExp.$1.trim()
}
vnode.push(createTextNode.call(vm, textAst))
} else { // text 为文本节点的 ast 对象
vnode.push(createTextNode.call(vm, text))
}
} else { // 元素节点
vnode.push(createElement.call(vm, tag, attr, genVNode(children, vm)))
}
}
return vnode
}