vue渲染器(一)

63 阅读9分钟

渲染器与响系统

在vue中,渲染器是整个框架最重要的功能之一,渲染器的好坏直接决定了vue的性能.下面是一个简单的渲染器结合之前完成的响应系统模拟代码

 function renderer(domString, container) {
                container.innerHTML = domString;
            }
            const count = ref(1);
            effect(() => {
                renderer(`<h1>${count.value}</h1>`, document.getElementById('app'));
            });
            const button = document.querySelector('button');
            button.addEventListener('click', () => {
                count.value++;
            });

image.png

image.png

封装一个renderer函数,将h1这段字符串渲染到页面上,结合响应系统,当count.value的值改变,重新触发renderer函数执行,重新渲染,这就是渲染器的基本概念. 接下来,封装一个创建渲染器的函数

            function createRenderer() {
                function render(domString, container) {
                    container.innerHTML = domString;
                }
                return {
                    render,
                };
            }

            const count = ref(1);
            effect(() => {
                const renderer = createRenderer();
                console.log(renderer);
                renderer.render(`<h1>${count.value}</h1>`, document.getElementById('app'));
            });
            const button = document.querySelector('button');
            button.addEventListener('click', () => {
                count.value++;
            });

为什么还有封装一个createRenderer函数来创建渲染器,我们传的第一个参数其实就是虚拟dom,因为之前把旧的虚拟dom渲染了,后面更新了dom,没有用把整个dom重新渲染,封装创建渲染器函数是为了后面写对比新旧dom的功能,然后进行局部渲染.

自定义渲染器

下面,我们实现一个"通用"的渲染器,将虚拟dom渲染到页面上. 下面是一个vnode(虚拟节点对象)

            const vonde = {
                type: 'h1',
                children: 'hello',
            };
            const renderer = createRenderer();
            renderer.render(vonde, document.querySelector('#app'));

下面是更改的createRenderer函数

            function createRenderer() {
                function patch(n1, n2, container) {
                    //如果n1不存在,以为挂载,则调用mountElement
                    if (!n1) {
                        mountElement(n2, container);
                    }else{
                        //n1不存在,则打补丁,新旧比较,局部替换
                    }
                }

                function render(vonde, container) {
                    if (vonde) {
                        patch(container._vonde, vonde, container);
                    } else {
                        if (container._vonde) {
                            container.innerHTML = '';
                        }
                    }
                    container._vnode = vonde;
                }
                return {
                    render,
                };
            }

上面代码意思是,当有vnode值,也就是有虚拟节点,则patch更新节点,如果没有,就是虚拟节点没有了,将容器设为空,在把当前节点保存到 container._vnode,即旧节点

patch函数是判断有没有旧节点,没有就是新节点,进行挂载,调用mountElement函数

            function mountElement(vonde, container) {
                //创建dom元素
                const el = document.createElement(vonde.type);
                //处理子节点,如果子节点是字符串,代表是文本节点
                if (typeof vonde.children === 'string') {
                    //设置元素的textContent
                    el.textContent = vonde.children;
                }
                //将元素添加到容器中
                container.appendChild(el);
            }

判断虚拟节点类型,创建节点,设置节点内容,在添加到容器中,渲染到页面. 虚拟dom有一个特别的能力,就是跨平台能力,我们上面使用的大多是浏览器的apii,所以,要将这些api抽离,设计通用的渲染器

            const renderer = createRenderer({
                createElement(tag) {
                    return document.createElement(tag);
                },
                //设置文本节点
                setElementText(el, text) {
                    el.textContent = text;
                },
                //用于给指定的parent添加指定元素
                insert(el, parent, anchor = null) {
                    parent.insertBefore(el, anchor);
                },
            });

我们将这些方法传入创建渲染器函数内,为什么要这么设计呢,因为我们现在是在浏览器,浏览器有这些api,如果换成小程序,uniapp,创建元素,设置元素内容,就不是这些api了

            function createRenderer(options) {
                //通过options得到操作Dom的api
                const { createElement, insert, setElementText } = options;

                //挂载元素
                function mountElement(vonde, container) {
                    //创建dom元素
                    //调用createElement函数创建元素
                    const el = createElement(vonde.type);
                    //处理子节点,如果子节点是字符串,代表是文本节点
                    if (typeof vonde.children === 'string') {
                        //设置元素的textContent
                        //调用setElementText设置元素文本节点
                        setElementText(el, vonde.children);
                    }
                    //将元素添加到容器中
                    //调用insert函数插入容器内
                    insert(el, container);
                }
                //打补丁
                function patch(n1, n2, container) {
                    //如果n1不存在,以为挂载,则调用mountElement
                    if (!n1) {
                        mountElement(n2, container);
                    } else {
                        //n1不存在,则打补丁,新旧比较,局部替换
                    }
                }
                //渲染函数
                function render(vonde, container) {
                    if (vonde) {
                        patch(container._vonde, vonde, container);
                    } else {
                        if (container._vonde) {
                            container.innerHTML = '';
                        }
                    }
                    container._vnode = vonde;
                }
                return {
                    render,
                };
            }

功能上没什么区别,就是将可以将一些操作抽离用我们传入的函数操作

image.png

这样我们的元素就成功挂载到页面上了

挂载与更新

上面我们把children值为字符串的元素渲染到页面上,但是一个节点可能具有文本节点,也可能会有其他元素节点,为了描述这些子节点,需要使用数组来描述

 const vonde = {
                type: 'h1',
                children: [
                    {
                        type: 'p',
                        children: 'hello',
                    },
                ],
            };

我们需要修改一下mountElement函数

                //挂载元素
                function mountElement(vonde, container) {
                    //创建dom元素
                    //调用createElement函数创建元素
                    const el = createElement(vonde.type);
                    //处理子节点,如果子节点是字符串,代表是文本节点
                    if (typeof vonde.children === 'string') {
                        //设置元素的textContent
                        //调用setElementText设置元素文本节点
                        setElementText(el, vonde.children);
                    } else if (Array.isArray(vonde.children)) {
                        //如果是数组,则遍历每一个子节点,调用patch函数挂载
                        vonde.children.forEach((child) => {
                            patch(null, child, el);
                        });
                    }
                    //将元素添加到容器中
                    //调用insert函数插入容器内
                    insert(el, container);
                }

这些代码意思是,当我们子节点是一个数组时,循环遍历每一个节点,调用patch函数,patch函数会继续新旧dom对比,我们这里没有新旧dom对比,就是直接挂载,patch会重新调用mountElement函数,这样就会继续递归遍历,直到遍历到children不是数组为止

正确设置元素属性

正常的页面元素都是有属性的,以button为例

        <button disabled></button>

什么这段属性,vue会解析成以下虚拟节点

const button = {
    type='button',
    props:{
        disabled:''
    }
}

这个虚拟节点,vue会这样把属性设置到节点上

    el.setAttribute('disabled','')

这样做逻辑没问题,浏览器会把按钮禁用,但是虚拟节点是下面这样的

const button = {
    type='button',
    props:{
        disabled:false
    }
}

由于setAttribute设置的值会被字符串化,所以相当于执行这段代码

    el.setAttribute('disabled','false')

所以要使用

    el.disabled=false

但是,虚拟节点是这样

const button = {
    type='button',
    props:{
        disabled:''
    }
}

js会把空字符串转化为布尔值

    el.disabled=false

这里本意是禁用按钮,但是执行的是不禁用的意思,所以这需要特殊处理

                //挂载元素
                function mountElement(vonde, container) {
                    //创建dom元素
                    //调用createElement函数创建元素
                    const el = createElement(vonde.type);
                    //处理子节点,如果子节点是字符串,代表是文本节点
                    if (typeof vonde.children === 'string') {
                        //设置元素的textContent
                        //调用setElementText设置元素文本节点
                        setElementText(el, vonde.children);
                    } else if (Array.isArray(vonde.children)) {
                        //如果是数组,则遍历每一个子节点,调用patch函数挂载
                        vonde.children.forEach((child) => {
                            patch(null, child, el);
                        });
                    }

                    if (vonde.props) {
                        for (const key in vonde.props) {
                            //用in操作符判断key是否存在对应的DOM Properties
                            if (key in el) {
                                //获取该 DOM Properties类型
                                const type = typeof el[key];
                                const value = vonde.props[key];
                                //如果是布尔值,且value为空字符串,则矫正为true
                                if (type === 'boolean' && value === '') el[key] = true;
                                else el[key] = value;
                            } else {
                                //如果没有对应DOM Properties 则使用setAttribute
                                el.setAttribute(key, vonde.props[key]);
                            }
                        }
                    }

                    //将元素添加到容器中
                    //调用insert函数插入容器内
                    insert(el, container);
                }

使用in判断 判断key是否在该元素js对象上,如果有则以为有对应属性直接修改,没有直接调用setAttribute设置属性

            const vonde = {
                type: 'h1',
                children: [
                    {
                        type: 'p',
                        children: 'hello',
                    },
                    {
                        type: 'button',
                        props: {
                            disabled: '',
                        },
                        children: 'Yes',
                    },
                ],
            };

image.png 当元素属性为只读属性,如input中的from时,无法通过el.form进行设置,所以这一种情况需要特殊处理

                //挂载元素
                function mountElement(vonde, container) {
                    //创建dom元素
                    //调用createElement函数创建元素
                    const el = createElement(vonde.type);
                    //处理子节点,如果子节点是字符串,代表是文本节点
                    if (typeof vonde.children === 'string') {
                        //设置元素的textContent
                        //调用setElementText设置元素文本节点
                        setElementText(el, vonde.children);
                    } else if (Array.isArray(vonde.children)) {
                        //如果是数组,则遍历每一个子节点,调用patch函数挂载
                        vonde.children.forEach((child) => {
                            patch(null, child, el);
                        });
                    }

                    if (vonde.props) {
                        for (const key in vonde.props) {
                            //使用shouldSetAsProps判断是否用setAttribute设置属性
                            if (shouldSetAsProps(el, key, value)) {
                                //获取该 DOM Properties类型
                                const type = typeof el[key];
                                const value = vonde.props[key];
                                //如果是布尔值,且value为空字符串,则矫正为true
                                if (type === 'boolean' && value === '') el[key] = true;
                                else el[key] = value;
                            } else {
                                //如果没有对应DOM Properties 则使用setAttribute
                                el.setAttribute(key, vonde.props[key]);
                            }
                        }
                    }

                    //将元素添加到容器中
                    //调用insert函数插入容器内
                    insert(el, container);
                }

为了代码可读性,将设置元素属性作为选项传递

 const renderer = createRenderer({
                createElement(tag) {
                    return document.createElement(tag);
                },
                //设置文本节点
                setElementText(el, text) {
                    el.textContent = text;
                },
                //用于给指定的parent添加指定元素
                insert(el, parent, anchor = null) {
                    parent.insertBefore(el, anchor);
                },

                //将属性设置操作封装到patchProps函数中,作为渲染器选项传递
                patchProps(el, key, prevValue, nextValue) {
                    //使用shouldSetAsProps判断是否用setAttribute设置属性
                    if (shouldSetAsProps(el, key, nextValue)) {
                        //获取该 DOM Properties类型
                        const type = typeof el[key];
                        //如果是布尔值,且value为空字符串,则矫正为true
                        if (type === 'boolean' && nextValue === '') el[key] = true;
                        else el[key] = nextValue;
                    } else {
                        //如果没有对应DOM Properties 则使用setAttribute
                        el.setAttribute(key, nextValue);
                    }
                },
            });

挂载元素的函数

                //挂载元素
                function mountElement(vonde, container) {
                    //创建dom元素
                    //调用createElement函数创建元素
                    const el = createElement(vonde.type);
                    //处理子节点,如果子节点是字符串,代表是文本节点
                    if (typeof vonde.children === 'string') {
                        //设置元素的textContent
                        //调用setElementText设置元素文本节点
                        setElementText(el, vonde.children);
                    } else if (Array.isArray(vonde.children)) {
                        //如果是数组,则遍历每一个子节点,调用patch函数挂载
                        vonde.children.forEach((child) => {
                            patch(null, child, el);
                        });
                    }

                    if (vonde.props) {
                        for (const key in vonde.props) {
                            //调用patchProps函数
                            patchProps(el, key, null, vonde.props[key]);
                        }
                    }

                    //将元素添加到容器中
                    //调用insert函数插入容器内
                    insert(el, container);
                }

class处理

class情况特殊,可以传字符串,对象,还有可能是数组,解析成虚拟节点可能为以下几种情况

    //一
    const vnode={
        type:'p',
        props:{
            class:'foo bar'
        }  
    }
     //二
    const vnode={
        type:'p',
        props:{
            class:{foo:true,bar:false}
        }  
    }
     //三
    const vnode={
        type:'p',
        props:['foo bar',{baz:true}]  
    }

因为class有很多种情况,所以要将class转换为字符串,这里封装一个normalizeClass函数

            //不同数据类型转换字符串算法,将不同形式类型转换字符串
            function normalizeClass(classData) {
                const arr = [];
                if (typeof classData === 'string') {
                    classData.split(' ').forEach((item) => arr.push(item));
                } else if (Array.isArray(classData)) {
                    classData.forEach((item) => {
                        if (typeof item === 'string') arr.push(item);
                        else if (typeof item === 'object') {
                            for (const key in item) {
                                if (item[key]) arr.push(key);
                            }
                        }
                    });
                } else if (typeof classData === 'object') {
                    for (const key in classData) {
                        if (classData[key]) arr.push(key);
                    }
                }
                return arr
            }

应该有更好的写法,这里先这样等以后优化

            const vonde = {
                type: 'h1',
                children: [
                    {
                        type: 'p',
                        children: 'hello',
                    },
                    {
                        type: 'button',
                        props: {
                            disabled: '',
                            class: normalizeClass(['foo bar', { baz: true }]),
                        },
                        children: 'Yes',
                    },
                ],
            };

这些class就可以转换为字符串形式,vue发现,当使用el.className时性能是最好的,所以,需要优化以下设置属性的函数

                //将属性设置操作封装到patchProps函数中,作为渲染器选项传递
                patchProps(el, key, prevValue, nextValue) {
                    //对class进行特殊处理
                    if (key === 'class') {
                        el.className = nextValue || '';
                    }
                    //使用shouldSetAsProps判断是否用setAttribute设置属性
                    else if (shouldSetAsProps(el, key, nextValue)) {
                        //获取该 DOM Properties类型
                        const type = typeof el[key];
                        //如果是布尔值,且value为空字符串,则矫正为true
                        if (type === 'boolean' && nextValue === '') el[key] = true;
                        else el[key] = nextValue;
                    } else {
                        //如果没有对应DOM Properties 则使用setAttribute
                        el.setAttribute(key, nextValue);
                    }
                },

这样,不管什么形式的class都可以挂载到节点上了

image.png

卸载操作

    //第一次挂载
    renderer.render(vonde, document.querySelector('#app'));
    //第二次挂载
    renderer.render(newVonde, document.querySelector('#app'));

看一下上面的代码,这是两次挂载操作,也就是更新dom,如果新的节点设为null,也就意味着什么都不渲染,也就是卸载之前渲染的内容.所以,要进行更新我们挂载节点的函数

               function mountElement(vonde, container) {
                    //创建dom元素
                    //调用createElement函数创建元素
                    //让vnode.el引用真实dom
                    const el = (vonde.el = createElement(vonde.type));
                    //处理子节点,如果子节点是字符串,代表是文本节点
                    if (typeof vonde.children === 'string') {
                        //设置元素的textContent
                        //调用setElementText设置元素文本节点
                        setElementText(el, vonde.children);
                    } else if (Array.isArray(vonde.children)) {
                        //如果是数组,则遍历每一个子节点,调用patch函数挂载
                        vonde.children.forEach((child) => {
                            patch(null, child, el);
                        });
                    }

                    if (vonde.props) {
                        for (const key in vonde.props) {
                            //调用patchProps函数
                            patchProps(el, key, null, vonde.props[key]);
                        }
                    }

                    //将元素添加到容器中
                    //调用insert函数插入容器内
                    insert(el, container);
                }

封装卸载函数

                //卸载操作
                function unmount(vonde) {
                    //根据vnode获取要卸载的真实dom元素
                    //获取el的父元素
                    const parent = vonde.el.parentNode;
                    //调用removeChild移除元素
                    if (parent) parent.removeChild(vonde.el);
                }

优化渲染函数

//渲染函数
                function render(vonde, container) {
                    if (vonde) {
                        patch(container._vonde, vonde, container);
                    } else {
                        if (container._vonde) {
                            //调用unmount函数卸载vnode
                            unmount(container._vnode);
                        }
                    }
                    container._vnode = vonde;
                }