阅读 162

小白也能看懂的vue3源码之渲染系统实现

前言

今天来模拟一下vue3的渲染系统的实现,不太了解这方面的可以认真看看,希望能对大家有帮助

渲染系统

众所周知vue框架是通过render函数将vue的template模板通过h函数解析成vnode(虚拟节点)慢慢变成了vdom(虚拟dom) 最后转换成真实元素,我们才能真正的在浏览器上看到。今天我们就来手动实现一个简单的渲染器,来探究探究其真正的原理。

template

渲染函数render-function

虚拟节点vnode

真实元素

浏览器显示

手写一个虚拟节点 来帮助我们实验

//1.通过h函数创建vnode形成vdom
        const vnode = h('div', {
            class: 'home',
            id: 'one'
        }, [
            h('h2', null, '哈哈,我是渲染器渲染出来的元素'),
            h('button',{onClick(){}},'按钮点击')
        ])复制代码

我们现在要做的就是将虚拟节点中的内容转化成真正的元素

h函数 转化成vnode对象

传递参数

  • tag 标签名 如div span

  • props 属性名如class id

  • children 字符串为直接填入内容 数组可嵌套多个子节点

//将vnode转换成 avascript对象 -> {}
const h = (tag, props, children) => {
    return {
        tag,
        props,
        children
    }
}复制代码

它干的活很简单,直接转化成对象就行了 (实际上vue内部源码还增加了其它属性,来应对其它的情况)
我们来看看转换后的vnode 实际上现在是一个小的vdom了

image.png

实质上就对象里面套对象的数据结构,现在我们就是考虑将其转换为真实dom再进行挂载
接下来我们将其生成的vdom挂载到特定的容器上

// 2.通过mount函数, 将vnode挂载到div#app上
        mount(vnode,document.querySelector('#app'))复制代码

实现mount挂载函数 将VNode挂载到DOM上

传递参数 vnode 虚拟节点 container 挂载到的容器上

//解析vnode节点形成真正的元素节点   vnode -> element

const mount = (vnode, container) => {
    //1.创建元素节点
    const el = vnode.el = document.createElement(vnode.tag)

    //2.判断属性是否有值 有则遍历 再判断属性是否为函数 分别处理
    if (vnode.props) {
        for (key in vnode.props) {
            const value = vnode.props[key] //取值
            if (key.startsWith('on')) { //匹配是否以on开头的属性
                //添加监听 删除'on'再小写 eg:onClick->click
                el.addEventListener(key.slice(2).toLowerCase(), value)
            } else {
                el.setAttribute(key, value) //添加属性
            }
        }
    }

    //3.处理children
    if (vnode.children) {
        if (typeof vnode.children === 'string') { //字符串直接填入
            el.textContent = vnode.children
        } else {
            vnode.children.forEach(item => { //遍历直接执行递归
                mount(item, el)
            });
        }
    }
    container.appendChild(el)
}复制代码

这个实现起来稍微复杂了点,主要是需要考虑的情况比较多,这里也没有写完整,主要步骤实现就是这样来做的。

这样执行mount函数后页面上就能真正的显示我们的dom元素了 如下:

image.png

但是当我们更新了dom时它又是怎么进行及时更新呢?

patch函数,用于对两个VNode进行对比,决定如何处理新的VNode

  • 情况一tag标签不同

 //通过h函数创建vnode形成vdom
        const vnode = h('div', {
            class: 'home',
            id: 'one'
        }, [
            h('h2', null, '哈哈,我是渲染器渲染出来的元素'),
            h('button', {
                onClick() {}
            }, '按钮点击')
        ])
        console.log(vnode);

        // 2.通过mount函数, 将vnode挂载到div#app上
        mount(vnode, document.querySelector('#app'))

        //3.通过patch函数进行diff算法更新节点
        setTimeout(() => {
            const newVnode = h('p', {
                class: 'home-new',
                id: 'one-new'
            }, [
                h('h3', null, '哈哈,我是渲染器渲染出来的元素'),
            ])
            patch(vnode,newVnode)
        }, 2000)复制代码

patch 处理

//主要思路是比较出不同点,更新dom
const patch = (n1, n2) => {
    //1.节点标签不一致 简单粗暴 将旧节点移除新节点挂载上去 做一个替换效果
    if (n1.tag !== n2.tag) {
        const n1Parent = n1.el.parentElement //获取父节点
        n1Parent.removeChild(n1.el) //移除n1子节点
        mount(n2, n1Parent)//将n2挂载到父节点
    }else{

        
    }
}复制代码

效果展示 成功替换

image.png

  • 情况二 props中的属性发生了变化 增删改

     //通过h函数创建vnode形成vdom
        const vnode = h('div', {
            class: 'home',
            id: 'one'
        }, [
            h('h2', null, '哈哈,我是渲染器渲染出来的元素'),
            h('button', {
                onClick() {}
            }, '按钮点击')
        ])
        console.log(vnode);

        // 2.通过mount函数, 将vnode挂载到div#app上
        mount(vnode, document.querySelector('#app'))
        //3.通过patch函数进行diff算法更新节点
        setTimeout(() => {
            //情况二:props中的属性发生了变化 增删改
           const newVnode = h('div', {
                class: 'home-new',
                name: 'kzj',
                onClick() {
                    console.log('hahha');
                }
            }, [
                h('h3', null, '呵呵,我是渲染器渲染出来的元素'),
            ])
            patch(vnode,newVnode)
        }, 2000)复制代码

我们这里改变了class类名 增加了name属性 删除了id属性
patch 处理

//主要思路是比较出不同点,更新dom
const patch = (n1, n2) => {
    //1.节点标签不一致 简单粗暴 将旧节点移除新节点挂载上去 做一个替换效果
    if (n1.tag !== n2.tag) {
        const n1Parent = n1.el.parentElement //获取父节点
        n1Parent.removeChild(n1.el) //移除n1子节点
        mount(n2, n1Parent)//将n2挂载到父节点
    }else{
        //取出element对象 并在n2中保存
        const el = n2.el = n1.el

        //2.属性新增 修改 删除
        const oldProps = n1.props || {}
        const newProps = n2.props || {}
        // 修改和新增处理
        for(key in newProps){
            const oldValue = oldProps[key]
            const newValue = newProps[key]
            if(oldValue !== newValue){
                if(key.startsWith('on')){//更新函数
                    el.addEventListener(key.slice(2).toLowerCase(),newValue)
                }else{
                    el.setAttribute(key,newValue)//更新属性
                }
            }
        }
        //删除处理
        for(key in oldProps){
            if(!(key in newProps)){//如果该属性不在新对象中
                if(key.startsWith('on')){//移除方法
                    const value = oldProps[key]
                    el.removeEventListener(key.slice(2).toLowerCase(),value)
                }else{
                    el.removeAttribute(key)//移除该属性 
                }
            }
        }

    }
}复制代码

效果展示 成功实现 增删改都能实现 点击也能触发函数

image.png

image.png

这里可能有细心的同学对下面子元素怎么没改变有点疑问了,那是因为我们现在还没有对children处理呢,所以上面肯定是不会改变的。那为啥第一种情况标签不同就能改变呢,这个不用多说了吧,虽然它没有去做props children处理,但是它简单粗暴呀,把根节点直接给替换了,你说能不改变吗?
下面开始对children开始处理

  • 情况三 children 发生改变

 //通过h函数创建vnode形成vdom
        const vnode = h('div', {
            class: 'home',
            id: 'one'
        }, [
            h('h2', null, '哈哈,我是渲染器渲染出来的元素'),
            h('button', {
                onClick() {}
            }, '按钮点击')
        ])
        console.log(vnode);

        // 2.通过mount函数, 将vnode挂载到div#app上
        mount(vnode, document.querySelector('#app'))

        //3.通过patch函数进行diff算法更新节点
        setTimeout(() => {
            //情况三:children 发生改变
            const newVnode = h('div', {
                class: 'home-new',
                name: 'kzj',
                onClick() {
                    console.log('hahha');
                }
            }, [
                h('h3', null, '呵呵,我是渲染器渲染出来的元素'),
                h('h4', null, '嘻嘻'),
                h('h5', null, '嘿嘿'),
            ])
            patch(vnode, newVnode)
        }, 2000)复制代码

我们这里新增了两个h标签
patch处理 这里细分几种情况处理

  1. 判断是不是字符串 处理

  2. 数组处理 2.1旧节点大于新节点--移除多余旧节点 2.2旧节点小于新节点--增加多余新节点

//主要思路是比较出不同点,更新dom
const patch = (n1, n2) => {
    //1.节点标签不一致 简单粗暴 将旧节点移除新节点挂载上去 做一个替换效果
    if (n1.tag !== n2.tag) {
        const n1Parent = n1.el.parentElement //获取父节点
        n1Parent.removeChild(n1.el) //移除n1子节点
        mount(n2, n1Parent)//将n2挂载到父节点
    } else {
        //取出element对象 并在n2中保存
        const el = n2.el = n1.el

        //2.属性新增 修改 删除
        const oldProps = n1.props || {}
        const newProps = n2.props || {}
        // 修改和新增处理
        for (key in newProps) {
            const oldValue = oldProps[key]
            const newValue = newProps[key]
            if (oldValue !== newValue) {
                if (key.startsWith('on')) {
                    el.addEventListener(key.slice(2).toLowerCase(), newValue)
                } else {
                    el.setAttribute(key, newValue)
                }
            }
        }
        //删除处理
        for (key in oldProps) {
            if (!(key in newProps)) {//如果该属性不在新对象中
                if (key.startsWith('on')) {//移除方法
                    const value = oldProps[key]
                    el.removeEventListener(key.slice(2).toLowerCase(), value)
                } else {
                    el.removeAttribute(key)//移除该属性 
                }
            }
        }

        //3.children处理
        const oldChildren = n1.children || [];
        const newChildren = n2.children || [];
        //3.1判断是不是字符串 处理
        if (typeof newChildren === 'string') {
            if (typeof oldChildren === 'string') {
                if (oldChildren !== newChildren) { //都是且值不同
                    el.textContent = newChildren
                }
            } else {
                el.innerHtml = newChildren //旧节点是数组或其它类型
            }
        } else {
            //数组处理
            //n1 [v1,v2,v3]
            //n2 [v1,v5,v6]
            //简单diff算法实现
            const commonLength = Math.min(oldChildren.length, newChildren.length)//最小长度
            for (let i = 0; i < commonLength; i++) {
                patch(oldChildren[i],newChildren[i])//递归更新节点
            }
            
            //n1 [v1,v2,v3,v7,v8]
            //n2 [v1,v5,v6]
            //多余节点处理 旧节点多--移除节点 v7 v8
            if(oldChildren.length > commonLength){
                oldChildren.slice(commonLength).forEach(item =>{
                    el.removeChild(item.el)//移除
                })
            }
            
            //n1 [v1,v2,v3]
            //n2 [v1,v5,v6,v7,v8]
            //新节点多--增加节点 v7 v8
            if(newChildren.length > commonLength){
                newChildren.slice(commonLength).forEach(item =>{
                    mount(item,el)//挂载
                })
            }
        }

    }
}复制代码

效果展示 成功实现

image.png

这样我们就封装一个完整的渲染系统了,理解了实现过程也能更好的帮助我们了解vue内部是如何来实现渲染系统的。


作者:vs心动
链接:https://juejin.cn/post/7016994565269930020


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐