前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >动手写一个简易的 Virtual DOM,加强阅读源码的能力

动手写一个简易的 Virtual DOM,加强阅读源码的能力

作者头像
前端小智@大迁世界
发布2022-06-15 14:26:17
2270
发布2022-06-15 14:26:17
举报
文章被收录于专栏:终身学习者

作者:Siddharth 译者:前端小智 来源:dev

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

你可能听说过Virtual DOM(以及Shadow DOM)。甚至可能使用过它(JSX基本上是VDOM的语法糖)。如果你想了解更多,那么就看看今天这篇文章。

什么是虚拟DOM?

DOM操作很贵。做一次时,差异可能看起来很小(分配一个属性给一个对象之间大约0.4毫秒的差异),但它会随着时间的推移而增加。

代码语言:javascript
复制
// 将属性赋值给对象1000次
let obj = {};
console.time("obj");
for (let i = 0; i < 1000; i++) {
  obj[i] = i;
}
console.timeEnd("obj");

// 操纵dom 1000次
console.time("dom");
for (let i = 0; i < 1000; i++) {
  document.querySelector(".some-element").innerHTML += i;
}
console.timeEnd("dom");

当我运行上面的代码片段时,我发现第一个循环花费了约3ms,而第二个循环花费了约41ms

我们举一个更真实的例子。

代码语言:javascript
复制
function generateList(list) {
    let ul = document.createElement('ul');
    document.getElementByClassName('.fruits').appendChild(ul);

    list.forEach(function (item) {
        let li = document.createElement('li');
        ul.appendChild(li);
        li.innerHTML += item;
    });

    return ul;
}

document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Orange"])

到目前为止,一切都好。现在,如果数组改变,我们需要重新渲染,我们这样做:

代码语言:javascript
复制
document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Mango"])

看看出了什么问题?

即使只需要改变一个元素,我们也会改变整个元素,因为我们很懒。

这就是为什么创建了虚拟DOM的原因。那什么是虚拟 Dom?

Virtual DOM是DOM作为对象的表示。 假设我们有下面的 HTML:

代码语言:javascript
复制
<div class="contents">
    <p>Text here</p>
    <p>Some other <b>Bold</b> content</p>
</div>

它可以写作以下VDOM对象:

代码语言:javascript
复制
let vdom = {
    tag: "div",
    props: { class: 'contents' },
    children: [
        {
            tag: "p",
            children: "Text here"
        },
        {
            tag: "p",
            children: ["Some other ", { tag: "b", children: "Bold" }, " content"]
        }

    ]
}

请注意,实际开发中可能存在更多属性,这是一个简化的版本。

VDOM是一个对象,带有:

  • 一个名为tag(有时也称为type)的属性,它表示标签的名称
  • 一个名为props的属性,包含所有 props
  • 如果内容只是文本,则为字符串
  • 如果内容包含元素,则vdom数组

我们这样使用 VDOM:

  • 我们改变了vdom而不是dom
  • 函数检查DOM和VDOM之间的所有差异,只更改变化的部分
  • 改变VDOM被标记为最新的改变,这样我们下次比较VDOM时就可以节省更多的时间。

有什么好处?

知道了什么是 VDOM,我们来改进一下前面的 generateList函数。

代码语言:javascript
复制
function generateList(list) {
    // VDOM 生成过程,待下补上
}

patch(oldUL, generateList(["Banana", "Apple", "Orange"]));

不要介意patch函数,它的作用是就将更改的部分附加到DOM中。以后再改变DOM时:

代码语言:javascript
复制
patch(oldUL, generateList(["Banana", "Apple", "Mango"]));

patch函数发现只有第三个li发生了变化,,而不是所有三个元素都发生了变化,所以只会操作第三个 li 元素。

构建 VDOM!

我们需要做4件事:

  • 创建一个虚拟节点(vnode)
  • 挂载 VDOM
  • 卸载 VDOM
  • Patch (比较两个vnode,找出差异,然后挂载)

创建 vnode

代码语言:javascript
复制
function createVNode(tag, props = {}, children = []) {
    return { tag, props, children}
}

在Vue(和许多其他地方)中,此函数称为 h,hyperscript 的缩写。

挂载 VDOM

通过挂载,将vnode附加到任何容器,如#app或任何其他应该挂载它的地方。

这个函数将递归遍历所有节点的子节点,并将它们挂载到各自的容器中。

注意,下面的所有代码都放在挂载函数中。

代码语言:javascript
复制
function mount(vnode, container) { ... }

创建DOM元素

代码语言:javascript
复制
const element = (vnode.element = document.createElement(vnode.tag))

你可能会想这个vnode.element是什么。 它只是一个内部设置的属性,我们可以根据它知道哪个元素是vnode的父元素。

props 对象设置所有属性。我们可以对它们进行循环

代码语言:javascript
复制
Object.entries(vnode.props || {}).forEach([key, value] => {
    element.setAttribute(key, value)
})

挂载子元素,有两种情况需要处理:

  • children 只是文本
  • children 是 vnode 数组
代码语言:javascript
复制
if (typeof vnode.children === 'string') {
    element.textContent = vnode.children
} else {
    vnode.children.forEach(child => {
        mount(child, element) // 递归挂载子节点
    })
}

最后,我们必须将内容添加到DOM中:

代码语言:javascript
复制
container.appendChild(element)

最终的结果:

代码语言:javascript
复制
function mount(vnode, container) { 
    const element = (vnode.element = document.createElement(vnode.tag))

    Object.entries(vnode.props || {}).forEach([key, value] => {
        element.setAttribute(key, value)
    })

    if (typeof vnode.children === 'string') {
        element.textContent = vnode.children
    } else {
        vnode.children.forEach(child => {
            mount(child, element) // Recursively mount the children
        })
    }

    container.appendChild(element)
}

卸载 vnode

卸载就像从DOM中删除一个元素一样简单:

代码语言:javascript
复制
function unmount(vnode) {
    vnode.element.parentNode.removeChild(vnode.element)
}

patch vnode.

这是我们必须编写的(相对而言)最复杂的函数。要做的事情就是找出两个vnode之间的区别,只对更改部分进行 patch。

代码语言:javascript
复制
function patch(VNode1, VNode2) {
    // 指定父级元素
    const element = (VNode2.element = VNode1.element);

    // 现在我们要检查两个vnode之间的区别

    // 如果节点具有不同的标记,则说明整个内容已经更改。
    if (VNode1.tag !== VNode2.tag) {
        // 只需卸载旧节点并挂载新节点
        mount(VNode2, element.parentNode)
        unmount(Vnode1)
    } else {
        // 节点具有相同的标签
        // 所以我们要检查两个部分
        // - Props
        // - Children

        // 这里不打算检查 Props,因为它会增加代码的复杂性,我们先来看怎么检查 Children 就行啦

        // 检查 Children
        // 如果新节点的 children 是字符串
        if (typeof VNode2.children == "string") {
            // 如果两个孩子完全不同
            if (VNode2.children !== VNode1.children) {
                element.textContent = VNode2.children;
            }
        } else {
            // 如果新节点的 children 是一个数组
            // - children 的长度是一样的
            // - 旧节点比新节点有更多的子节点
            // - 新节点比旧节点有更多的子节点

            // 检查长度
            const children1 = VNode1.children;
            const children2 = VNode2.children;
            const commonLen = Math.min(children1.length, children2.length)

            // 递归地调用所有公共子节点的patch
            for (let i = 0; i < commonLen; i++) {
                patch(children1[i], children2[i])
            }

            // 如果新节点的children 比旧节点的少
            if (children1.length > children2.length) {
                children1.slice(children2.length).forEach(child => {
                    unmount(child)
                })
            }

            //  如果新节点的children 比旧节点的多
            if (children2.length > children1.length) {
                children2.slice(children1.length).forEach(child => {
                    mount(child, element)
                })
            }

        }
    }
}

这是vdom实现的一个基本版本,方便我们快速掌握这个概念。当然还有一些事情要做,包括检查 props 和一些性能方面的改进。

现在让我们渲染一个vdom!

回到generateList例子。对于我们的vdom实现,我们可以这样做

代码语言:javascript
复制
function generateList(list) {
    let children = list.map(child => createVNode("li", null, child));

    return createVNode("ul", { class: 'fruits-ul' }, children)
}

mount(generateList(["apple", "banana", "orange"]), document.querySelector("#app")/* any selector */)

线上示例:https://codepen.io/SiddharthS...

~完,我是小智,SPA 走一波,下期见!


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/siddharthshyni...

交流

本文 GitHub https://github.com/qq44924588... 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是虚拟DOM?
  • 有什么好处?
  • 构建 VDOM!
  • 创建 vnode
  • 挂载 VDOM
  • 卸载 vnode
  • patch vnode.
  • 交流
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档