Appearance
一、render函数
这个render和组件中的render不是同一个render,它是用来渲染根组件的
render函数的作用是将虚拟节点(vnode)渲染到指定容器(container)中。具体来说,分为三步:挂载、更新和卸载。
- **挂载:**如果容器中没有之前的虚拟节点(
container._vnode),则直接将新的虚拟节点挂载到容器中。 - **更新:**如果容器中有之前的虚拟节点,则对比新旧虚拟节点,并进行更新操作。
- **卸载:**如果传入的虚拟节点为
null,则卸载容器中现有的虚拟节点。
函数具体逻辑如下:
- 如果传入的虚拟节点为
null,且容器中有之前的虚拟节点,则调用unmount函数卸载之前的虚拟节点。 - 否则,调用
patch函数进行挂载或更新操作。 - 最后,将新的虚拟节点保存到容器
_vnode属性中,以便下次更新时使用。
ts
// renderer.ts
const render = (vnode, container) => {
/**
* 分三步
* 1. 挂载
* 2. 更新
* 3. 卸载
*/
if(vnode == null) {
if(container._vnode) {
// 卸载
unmount(container._vnode)
}
} else {
// 挂载和更新
patch(container._vnode)
}
container._vnode = vnode
}二、patch函数
patch函数的作用是用于更新和挂载虚拟节点(vnode),具体来说,它会根据传入的老节点(n1)和新节点(n2)的情况,决定是进行挂载操作还是更新操作。函数的逻辑如下:
- **相同节点检查:**如果传入的老节点和新节点是同一个节点,则不进行任何操作。
- **类型检查:**如果老节点存在且老节点和新节点的类型不同,则卸载老节点,并将老节点设置为
null。 - **挂载:**如果老节点为
null,则直接挂载新节点到容器中。 - **更新:**如果老节点存在且类型相同,则进行更新操作。
总的来说,这个函数通过对比老节点和新节点,决定是进行挂载还是更新,从而实现虚拟节点的高效渲染。
ts
/**
* 更新和挂载,都用这个函数
* @param n1 老节点,如果有,表示要和n2做diff。如果没有,直接挂载n2
* @param n2 新节点
* @param container 要挂载的容器
*/
const patch = (n1, n2, container) {
if(n1 === n2) {
return
}
if(n1 && !isSameVNodeType(n1, n2)) {
// 如果两个节点不是同一个类型,那就卸载 n1 直接挂载 n2
unmount(n1)
n1 = null
}
if(n1 == null) {
// 挂载元素
mountElement(n2, container)
} else {
// 更新元素
patchElement(n1, n2)
}
}这里用到了一个辅助函数isSameVNodeType,它是用来判断这个节点是否可以复用,逻辑如下:
ts
// vnode.ts
export function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key
}可以看到这个判断逻辑中,必须是相同的type,并且key也相同,才可以复用。也就是说,div和p两个标签,不能复用,不同的key也是如此。但是如果没有传递key,那就表示key是undefined,两个undefined是相同的,所以没传key,就意味着key相等。
三、unmount函数
unmount函数会卸载虚拟节点(vnode)。具体来说,它会根据虚拟节点的类型和子节点的情况,递归卸载所有子节点,并最终移除对应的DOM元素。
- **检查子节点类型:**如果虚拟节点的子类型是数组类型,则递归卸载所有子节点。
- **移除DOM元素:**调用
hostRemove函数移除虚拟节点对应的DOM元素。
ts
// 卸载子元素
const unmountChildren = (children) => {
for(let i = 0; i < children.length; i++) {
unmount(children[i])
}
}
// 卸载
const unmount = (vnode) => {
// 卸载
const { type, shapeFlag, children } = vnode
if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 子节点是数组
unmountChildren(children)
}
// 移除dom元素
hostRemove(vnode.el)
}四、mountElement
mountElement函数会将虚拟节点(vnode)挂载到指定的容器(container)中。具体涞水,它分为一下几个步骤:
- **创建一个DOM节点:**根据虚拟节点的类型(
type),创建一个对应的DOM元素,并将其赋值给虚拟节点的el属性。 - **设置节点的属性:**遍历虚拟节点的属性(
props),并使用hostPatchProp函数将这些属性设置到刚创建的DOM元素上。 - **挂载子节点:**根据虚拟节点的
shapeFlag判断节点的类型。如果子节点是文本,则使用hostSetElementText函数设置文本内容,如果子节点是数组,则递归调用mountChildren函数挂载每个子节点。 - **插入到容器中:**最后,将创建好的DOM元素插入到指定的容器中。
ts
// 挂载节点
const mountElement = (vnode, container) => {
/**
* 1. 创建一个dom节点
* 2. 设置他的props
* 3. 挂载他的子节点
*/
const { type, props, children, shapeFlag } = vnode
// 创建dom袁术 type = div p span
const el = hostCreateElement(type)
vnode.el = el
if(props) {
for(const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
// 处理子节点
if(shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 子节点是文本
hostSetElementText(el, children)
} else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 子节点是数组
mountChildren(children, el)
}
// 把 el 插入到 container 中
hostInsert(el, container)
}
// 挂载子元素
const mountChildren = (children, el) => {
for (let i = 0; i < children.length; i++) {
const child = children[i]
// 递归挂载子节点
patch(null, child, el)
}
}五、patchElement
patchElement函数的作用是更新已经存在的DOM元素,以便复用现有的DOM结构并应用新的属性和子节点。具体来说,它分一下几个步骤:
- **复用DOM元素:**将旧节点(
n1)的DOM元素(el)赋值给新虚拟节点(n2),一边复用现有的DOM元素。 - **更新属性(props):**调用
patchProps函数,对比旧属性(oldProps)和新属性(newProps),并应用属性的变化。 - **更新子节点:**调用
patchChildren函数,对比旧节点和新子节点,并应用子节点变化。
ts
const patchElement = (n1, n2) => {
/**
* 1. 复用dom元素
* 2. 更新props
* 3. 更新children
*/
const oldProps = n1.props
const newProps = n2.props
patchProps(el, oldProps, newProps)
}六、patchProp
patchProp函数的作用是更新DOM元素的属性(props)。具体来说,执行以下操作:
- **清楚旧属性:**如果存在旧属性(
oldProps),会遍历所有旧属性,并调用hostPatchProp函数将每个属性从DOM元素上移除。这是通过将新值设为null实现的。 - **设置新属性:**如果存在新属性(
newProps),会遍历所有新属性,并调用hostPatchProp函数将每个属性设置到DOM元素上。会传入旧的属性值和新的属性值,以便hostPatchProp函数能够进行更智能的更新。
ts
const patchProps = (el, oldProps, newProps) => {
/**
* 1. 把老的props删除掉
* 2. 把新的props全部设置上
*/
if(oldProps) {
// 把老的props干掉
for(const key in oldProps) {
hostPatchProp(el, key, oldProps[key], null)
}
}
if(newProps) {
for(const key in newProps) {
hostPatchProps(el, key, oldProps?.[key], newProps[key])
}
}
}七、patchChildren
patchChildren函数赋值更新子元素,由于子元素的情况比较多,有一下情况:
- 新的子元素是文本
- 老节点是数组,卸载老的
children,将新的文本设置成children - 老的是文本,直接替换
- 老的是
null,不用关心老的,将新的设置成children
- 老节点是数组,卸载老的
- 新的子元素是数组
- 老的是数组,那就和新的做全量
diff - 老的是文本,把老的清空,挂载新的
children - 老的是
null,不用关心老的,直接挂载新的children
- 老的是数组,那就和新的做全量
- 新的子元素是null
- 老的是文本,把
children设置成空 - 老的是数组,卸载老的
- 老的是
null,俩个都是null,不作操作。
- 老的是文本,把
ts
const patchChildren = (n1, n2) => {
const el = n2.el
/**
* 1. 新节点n2的子节点是文本
* 1.1 老的是数组
* 1.2 老的也是文本
* 2. 新节点子节点是数组 或者null
* 2.1 老的也是数组
* 2.2 老的是文本
* 2.3 老的可能是null
*/
const prevShapeFlag = n1.shapeFlag
const shapeFlag = n2.shapeFlag
if(shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 新的是文本
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 老的是数组,把老的children卸载掉
unmountChildren(n1.children)
}
if (n1.children !== n2.children) {
// 设置文本,如果n1和n2的children不一样
hostSetElementText(el, n2.children)
}
} else {
// 老的有可能是数组或者null 或者文本
// 新的是数组,或者null, 老的也有可能是null
// 老的是文本
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 把老的子元素节点干掉
hostSetElementText(el, '')
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 挂载新的节点
mountChildren(n2.children, el)
}
} else {
// 老的数组或者null
// 新的是数组或者null
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 老的是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新的也是数组, 全量diff
} else {
// 新的不是数组,卸载老的数组
unmountChildren(n1.children)
}
} else {
// 老的是null
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新的是数组
mountChildren(n2.children, el)
}
}
}
}
}