乐趣区

关于javascript:动手写一个简易的-Virtual-DOM加强阅读源码的能力

作者: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… 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

退出移动版