
你可能听说过Virtual DOM(以及Shadow DOM)。动手的V读源甚至可能使用过它(JSX基本上是简易M加VDOM的语法糖)。如果你想了解更多,强阅那么就看看今天这篇文章。动手的V读源
什么是简易M加虚拟DOM?
DOM操作很贵。做一次时,强阅差异可能看起来很小(分配一个属性给一个对象之间大约0.4毫秒的动手的V读源差异),但它会随着时间的简易M加推移而增加。
// 将属性赋值给对象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,动手的V读源而第二个循环花费了约41ms。简易M加
我们举一个更真实的强阅例子。
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",动手的V读源 "Apple", "Orange"]) 到目前为止,一切都好。简易M加现在,强阅如果数组改变,我们需要重新渲染,我们这样做:
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/SiddharthShyniben/pen/MWpQrwM
~完,我是小智,SPA 走一波,下期见!
作者:Siddharth
译者:前端小智 来源:dev原文:https://dev.to/siddharthshyniben/what-is-the-virtual-dom-let-s-build-it-5070
