Skip to content

一、render函数

这个render和组件中的render不是同一个render,它是用来渲染根组件的

render函数的作用是将虚拟节点(vnode)渲染到指定容器(container)中。具体来说,分为三步:挂载、更新和卸载。

  1. **挂载:**如果容器中没有之前的虚拟节点(container._vnode),则直接将新的虚拟节点挂载到容器中。
  2. **更新:**如果容器中有之前的虚拟节点,则对比新旧虚拟节点,并进行更新操作。
  3. **卸载:**如果传入的虚拟节点为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)的情况,决定是进行挂载操作还是更新操作。函数的逻辑如下:

  1. **相同节点检查:**如果传入的老节点和新节点是同一个节点,则不进行任何操作。
  2. **类型检查:**如果老节点存在且老节点和新节点的类型不同,则卸载老节点,并将老节点设置为null
  3. **挂载:**如果老节点为null,则直接挂载新节点到容器中。
  4. **更新:**如果老节点存在且类型相同,则进行更新操作。

总的来说,这个函数通过对比老节点和新节点,决定是进行挂载还是更新,从而实现虚拟节点的高效渲染。

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也相同,才可以复用。也就是说,divp两个标签,不能复用,不同的key也是如此。但是如果没有传递key,那就表示keyundefined,两个undefined是相同的,所以没传key,就意味着key相等。

三、unmount函数

unmount函数会卸载虚拟节点(vnode)。具体来说,它会根据虚拟节点的类型和子节点的情况,递归卸载所有子节点,并最终移除对应的DOM元素。

  1. **检查子节点类型:**如果虚拟节点的子类型是数组类型,则递归卸载所有子节点。
  2. **移除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)中。具体涞水,它分为一下几个步骤:

  1. **创建一个DOM节点:**根据虚拟节点的类型(type),创建一个对应的DOM元素,并将其赋值给虚拟节点的el属性。
  2. **设置节点的属性:**遍历虚拟节点的属性(props),并使用hostPatchProp函数将这些属性设置到刚创建的DOM元素上。
  3. **挂载子节点:**根据虚拟节点的shapeFlag判断节点的类型。如果子节点是文本,则使用hostSetElementText函数设置文本内容,如果子节点是数组,则递归调用mountChildren函数挂载每个子节点。
  4. **插入到容器中:**最后,将创建好的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结构并应用新的属性和子节点。具体来说,它分一下几个步骤:

  1. **复用DOM元素:**将旧节点(n1)的DOM元素(el)赋值给新虚拟节点(n2),一边复用现有的DOM元素。
  2. **更新属性(props):**调用patchProps函数,对比旧属性(oldProps)和新属性(newProps),并应用属性的变化。
  3. **更新子节点:**调用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)。具体来说,执行以下操作:

  1. **清楚旧属性:**如果存在旧属性(oldProps),会遍历所有旧属性,并调用hostPatchProp函数将每个属性从DOM元素上移除。这是通过将新值设为null实现的。
  2. **设置新属性:**如果存在新属性(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函数赋值更新子元素,由于子元素的情况比较多,有一下情况:

  1. 新的子元素是文本
    • 老节点是数组,卸载老的children,将新的文本设置成children
    • 老的是文本,直接替换
    • 老的是null,不用关心老的,将新的设置成children
  2. 新的子元素是数组
    • 老的是数组,那就和新的做全量diff
    • 老的是文本,把老的清空,挂载新的children
    • 老的是null,不用关心老的,直接挂载新的children
  3. 新的子元素是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)
                }
            }
        }
    }
}

Released under the MIT License.