渲染subTree
hello 大家好,🙎🏻♀️🙋🏻♀️🙆🏻♀️
我是一个热爱知识传递,正在学习写作的作者,ClyingDeng
凳凳!
在实现 subTree 的赋值后,我们需要去渲染这个 subTree,然后把渲染的结果赋值到subTree上。
节点初始化
上文输出的subTree结果:
从中可以看出shapeFlag的值是9,代表我们的节点是元素也是文本。
我们进行subTree的初次渲染:
patch(null, subTree, container)
在patch中我们之前只写了组件的初始化,并没有考虑元素的情况,因此我们需要判断添加该条件情况:
// 传入的是元素
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container)
}
在processElement
中处理元素。
const processElement = (n1, n2, container) => {
if (n1 == null) {
// 元素初始化
mountElement(n2, container)
} else {
// 元素更新
patchElement(n2, container)
}
}
如果一开始没有旧节点,就初始化元素;有,就进行对比更新。在此我们以mountElement
初始化为例。
拿到我们的vnode,我们最先要做的就是创建真实元素(hostCreateElement
),然后插入到相应的元素位置(hostInsert
)。再将真实元素挂载到vnode上,方便后续使用。
接着我们处理vnode的children。mountElement
接收的vnode的children可能是字符串、数组、对象数组、字符串数组。
如果孩子是文本,我们直接创建文本节点hostSetElementText(el, vnode.children as string)
。
const mountElement = (vnode, container) => {
const { type, props, shapeFlag, children } = vnode
let el = (vnode.el = hostCreateElement(vnode.type))
// 处理孩子
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 孩子是文本 创建文本
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 数组
mountChildren(children, el)
}
hostInsert(el, container)
}
如果孩子是数组,我们就需要再次处理,走到mountChildren
这个函数。
循环children,传入的数组可能不是vnode对象数组,我们就要将其统一格式化成对象数组,再进行初始化孩子节点。
const mountChildren = (children, container) => {
// 如果是一个文本 直接el.textContent = children
// 如果是数组 ['文本1','文本2'] 就需要创建两个文本节点 塞入到元素中
for (let i = 0; i < children.length; i++) {
// normalizeVNode 将字符串格式化成vnode对象
const child = children[i] = normalizeVNode(children[i])
patch(null, child, container) // 文本需要特殊处理
}
}
其中normalizeVNode
这个函数就是将字符串或数字组成的数组转成vnode对象数组。
export const Text = Symbol()
export function normalizeVNode(child) {
if (typeof child === 'object') {
// children arrays
return child
} else {
// strings and numbers
return createVNode(Text, null, String(child))
}
}
我们这边考虑的比较简单,源码中判断的情况就比较全面了:
再次初始化孩子节点时,如果是文本情况,我们就需要特殊处理一下文本。
const patch = (n1, n2, container) => {
if (n1 === n2) return
const { type, shapeFlag } = n2
switch (type) {
case Text: // 文本
processText(n1, n2, container)
break
default:
// 传入的是元素
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container)
}
// 组件
else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container)
}
}
}
参照源码,在此我也是用switch去判断,如果是文本情况就需要再去创建初始化文本--processText。
const processText = (n1, n2, container) => {
if (n1 === null) {
// 将n2初始化成文本,然后插入相应位置
hostInsert((n2.el = hostCreateText(n2.children as string)),
container,)
}
else {
const el = (n2.el = n1.el)
if (n2.children !== n1.children) { // 新孩子不等于旧孩子 直接替换
hostSetText(el, n2.children as string)
}
}
}
和之前处理元素类似,如果一开始没有,就创建文本并将其插入到相应位置;如果两个节点都存在,我们用新的代替老的,创建新的文本节点。
当然不仅仅是文本需要特殊处理,还存在其他情况,比如说注释等。
元素孩子节点的初始化就此可以告一段落了,我们可以看下案例结果:
属性初始化
在元素初始化的时候,我们可以通过hostPatchProp
进行属性初始化:
const mountElement = (vnode, container) => {
// vnode中的children 可能是字符串 数组 对象数组 字符串数组
const { type, props, shapeFlag, children } = vnode
let el = (vnode.el = hostCreateElement(vnode.type))
// 处理孩子
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 孩子是文本 创建文本
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 数组
mountChildren(children, el)
}
// 处理属性
if (props) {
for (const key in props) {
hostPatchProp(el,key,null,props[key]) //给元素添加属性
}
}
hostInsert(el, container)
}
结果验证如下图:
可以看到点击文字时,成功触发add方法,并输出count自加后的结果。
这样我们的元素和属性就已经初始化完成了。
感兴趣的朋友可以关注 手写vue3系列 专栏或者点击关注作者ClyingDeng哦(●'◡'●)!。 如果不足,请多指教。
转载自:https://juejin.cn/post/7207421111076012090