探索Vue渲染器的内部机制

110 阅读4分钟

前言

在这篇文章中梳理了关于虚拟dom的一些知识点深入剖析虚拟DOM原理,这篇文章主要理解一下vue中渲染器,,这也算是一个大的模块了,跟响应式系统属于一个级别。像挂载、更新、diff算法等这些操作都是在这个阶段进行的,下面将会对这些知识点一一进行展开。

渲染器的概念

渲染器可以用renderer来表示,但是要跟render进行区分,它们两个虽然都是函数但是都有各自的含义,前者用来表示渲染器,后者表示渲染,可以理解为一个名词一个是动词。渲染器包括render函数,是因为渲染器不仅能够把虚拟DOM渲染为真实DOM元素,而且还具有跨平台的能力,也就是说不止在浏览器中渲染。

function createRenderer(){
    function render(vnode,container){
    
    }
    // ...服务端渲染时
    function hydrate(vnode,container){
    
    }
    
    return {
        render,
        hydrate
    }
}

根据上面的代码可以很清晰的看到render函数只是其中的一部分,当需要使用render函数进行渲染时,只需要调用createRenderer函数得到一个render函数即可。

const renderer = createRenderer();
renderer.render(vnode,document.querySelector("#app"));

渲染器的应用

根据上述例子我们了解到通过使用createRenderer函数创建出一个渲染器,然后使用渲染器中的render函数进行渲染,当首次调用render函数时会执行挂载操作,也就是创建新的DOM元素,但是当第二次渲染时就不应该执行挂载操作了,而是应该执行更新操作。

const renderer = createRenderer();
// 第一次渲染
renderer.render(vnode,document.querySelector("#app"));

// 第二次渲染
renderer.render(nnode,document.querySelector("#app"));

在这种情况下,第二次渲染就应该执行更新操作,使用新的vnode和旧的vnode进行比较找出变更点变进行变更。这个过程就叫做“打补丁”,挂载本身其实也可以看做是一种打补丁,,它的特殊之处就在于旧的vnode是不存在的,所以根据这一逻辑就很容易写出下面代码:

function createRender(){
    function render(vnode,container){
        if(vnode){ // 挂载或更新阶段
            patch(container._vnode,vnode,container)
        }else{ 
            // 旧的vnode存在,新的vnode不存在 卸载阶段
            if(container._vnode){
                container.innerHTML = "";
            }
        }
        container._vnode = vnode;
    }
    return {
        render
    }
}

const renderer = createRenderer() 
// 首次渲染 挂载阶段 
renderer(vnode1,document.getElementById("#app")) 
// 第二次渲染 更新阶段 
renderer(vnode2,document.getElementById("#app")) 
// 卸载阶段
renderer(null,document.getElementById("#app"))

render函数中首先判断了是否有vnode来区分挂载和卸载操作,若没有vnode则是需要进行卸载,若有vnode则需要统一走patch方法并把新的vnode和旧的vnode传递过去进行挂载和更新阶段的区分(这里再次理解依稀挂载也是一种打补丁)。

function patch(oldVnode,newVnode){
    if(!oldVnode){
        moutElement(oldVnode,container);
    }else{
        // 打补丁...
    }
}

function moutElement(vnode,container){
    const el = document.createElement(vnode.type);
    if(typeof vnode.children=="string"){
        el.textContent = vnode.children;
    }else if(Array.isArray(vnode.children)){
        vnode.children.forEach(child=>{
            pathch(null,child,el)
        })
    }
    container.appendChild(el);
}

patch方法中通过判断是否有oldVnode来区分挂载和更新阶段,在首次渲染时因为没有oldVnode所以会直接走moutElement挂载方法,在挂载的方法中通过操作原生dom来进行页面的显示,如果子节点是一个数组则循环遍历并挂载数组中的虚拟节点,打补丁我们放到后面细聊。

Case

   const vnode = {
        type: 'p',
        props: {
            onClick: () => alert("You take me to hell !")
        },
        children: [
            {
                type: "p",
                props: {},
                children: "From heaven I fell"
            },
            {
                type: "div",
                props: {},
                children: "Hell"
            },

        ]
    }

    function createRenderer() {
        function render(vnode, container) {
            const el = document.createElement(vnode.type);
            if (vnode.props) {
                for (let key in vnode.props) {
                    if (/^on/.test(key)) {
                        el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key]) // onClick ==> click
                    }
                }
                if (typeof vnode.children === "string") {
                    el.appendChild(document.createTextNode(vnode.children))
                } else if (Array.isArray(vnode.children)) {
                    vnode.children.forEach(child => {
                        render(child, el);
                    })
                }
                container.appendChild(el);
            }
        }
        return {
            render,
        }
    }
    const renderer = createRenderer();
    renderer.render(vnode, document.getElementById("app"));

以上就是渲染器的实现思路了,是不是也没有想象中的那么难,Case中只考虑了挂载阶段的代码所以没有像上面那样写的很详细。

  • 首先根据虚拟节点的type创建容器元素;
  • 遍历虚拟节点的props匹配以on开头的属性,将其转换为原生事件即可;
  • 判断传递的虚拟节点的子节点的类型,如果是字符串则创建一个文本节点并添加到容器元素中,如果是数组则遍历进行递归,把当前的父元素传递到render函数中表示把创建的元素添加到当前的父元素容器中;

总结

这篇文章主要渲染器的相关概念,代码主要区分了一下挂载、更新和销毁阶段,现在做的还仅仅是挂载节点,归根结底都是用一些熟悉的DOM的API来完成渲染操作,渲染器的精髓在于更新节点的阶段,这个就留到后面再说了。