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

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

[[409084]]

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

什么是虚拟DOM?

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

// 将属性赋值给对象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"); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

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

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

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"]) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

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

document.querySelector("ul.some-selector").innerHTML = generateList(["Banana""Apple""Mango"]) 
  • 1.

看看出了什么问题?

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

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

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

<div class="contents"
    <p>Text here</p> 
    <p>Some other <b>Bold</b> content</p> 
</div> 
  • 1.
  • 2.
  • 3.
  • 4.

 它可以写作以下VDOM对象:

let vdom = { 
    tag: "div"
    props: { class: 'contents' }, 
    children: [ 
        { 
            tag: "p"
            children: "Text here" 
        }, 
        { 
            tag: "p"
            children: ["Some other ", { tag: "b", children: "Bold" }, " content"
        } 
 
    ] 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

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

VDOM是一个对象,带有:

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

我们这样使用 VDOM:

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

有什么好处?

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

function generateList(list) { 
    // VDOM 生成过程,待下补上 

 
patch(oldUL, generateList(["Banana""Apple""Orange"])); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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

patch(oldUL, generateList(["Banana""Apple""Mango"])); 
  • 1.

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

构建 VDOM!

我们需要做4件事:

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

创建 vnode

function createVNode(tag, props = {}, children = []) { 
    return { tag, props, children} 

  • 1.
  • 2.
  • 3.

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

挂载 VDOM

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

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

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

function mount(vnode, container) { ... } 
  • 1.

创建DOM元素

const element = (vnode.element = document.createElement(vnode.tag)) 
  • 1.

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

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

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

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

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

container.appendChild(element) 
  • 1.

最终的结果:

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) 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

卸载 vnode

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

function unmount(vnode) { 
    vnode.element.parentNode.removeChild(vnode.element) 

  • 1.
  • 2.
  • 3.

patch vnode.

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

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) 
                }) 
            } 
 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.

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

现在让我们渲染一个vdom!

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

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 */) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

线上示例:https://codepen.io/SiddharthShyniben/pen/MWpQrwM

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

作者:Siddharth

译者:前端小智 来源:dev原文:https://dev.to/siddharthshyniben/what-is-the-virtual-dom-let-s-build-it-5070

 

责任编辑:姜华 来源: 大迁世界
相关推荐

2017-03-02 13:31:02

监控系统

2022-01-10 11:04:41

单链表面试编程

2023-12-16 13:21:00

Python元类ORM

2015-06-02 10:24:43

iOS网络请求降低耦合

2015-06-02 09:51:40

iOS网络请求封装接口

2022-05-06 19:42:53

DOM

2015-06-02 09:41:00

iOS网络请求NSURLSessio

2021-05-26 05:22:09

Virtual DOMSnabbdom虚拟DOM

2021-01-28 07:21:13

算法虚拟DOM前端

2021-02-20 09:45:02

RPC框架Java

2020-10-12 08:56:47

Virtual dom

2014-02-14 09:37:01

JavascriptDOM

2021-02-22 17:17:38

Proxy缓存代码

2022-10-31 08:27:53

Database数据数据库

2014-07-29 09:44:58

jQuery源码

2024-04-24 11:42:21

Redis延迟消息数据库

2013-06-18 09:51:52

PomeloPomelo平台搭建平台

2017-08-11 17:55:48

前端JavaScript模板引擎

2020-12-17 06:19:36

安全隐私个人信息

2022-03-09 09:43:01

工具类线程项目
点赞
收藏

51CTO技术栈公众号