4

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

 2 years ago
source link: https://segmentfault.com/a/1190000040120149
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

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

你可能听说过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");

当我运行上面的代码片段时,我发现第一个循环花费了约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"])

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

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

看看出了什么问题?

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

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

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

<div class="contents">
    <p>Text here</p>
    <p>Some other <b>Bold</b> content</p>
</div>

它可以写作以下VDOM对象:

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函数。

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

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

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

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

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

构建 VDOM!

我们需要做4件事:

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

创建 vnode

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

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

挂载 VDOM

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

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

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

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

创建DOM元素

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

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

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

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

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

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

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

container.appendChild(element)

最终的结果:

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中删除一个元素一样简单:

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

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

        }
    }
}

这是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 */)

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

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


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

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

有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK