Vue 渲染器实现原理

Vue 组件渲染管线

Vue 组件渲染分为以下几个主要过程:

  • 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。

  • 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。

  • 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

最简模型

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue Renderer</title>
    <style>
        .button {
            display: block;
            width: 90%;
            height: 30px;
            margin: 0 auto;
            background-color: red;
            color: #fff;
            border: none;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script src="https://unpkg.com/@vue/reactivity@3.3.4/dist/reactivity.global.js"></script>
    <script>
        const Text = Symbol.for('Text');
        const Comment = Symbol.for('Comment');

        /**
         * 渲染器实现
         */
        function createRenderer({ createElement, setElementText, insert, createText, setText, patchProp }) {
            // 挂载元素
            function mountElement(vnode, container) {
                // 创建真实 DOM 元素
                const el = createElement(vnode.type);
                // 保存真实 DOM 元素引用
                vnode.el = el;
                // 处理子节点
                if (typeof vnode.children === 'string') {
                    setElementText(el, vnode.children);
                } else if (Array.isArray(vnode.children)) {
                    vnode.children.forEach(child => {
                        mountElement(child, el);
                    });
                }

                // 处理属性
                Object.keys(vnode.props || {}).forEach(key => {
                    patchProp(el, key, null, vnode.props[key]);
                });

                // 挂载
                insert(el, container);
            }

            // 更新元素
            function patchElement(n1, n2) {
                console.log(n1, n2);
                const el = n2.el = n1.el;
                // 更新属性
                const oldProps = n1.props;
                const newProps = n2.props;
                Object.keys(newProps).forEach(key => {
                    if (newProps[key] !== oldProps[key]) {
                        patchProp(el, key, oldProps[key], newProps[key]);
                    }
                });
                Object.keys(oldProps).forEach(key => {
                    if (!(key in newProps)) {
                        patchProp(el, key, oldProps[key], null);
                    }
                })
                // 更新子节点
                patchChildren(n1, n2, el);
            }

            // 更新子节点(没有子节点、文本子节点、一组子节点)
            function patchChildren(n1, n2, el) {
                // 判断新子节点是否是文本节点
                if (typeof n2.children === 'string') {
                    if (Array.isArray(n1.children)) {
                        n1.children.forEach((child) => {
                            unmount(child);
                        });
                    }

                    setElementText(el, n2.children);
                } else if (Array.isArray(n2.children)) {
                    // 新子节点是一组子节点,旧子节点也是一组子节点,则需要 Diff 新旧子节点
                    if (Array.isArray(n1.children)) {
                        // @TODO vnode diff
                    } else {
                        // 旧子节点是文本节点或者不存在,则清空容器,直接挂载新节点
                        setElementText(container, '');
                        n2.children.forEach((child) => {
                            mountElement(child, container);
                        });
                    }
                } else {
                    if (Array.isArray(n1.children)) {
                        n1.children.forEach((child) => {
                            unmount(child);
                        });
                    } else {
                        setElementText(container, '');
                    }
                }
            }

            // 挂载或更新
            function patch(n1, n2, container) {
                // 如果 n1 存在,则对比 n1 和 n2 的类型;如果类型不一致,则卸载 n1
                if (n1 && n1.type !== n2.type) {
                    unmount(n1);
                    n1 = null;
                }

                const { type } = n2;
                if (typeof type === 'string') {
                    // 标签元素节点
                    if (!n1) {
                        mountElement(n2, container);
                    } else {
                        patchElement(n1, n2);
                    }
                } else if (type === Text) {
                    // 文本节点
                    if (!n1) {
                        const el = n2.el = createText(n2.children);
                        insert(el, container);
                    } else {
                        const el = n2.el = n1.el;
                        if (n1.children !== n2.children) {
                            setText(n2.children);
                        }
                    }
                } else if (type === Comment) {
                    // 注释节点
                    if (!n1) {
                        const el = n2.el = createComment(n2.children);
                        insert(el, container);
                    } else {
                        if (n1.children !== n2.children) {
                            setText(n2.children);
                        }
                    }
                }
            }

            // 卸载
            function unmount(vnode) {
                const parent = vnode.el.parentNode;
                if (parent) {
                    parent.removeChild(vnode.el);
                }
            }

            // 渲染
            function render(vnode, container) {
                if (vnode) {
                    // vnode 挂载或打补丁
                    patch(container._vnode, vnode, container)
                } else {
                    // 卸载 vnode
                    if (container._vnode) {
                        unmount(container._vnode)
                    }
                }

                // 保存本次渲染的 vnode
                container._vnode = vnode;
            }

            return {
                render
            }
        }

        /**
         * 测试代码
         */
        const { render } = createRenderer({
            createElement(tag) {
                console.log('createElement: ', tag);
                return document.createElement(tag);
            },
            setElementText(el, text) {
                console.log('setElementText: ', el, text);
                el.textContent = text;
            },
            insert(el, parent) {
                console.log('insert: ', el, parent);
                parent.appendChild(el);
            },
            createText(text) {
                console.log('createText: ', text);
                return document.createTextNode(text);
            },
            setText(el, text) {
                console.log('setText: ', el, text);
                el.nodeValue = text;
            },
            createComment(text) {
                console.log('createComment: ', text);
                return document.createComment(text);
            },
            patchProp(el, key, prevValue, nextValue) {
                console.log('patchProp: ', el, key, prevValue, nextValue);
                if (/^on/.test(key)) {
                    const invokers = el._invokers || (el._invokers = {});
                    let invoker = invokers[key];
                    const name = key.slice(2).toLowerCase();
                    if (nextValue) {
                        if (!invoker) {
                            invoker = (e) => {
                                if (e.timeStamp < invoker.attached) {
                                    return;
                                }

                                if (Array.isArray(invoker.value)) {
                                    invoker.value.forEach(fn => fn(e));
                                } else {
                                    invoker.value(e);
                                }
                            };
                            invoker.value = nextValue;
                            invoker.attached = performance.now();
                            el.addEventListener(name, invoker);
                            el._invokers[key] = invoker;
                        } else {
                            invoker.value = nextValue;
                        }
                    } else if (invoker) {
                        el.removeEventListener(name, invoker);
                    }
                } else if (key === 'class') {
                    el.className = nextValue || '';
                } else if (key in el) {
                    // disabled、form 等属性值需要特殊处理
                    el[key] = nextValue;
                } else {
                    el.setAttribute(key, nextValue);
                }
            }
        });

        const { effect, ref } = VueReactivity;
        const enable = ref(false);
        effect(() => {
            const vnode = {
                type: 'div',
                props: enable.value ? {
                    onClick: () => {
                        console.log('div click event');
                    }
                } : {},
                children: [
                    {
                        type: 'button',
                        props: {
                            type: 'button',
                            class: 'button',
                            onClick: () => {
                                enable.value = true;
                                console.log('button click event');
                            },
                            onMouseover: () => {
                                console.log('button hover event');
                            }
                        },
                        children: '点击一下'
                    }
                ]
            };
            const container = document.getElementById('app');
            render(vnode, container);
        });
    </script>
</body>

</html>

参考