我学会了,实现一个vdom

简介: 在vue和react中都有用到使用vdom来进行渲染界面,在页面刚创建的时候使用vdom来渲染界面性能并没有直接使用dom操作来渲染页面好,但是在更新界面的时候使用虚拟dom比直接使用dom操作要性能好,当然你如果原生dom操作非常熟练、经验也非常丰富,可能也有例外,不过那么多原生dom操作也会有比较大的心智压

前言

在vue和react中都有用到使用vdom来进行渲染界面,在页面刚创建的时候使用vdom来渲染界面性能并没有直接使用dom操作来渲染页面好,但是在更新界面的时候使用虚拟dom比直接使用dom操作要性能好,当然你如果原生dom操作非常熟练、经验也非常丰富,可能也有例外,不过那么多原生dom操作也会有比较大的心智压力。

准备工作

vite 官网:https://cn.vitejs.dev/guide/

使用vite是因为它比较快,也很成熟,支持多种模板,实现虚拟dom的话只需要空的模板即可,然后自己加个jest,然后再加个esm 转 cjs的babel插件即可,因为jset默认不支持import这样的语法。

jset 官网:https://jestjs.io/zh-Hans/docs/getting-started

babel插件:@babel/plugin-transform-modules-commonjs

yarn create vite vdom
cd vdom
yarn install
yarn add jset
yarn add @babel/plugin-transform-modules-commonjs

其它的看仓库代码,能正常运行,也都有注释,代码是基于下面的设计思路来实现的,没那么复杂。在浏览器直接看代码,点击 github1s

设计思路

四个操作:正常的虚拟dom操作,diff算法,h函数返回虚拟dom,还有renderer渲染器将dom插入进视图。

dom操作

用过jquery就知道,dom操作也无非就是增删改查。

// 新增
createElement(typeName: string): HTMLDomElement;
insert(el:HTMLDomElement, parent:HTMLDomElement): void;
createText(text: string): Text;

// 删除
remove(el:HTMLDomElement, parent:HTMLDomElement): void;

// 修改
replaceElement(oldEle:HTMLDomElement, newEle:HTMLDomElement): void;
setText(el:HTMLDomElement, text: string): void;
setInnerHTML(el:HTMLDomElement, text: string): void;

// 事件的名称纠正和绑定,然后属性的替换、设置和移除
patchProp(el: HTMLDomElement, key: string, prevVal: any, nextVal: any): void;

diff 算法

这里只做大概的diff设计,简单理解的diff,diff开发中比较常见的,比如git diff、code diff。

先对比tag,不一样就直接替换。

tag一样就检测props。props存在的话,props中新节点不是老节点,直接替换。props中老节点有但是新节点没有,那么全删掉。
然后检测children,children有两种,一种是字符串,一种是数组。
新的children是字符串,而之前的children也是字符串,那就对比字符串,看看是否要替换。
新的children是字符串,之前的children是数组,那就整个替换。
新的children是数组,而之前的children是字符串,那就把之前的节点清空,再遍历挂载新children中的的元素。
新的children是数组,而之前的children也是数组,这里就复杂一点了,需要递归。简单的话就是按照新旧节点中最少的节点数来进行循环递归,然后看看有没有必要移除掉旧节点中多余的节点,比如旧节点比新节点多。最后看看有没有必要添加新的节点,比如新的节点比旧节点多。

递归是一个深度优先遍历的过程,并不复杂,我有一篇文章有写通过链表来思考递归,递归和回溯的设计是接触算法比较友好的思想的噢,不会很难。

h函数

h用于返回一个虚拟dom,类似一个集装箱吧,把虚拟dom组装好,返回给你。这里只做简单的实现,理解思想就行。

// 这里偷了点懒,不过理解它的入参类型即可,不用纠结具体的参数中每一个类型具体的细节
h(tag: string, props: object, children: any[]): object;

renderer 渲染器

这里实现一个mountElement就行了,将vnode渲染成真实的dom,处理props,处理children,插入到视图中,返回传入的vonde。

mountElement(vnode: object, container: string|HTMLDomElement): object;

代码实现

dom.js


import { log } from './_log.js'

/* 新增相关 */

export function createElement(typeName) {
    log('创建元素',['createElement'])
    return document.createElement(typeName)
}

export function insert (el, parent) {
    log('插入元素',['insert'])
    parent.append(el)
}

export function createText(text) {
    log('创建文本',['createText'])
    return new Text(text)
}

/* 删除相关 */

export function remove (el, parent) {
    log('移除元素',['insert'])
    parent.remove(el)
}

/* 修改相关 */

export function replaceElement (oldEle, newEle) {
    log('替换元素',['repalceElement'])
    oldEle.repaceWith(newEle)
}

export function setText (el, text) {
    log('设置文本', ['setText'])
    el.textContent = text // 支持空格换行,innerText会把空白符都清除
    // el.innerText = text
}

export function setInnerHTML (el, text) {
    log('设置InnerHTML', ['setInnerHTML'])
    el.innerHTML = text
}

/* 事件的名称纠正和绑定,然后属性的替换、设置和移除 相关*/
export function patchProp (el, key, prevVal, nextVal) {
    log('patchProp')

    if (key.starsWith('on')) {
        // el.addEventListener(key.slice(2).toLowerCase(), nextVal)
        el.addEventListener(key.slice(2).toLocaleLowerCase(), nextVal) // 支持不同的语言环境时,采用本地地区的转换方法
        log('事件名称纠正和绑定')
        return
    }

    log('属性替换、设置或者移除')
    nextVal === null ? el.removeAttribute(key) : el.setAttribute(key, nextVal)
}

h.js

import { log } from './_log'

export function h(tag, props, children = []){
    log('返回vdom',['h'])
    return {
        tag, props, children
    }
}

renderer.js

import {
    createElement,
    patchProp,
    insert,
    createText
} from './dom'

export function createMount () {

    return mountElement
    function mountElement(vnode, container) {
        vnode.el = createElement(vnode.tag)
        const el = vnode.el
        
        // 处理props
        vnode.props && Object.keys(vnode.props).forEach(keyName => {
            const val = vnode.props[keyName]
            patchProp(vnode.el, keyName, null, val)
        })

        // 处理children
        Array.isArray(vnode.children) ? vnode.children.forEach(v => mountElement(v, el)) : insert(createText(vnode.children), el)

        // 插入到视图中
        insert(el, container)

        // 返回vnode
        return vnode
    }

}

diff.js

import {
    patchProp,
    setText,
    createElement,
    replaceElement,
    remove
} from './dom'

export function createDiff(mountElement) {
    
    return diff
    function diff (oldNode, newNode) {
        const { props: oldProps, el: oldEl, tag: oldTag, children: oldChildren = [] } = oldNode
        const { props: newProps, tag: newTag, children: newChildren = [] } = newNode

        // 标签不同 便可替换
        if (oldTag !== newTag) {
            replaceElement(oldEl, createElement(newTag))
            return
        }

        const el = newNode.el = oldEl

        if (newProps) {
            // 新旧不同 便可替换
            Object.keys(newProps).forEach(keyName => {
                newProps[keyName] !== oldProps[keyName] && patchProp(el, keyName, oldProps[keyName], newProps[keyName])
            })

            // 旧有新无 便可移除
            Object.keys(oldProps).forEach(keyName => {
                (!newProps[keyName]) && patchProp(el, keyName, oldProps[keyName], null)
            })
        }
        if (typeof newChildren === 'string') {
            // 都是字符串,但值不同,那么直接替换
            typeof oldChildren === 'string' && newChildren !== oldChildren && setText(el, newChildren)

            // 新children为字符串,旧children为数组,那么直接整个替换
            Array.isArray(oldChildren) && setText(oldEl, newChildren)

            return
        }

        // 非字符串 非数组,那就是错误的数据类型
        if (!Array.isArray(newChildren)) {
            throw 'children is not string or array.'
        }

        // 旧children为字符串,新children为数组,那就先清空dom内容,再生成新children的dom内容挂载到之前清空的这个dom中
        typeof oldChildren === 'string' && (setText(el, ''), newChildren.forEach(vnode => { mountElement(vnode, el) }))

        if (Array.isArray(oldChildren)) {
            const [oldLen, newLen] = [newChildren.length, oldChildren.length]
            const minLen = Math.min(oldLen, newLen)

            // 最小长度的对比,从左到右,按照顺序对比vnode
            for (let i = 0; i < minLen; i++) {
                const [oldVnode, newVnode] = [oldChildren[i], newChildren[i]]
                diff(oldVnode, newVnode)
            }

            // 移除多的节点
            oldLen > minLen && oldChildren.filter((_, i) => i >= minLen).forEach(vnode => remove(vnode.el, el))

            // 添加少的节点
            newLen > minLen && oldChildren.filter((_, i) => i >= minLen).forEach(vnode => mountElement(vnode, el))

        }


    }
}

总结

从图中看更新虚拟dom的节点在更新的时候是针对性的更新数据变更的那部分dom,而真实dom操作一般都是数据发生变化导致整个部分的dom都会重新渲染,从而让页面重新排版重新布局,性能损耗大同时体验也不好。

虚拟DOM的存在是为了提高渲染的性能,减少多余的重绘和回流,原生DOM操作的话,当你数据发生变化,会整块替换。而虚拟dom通过diff对比找到需要更新的dom,从而针对性的替换那一小部分,所以性能更优。

使用虚拟DOM算法的性能损耗 = 虚拟DOM增删改 + diff 真实DOM差异 + 针对性的一小部分渲染时的回流和重绘。
操作真实DOM的的性能损耗 = 真实DOM的增删改 + 大部分或者小部分渲染时的回流和重绘。

综上分析,如果你使用真实DOM的操作,在遇到很大的业务场景时,如果你的经验丰富并且写的非常好,那么肯定比虚拟DOM的性能好,但是这和个人水平及心智上的抗压压力有关系,没有谁愿意写那种非常冗余并且重复、还难以维护的代码、开发效率还低的代码。
所以这就是虚拟dom存在的意义,让你的开发更加的便捷,同时降低了心智上的压力,能够更好的关注具体的业务从而做出更好的应用,性能在绝大多数情况下都是很不错的。

目录
相关文章
|
2天前
|
机器学习/深度学习 人工智能 算法
解密巴黎奥运会中的阿里云AI技术
2024年巴黎奥运会圆满结束,中国代表团金牌数与美国并列第一,展现了卓越实力。阿里云作为官方云服务合作伙伴,通过先进的AI技术深度融入奥运的各项环节,实现了大规模的云上转播,超越传统卫星转播,为全球观众提供流畅、高清的观赛体验。其中,“子弹时间”回放技术在多个场馆的应用,让观众享受到了电影般的多角度精彩瞬间。此外,8K超高清直播、AI智能解说和通义APP等创新,极大地提升了赛事观赏性和互动性。能耗宝(Energy Expert)的部署则助力实现了赛事的可持续发展目标。巴黎奥运会的成功举办标志着体育赛事正式进入AI时代,开启了体育与科技融合的新篇章。
解密巴黎奥运会中的阿里云AI技术
|
10天前
|
开发框架 自然语言处理 API
基于RAG搭建企业级知识库在线问答
本文介绍如何使用搜索开发工作台快速搭建基于RAG开发链路的知识库问答应用。
7573 16
|
17天前
|
弹性计算 关系型数据库 Serverless
函数计算驱动多媒体文件处理:高效、稳定与成本优化实践
本次测评的解决方案《告别资源瓶颈,函数计算驱动多媒体文件处理》展示了如何利用阿里云函数计算高效处理多媒体文件。文档结构清晰、内容详实,适合新客户参考。方案提供了一键部署与手动部署两种方式,前者简便快捷,后者灵活性高但步骤较多。通过部署,用户可体验到基于函数计算的文件处理服务,显著提升处理效率和系统稳定性。此外,测评还对比了应用内处理文件与函数计算处理文件的不同,突出了函数计算在资源管理和成本控制方面的优势。
22673 18
|
11天前
|
SQL 分布式计算 数据库
畅捷通基于Flink的实时数仓落地实践
本文整理自畅捷通总架构师、阿里云MVP专家郑芸老师在 Flink Forward Asia 2023 中闭门会上的分享。
8187 14
畅捷通基于Flink的实时数仓落地实践
|
17天前
|
机器学习/深度学习 存储 人工智能
提升深度学习性能的利器—全面解析PAI-TorchAcc的优化技术与应用场景
在当今深度学习的快速发展中,模型训练和推理的效率变得尤为重要。为了应对计算需求不断增长的挑战,AI加速引擎应运而生。其中,PAI-TorchAcc作为一个新兴的加速引擎,旨在提升PyTorch框架下的计算性能。本文将详细介绍PAI-TorchAcc的基本概念、主要特性,并通过代码实例展示其性能优势。
17683 146
|
11天前
|
前端开发 Java Go
关于智能编码助手【通义灵码】,开发者们这么说...
现在通过体验活动首次完成通义灵码免费下载及使用的新用户,即可获得限量定制帆布包 1 个;分享体验截图到活动页面,还可参与抽奖活动,iPhone15 手机、机械键盘、智能手环等大奖等你拿!
7153 11
|
13天前
|
人工智能 JSON Serverless
【AI 冰封挑战】搭档函数计算,“冰”封你的夏日记忆
夏日炎炎,别让高温打败你的创意,立即体验 ComfyUI 自制冰冻滤镜!无需繁琐的后期技巧,三步开启一段清凉无比的视觉探险。参与实验并上传作品即可获得运动无线蓝牙耳机,限量 800 个,先到先得!
8229 11
|
19天前
|
人工智能 运维 Cloud Native
实战基于阿里云的AIGC在运维领域的探索
传统运维模式已难以应对日益复杂的海量数据和业务需求,效率低下,故障难解。而人工智能的崛起,特别是AIGC技术的出现,为运维领域带来了新的机遇。AIGC能够自动生成运维脚本、分析海量数据,预测潜在故障,甚至提供解决方案,为运维工作注入智能化力量,推动运维向更高效、更智能的方向发展。
16240 18
实战基于阿里云的AIGC在运维领域的探索
|
19天前
|
机器学习/深度学习 自然语言处理 算法
未来语音交互新纪元:FunAudioLLM技术揭秘与深度评测
人类自古以来便致力于研究自身并尝试模仿,早在2000多年前的《列子·汤问》中,便记载了巧匠们创造出能言善舞的类人机器人的传说。
11462 112
|
27天前
|
存储 SQL OLAP
分析性能提升40%,阿里云Hologres流量场景最佳实践
分析性能提升40%,阿里云Hologres流量场景最佳实践