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

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

什么是虚拟DOM?

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

 
 
 
 
  1. // 将属性赋值给对象1000次
  2. let obj = {};
  3. console.time("obj");
  4. for (let i = 0; i < 1000; i++) {
  5.   obj[i] = i;
  6. }
  7. console.timeEnd("obj");
  8. // 操纵dom 1000次
  9. console.time("dom");
  10. for (let i = 0; i < 1000; i++) {
  11.   document.querySelector(".some-element").innerHTML += i;
  12. }
  13. console.timeEnd("dom");

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

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

 
 
 
 
  1. function generateList(list) {
  2.     let ul = document.createElement('ul');
  3.     document.getElementByClassName('.fruits').appendChild(ul);
  4.     list.forEach(function (item) {
  5.         let li = document.createElement('li');
  6.         ul.appendChild(li);
  7.         li.innerHTML += item;
  8.     });
  9.     return ul;
  10. }
  11. document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Orange"])

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

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

看看出了什么问题?

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

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

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

  
 
 
 
  1.     

    Text here

  2.     

    Some other Bold content

 它可以写作以下VDOM对象:

 
 
 
 
  1. let vdom = {
  2.     tag: "div",
  3.     props: { class: 'contents' },
  4.     children: [
  5.         {
  6.             tag: "p",
  7.             children: "Text here"
  8.         },
  9.         {
  10.             tag: "p",
  11.             children: ["Some other ", { tag: "b", children: "Bold" }, " content"]
  12.         }
  13.     ]
  14. }

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

VDOM是一个对象,带有:

我们这样使用 VDOM:

有什么好处?

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

 
 
 
 
  1. function generateList(list) {
  2.     // VDOM 生成过程,待下补上
  3. }
  4. patch(oldUL, generateList(["Banana", "Apple", "Orange"]));

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

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

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

构建 VDOM!

我们需要做4件事:

创建 vnode

 
 
 
 
  1. function createVNode(tag, props = {}, children = []) {
  2.     return { tag, props, children}
  3. }

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

挂载 VDOM

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

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

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

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

创建DOM元素

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

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

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

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

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

 
 
 
 
  1. if (typeof vnode.children === 'string') {
  2.     element.textContent = vnode.children
  3. } else {
  4.     vnode.children.forEach(child => {
  5.         mount(child, element) // 递归挂载子节点
  6.     })
  7. }

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

 
 
 
 
  1. container.appendChild(element)

最终的结果:

 
 
 
 
  1. function mount(vnode, container) { 
  2.     const element = (vnode.element = document.createElement(vnode.tag))
  3.     Object.entries(vnode.props || {}).forEach([key, value] => {
  4.         element.setAttribute(key, value)
  5.     })
  6.     if (typeof vnode.children === 'string') {
  7.         element.textContent = vnode.children
  8.     } else {
  9.         vnode.children.forEach(child => {
  10.             mount(child, element) // Recursively mount the children
  11.         })
  12.     }
  13.     container.appendChild(element)
  14. }

卸载 vnode

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

 
 
 
 
  1. function unmount(vnode) {
  2.     vnode.element.parentNode.removeChild(vnode.element)
  3. }

patch vnode.

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

 
 
 
 
  1. function patch(VNode1, VNode2) {
  2.     // 指定父级元素
  3.     const element = (VNode2.element = VNode1.element);
  4.     // 现在我们要检查两个vnode之间的区别
  5.     // 如果节点具有不同的标记,则说明整个内容已经更改。
  6.     if (VNode1.tag !== VNode2.tag) {
  7.         // 只需卸载旧节点并挂载新节点
  8.         mount(VNode2, element.parentNode)
  9.         unmount(Vnode1)
  10.     } else {
  11.         // 节点具有相同的标签
  12.         // 所以我们要检查两个部分
  13.         // - Props
  14.         // - Children
  15.         // 这里不打算检查 Props,因为它会增加代码的复杂性,我们先来看怎么检查 Children 就行啦
  16.         // 检查 Children
  17.         // 如果新节点的 children 是字符串
  18.         if (typeof VNode2.children == "string") {
  19.             // 如果两个孩子完全不同
  20.             if (VNode2.children !== VNode1.children) {
  21.                 element.textContent = VNode2.children;
  22.             }
  23.         } else {
  24.             // 如果新节点的 children 是一个数组
  25.             // - children 的长度是一样的
  26.             // - 旧节点比新节点有更多的子节点
  27.             // - 新节点比旧节点有更多的子节点
  28.             // 检查长度
  29.             const children1 = VNode1.children;
  30.             const children2 = VNode2.children;
  31.             const commonLen = Math.min(children1.length, children2.length)
  32.             // 递归地调用所有公共子节点的patch
  33.             for (let i = 0; i < commonLen; i++) {
  34.                 patch(children1[i], children2[i])
  35.             }
  36.             // 如果新节点的children 比旧节点的少
  37.             if (children1.length > children2.length) {
  38.                 children1.slice(children2.length).forEach(child => {
  39.                     unmount(child)
  40.                 })
  41.             }
  42.             //  如果新节点的children 比旧节点的多
  43.             if (children2.length > children1.length) {
  44.                 children2.slice(children1.length).forEach(child => {
  45.                     mount(child, element)
  46.                 })
  47.             }
  48.         }
  49.     }
  50. }

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

现在让我们渲染一个vdom!

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

 
 
 
 
  1. function generateList(list) {
  2.     let children = list.map(child => createVNode("li", null, child));
  3.     return createVNode("ul", { class: 'fruits-ul' }, children)
  4. }
  5. 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

网页名称:动手写一个简易的VirtualDOM,加强阅读源码的能力
分享URL:http://www.shufengxianlan.com/qtweb/news9/312059.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

猜你还喜欢下面的内容

响应式网站知识

分类信息网