文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Virtual DOM到底有什么迷人之处?如何搭建一款迷你版Virtual DOM库?

2024-12-03 03:13

关注

首先,我们创建一个index.html文件,写一下我们需要展示的内容,内容如下:

  1.  
  2. "en"
  3.  
  4.  
  5.     "UTF-8"
  6.     "X-UA-Compatible" content="IE=edge"
  7.     name="viewport" content="width=device-width, initial-scale=1.0"
  8.     vdom 
  9.      
  10.  
  11.  
  12.  
  13.     "app">
 
  •     "./vdom.js"
  •      
  •  
  •  
  • 我们在body标签内创建了一个id是app的DOM元素,用于被挂载节点。接着我们引入了一个vdom.js文件,这个文件就是我们将要实现的迷你版Virtual DOM库。最后,我们在script标签内定义了一个render方法,返回为一个h方法。调用mountNode方法挂载到id是app的DOM元素上。h方法中数据结构我们是借鉴snabbdom库,第一个参数是标签名,第二个参数是属性,最后一个参数是子节点。还有,你可能会注意到在h方法中我们使用了useStyleStr方法,这个方法主要作用是将style样式转化成页面能识别的结构,实现代码我会在最后给出。

    思路理清楚了,展示页面的代码也写完了。下面我们将重点看下vdom.js,如何一步一步地实现它。

    第一步

    我们看到index.html文件中首先需要调用mountNode方法,所以,我们先在vdom.js文件中定义一个mountNode方法。

    1. // Mount node 
    2. function mountNode(render, selector) { 
    3.  

    接着,我们会看到mountNode方法第一个参数是render方法,render方法返回了h方法,并且看到第一个参数是标签,第二个参数是属性,第三个参数是子节点。

    那么,我们接着在vdom.js文件中再定义一个h方法。

    1.  function h(tag, props, children) { 
    2.     return { tag, props, children }; 

    还没有结束,我们需要根据传入的三个参数tag、props、children来挂载到页面上。

    我们需要这样操作。我们在mountNode方法内封装一个mount方法,将传给mountNode方法的参数经过处理传给mount方法。

    1. // Mount node 
    2. function mountNode(render, selector) { 
    3.   mount(render(), document.querySelector(selector)) 

    接着,我们定义一个mount方法。

    1. function mount(vnode, container) { 
    2.     const el = document.createElement(vnode.tag); 
    3.     vnode.el = el; 
    4.     // props 
    5.     if (vnode.props) { 
    6.         for (const key in vnode.props) { 
    7.             if (key.startsWith('on')) { 
    8.                 el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{ 
    9.                     passive:true 
    10.                 }) 
    11.             } else { 
    12.                 el.setAttribute(key, vnode.props[key]); 
    13.             } 
    14.         } 
    15.     } 
    16.     if (vnode.children) { 
    17.         if (typeof vnode.children === "string") { 
    18.             el.textContent = vnode.children; 
    19.         } else { 
    20.             vnode.children.forEach(child => { 
    21.                 mount(child, el); 
    22.             }); 
    23.         } 
    24.     } 
    25.      
    26.     container.appendChild(el); 

    第一个参数是调用传进来的render方法,它返回的是h方法,而h方返回一个同名参数的对象{ tag, props, children },那么我们就可以通过vnode.tag、vnode.props、vnode.children取到它们。

    我们看到先是判断属性,如果属性字段开头含有,on标识就是代表事件,那么就从属性字段第三位截取,利用addEventListenerAPI创建一个监听事件。否则,直接利用setAttributeAPI设置属性。

    接着,再判断子节点,如果是字符串,我们直接将字符串赋给文本节点。否则就是节点,我们就递归调用mount方法。

    最后,我们将使用appendChildAPI把节点内容挂载到真实DOM中。

    页面正常显示。

    第二步

    我们知道Virtual DOM有以下两个特性:

    1. Virtual DOM可以维护程序的状态,跟踪上一次的状态。
    2. 通过比较前后两次的状态差异更新真实DOM。

    这就利用到了我们之前提到的diff算法。

    我们首先定义一个patch方法。因为要对比前后状态的差异,所以第一个参数是旧节点,第二个参数是新节点。

    1. function patch(n1, n2) { 
    2.     

    下面,我们还需要做一件事,那就是完善mountNode方法,为什么这样操作呢?是因为当状态改变时,只更新状态改变的DOM,也就是我们所说的差异更新。这时就需要配合patch方法做diff算法。

    相比之前,我们加上了对是否挂载节点进行了判断。如果没有挂载的话,就直接调用mount方法挂载节点。否则,调用patch方法进行差异更新。

    1. let isMounted = false
    2. let oldTree; 
    3.  
    4. // Mount node 
    5. function mountNode(render, selector) { 
    6.     if (!isMounted) { 
    7.         mount(oldTree = render(), document.querySelector(selector)); 
    8.         isMounted = true
    9.     } else { 
    10.         const newTree = render(); 
    11.         patch(oldTree, newTree); 
    12.         oldTree = newTree; 
    13.     } 
    14.  

    那么下面我们将主动看下patch方法,这也是在这个库中最复杂的方法。

    1. function patch(n1, n2) { 
    2.     // Implement this 
    3.     // 1. check if n1 and n2 are of the same type 
    4.     if (n1.tag !== n2.tag) { 
    5.         // 2. if notreplace 
    6.         const parent = n1.el.parentNode; 
    7.         const anchor = n1.el.nextSibling; 
    8.         parent.removeChild(n1.el); 
    9.         mount(n2, parent, anchor); 
    10.         return 
    11.     } 
    12.  
    13.     const el = n2.el = n1.el; 
    14.  
    15.     // 3. if yes 
    16.     // 3.1 diff props 
    17.     const oldProps = n1.props || {}; 
    18.     const newProps = n2.props || {}; 
    19.     for (const key in newProps) { 
    20.         const newValue = newProps[key]; 
    21.         const oldValue = oldProps[key]; 
    22.         if (newValue !== oldValue) { 
    23.             if (newValue != null) { 
    24.                 el.setAttribute(key, newValue); 
    25.             } else { 
    26.                 el.removeAttribute(key); 
    27.             } 
    28.         } 
    29.     } 
    30.     for (const key in oldProps) { 
    31.         if (!(key in newProps)) { 
    32.             el.removeAttribute(key); 
    33.         } 
    34.     } 
    35.     // 3.2 diff children 
    36.     const oc = n1.children; 
    37.     const nc = n2.children; 
    38.     if (typeof nc === 'string') { 
    39.         if (nc !== oc) { 
    40.             el.textContent = nc; 
    41.         } 
    42.     } else if (Array.isArray(nc)) { 
    43.         if (Array.isArray(oc)) { 
    44.             // array diff 
    45.             const commonLength = Math.min(oc.length, nc.length); 
    46.             for (let i = 0; i < commonLength; i++) { 
    47.                 patch(oc[i], nc[i]); 
    48.             } 
    49.             if (nc.length > oc.length) { 
    50.                 nc.slice(oc.length).forEach(c => mount(c, el)); 
    51.             } else if (oc.length > nc.length) { 
    52.                 oc.slice(nc.length).forEach(c => { 
    53.                     el.removeChild(c.el); 
    54.                 }) 
    55.             } 
    56.         } else { 
    57.             el.innerHTML = ''
    58.             nc.forEach(c => mount(c, el)); 
    59.         } 
    60.     } 

    我们从patch方法入参开始,两个参数分别是在mountNode方法中传进来的旧节点oldTree和新节点newTree,首先我们进行对新旧节点的标签进行对比。

    如果新旧节点的标签不相等,就移除旧节点。另外,利用nextSiblingAPI取指定节点之后紧跟的节点(在相同的树层级中)。然后,传给mount方法第三个参数。这时你可能会有疑问,mount方法不是有两个参数吗?对,但是这里我们需要传进去第三个参数,主要是为了对同级节点进行处理。

    1. if (n1.tag !== n2.tag) { 
    2.       // 2. if notreplace 
    3.       const parent = n1.el.parentNode; 
    4.       const anchor = n1.el.nextSibling; 
    5.       parent.removeChild(n1.el); 
    6.       mount(n2, parent, anchor); 
    7.       return 
    8.   } 

    所以,我们重新修改下mount方法。我们看到我们只是加上了对anchor参数是否为空的判断。

    如果anchor参数不为空,我们使用insertBeforeAPI,在参考节点之前插入一个拥有指定父节点的子节点。insertBeforeAPI第一个参数是用于插入的节点,第二个参数将要插在这个节点之前,如果这个参数为 null 则用于插入的节点将被插入到子节点的末尾。

    如果anchor参数为空,直接在父节点下的子节点列表末尾添加子节点。

    1. function mount(vnode, container, anchor) { 
    2.     const el = document.createElement(vnode.tag); 
    3.     vnode.el = el; 
    4.     // props 
    5.     if (vnode.props) { 
    6.         for (const key in vnode.props) { 
    7.             if (key.startsWith('on')) { 
    8.                 el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{ 
    9.                     passive:true 
    10.                 }) 
    11.             } else { 
    12.                 el.setAttribute(key, vnode.props[key]); 
    13.             } 
    14.         } 
    15.     } 
    16.     if (vnode.children) { 
    17.         if (typeof vnode.children === "string") { 
    18.             el.textContent = vnode.children; 
    19.         } else { 
    20.             vnode.children.forEach(child => { 
    21.                 mount(child, el); 
    22.             }); 
    23.         } 
    24.     } 
    25.     if (anchor) { 
    26.         container.insertBefore(el, anchor); 
    27.     } else { 
    28.         container.appendChild(el); 
    29.     } 

    下面,我们再回到patch方法。如果新旧节点的标签相等,我们首先要遍历新旧节点的属性。我们先遍历新节点的属性,判断新旧节点的属性值是否相同,如果不相同,再进行进一步处理。判断新节点的属性值是否为null,否则直接移除属性。然后,遍历旧节点的属性,如果属性名不在新节点属性表中,则直接移除属性。

    分析完了对新旧节点属性的对比,接下来,我们来分析第三个参数子节点。

    首先,我们分别定义两个变量oc、nc,分别赋予旧节点的children属性和新节点的children属性。如果新节点的children属性是字符串,并且新旧节点的内容不相同,那么就直接将新节点的文本内容赋予即可。

    接下来,我们看到利用Array.isArray()方法判断新节点的children属性是否是数组,如果是数组的话,就执行下面这些代码。

    1. else if (Array.isArray(nc)) { 
    2.         if (Array.isArray(oc)) { 
    3.             // array diff 
    4.             const commonLength = Math.min(oc.length, nc.length); 
    5.             for (let i = 0; i < commonLength; i++) { 
    6.                 patch(oc[i], nc[i]); 
    7.             } 
    8.             if (nc.length > oc.length) { 
    9.                 nc.slice(oc.length).forEach(c => mount(c, el)); 
    10.             } else if (oc.length > nc.length) { 
    11.                 oc.slice(nc.length).forEach(c => { 
    12.                     el.removeChild(c.el); 
    13.                 }) 
    14.             } 
    15.         } else { 
    16.             el.innerHTML = ''
    17.             nc.forEach(c => mount(c, el)); 
    18.         } 
    19.     } 

    我们看到里面又判断旧节点的children属性是否是数组。

    如果是,我们取新旧子节点数组的长度两者的最小值。然后,我们将其循环递归patch方法。为什么取最小值呢?是因为如果取的是他们共有的长度。然后,每次遍历递归时,判断nc.length和oc.length的大小,循环执行对应的方法。

    如果不是,直接将节点内容清空,重新循环执行mount方法。

    这样,我们搭建的迷你版Virtual DOM库就这样完成了。

    页面如下所示。

    源码

    index.html

    1.  
    2. "en"
    3.  
    4.  
    5.     "UTF-8"
    6.     "X-UA-Compatible" content="IE=edge"
    7.     name="viewport" content="width=device-width, initial-scale=1.0"
    8.     vdom 
    9.      
    10.  
    11.  
    12.  
    13.     "app">
     
  •     "./vdom.js"
  •      
  •  
  •  
  •  
  • vdom.js

    1.  // vdom --- 
    2.  function h(tag, props, children) { 
    3.     return { tag, props, children }; 
    4.  
    5. function mount(vnode, container, anchor) { 
    6.     const el = document.createElement(vnode.tag); 
    7.     vnode.el = el; 
    8.     // props 
    9.     if (vnode.props) { 
    10.         for (const key in vnode.props) { 
    11.             if (key.startsWith('on')) { 
    12.                 el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{ 
    13.                     passive:true 
    14.                 }) 
    15.             } else { 
    16.                 el.setAttribute(key, vnode.props[key]); 
    17.             } 
    18.         } 
    19.     } 
    20.     if (vnode.children) { 
    21.         if (typeof vnode.children === "string") { 
    22.             el.textContent = vnode.children; 
    23.         } else { 
    24.             vnode.children.forEach(child => { 
    25.                 mount(child, el); 
    26.             }); 
    27.         } 
    28.     } 
    29.     if (anchor) { 
    30.         container.insertBefore(el, anchor); 
    31.     } else { 
    32.         container.appendChild(el); 
    33.     } 
    34.  
    35. // processing strings 
    36. function useStyleStr(obj) { 
    37.     const reg = /^{|}/g; 
    38.     const reg1 = new RegExp('"',"g"); 
    39.     const str = JSON.stringify(obj); 
    40.     const ustr = str.replace(reg, '').replace(','';').replace(reg1,''); 
    41.     return ustr; 
    42.  
    43. function patch(n1, n2) { 
    44.     // Implement this 
    45.     // 1. check if n1 and n2 are of the same type 
    46.     if (n1.tag !== n2.tag) { 
    47.         // 2. if notreplace 
    48.         const parent = n1.el.parentNode; 
    49.         const anchor = n1.el.nextSibling; 
    50.         parent.removeChild(n1.el); 
    51.         mount(n2, parent, anchor); 
    52.         return 
    53.     } 
    54.  
    55.     const el = n2.el = n1.el; 
    56.  
    57.     // 3. if yes 
    58.     // 3.1 diff props 
    59.     const oldProps = n1.props || {}; 
    60.     const newProps = n2.props || {}; 
    61.     for (const key in newProps) { 
    62.         const newValue = newProps[key]; 
    63.         const oldValue = oldProps[key]; 
    64.         if (newValue !== oldValue) { 
    65.             if (newValue != null) { 
    66.                 el.setAttribute(key, newValue); 
    67.             } else { 
    68.                 el.removeAttribute(key); 
    69.             } 
    70.         } 
    71.     } 
    72.     for (const key in oldProps) { 
    73.         if (!(key in newProps)) { 
    74.             el.removeAttribute(key); 
    75.         } 
    76.     } 
    77.     // 3.2 diff children 
    78.     const oc = n1.children; 
    79.     const nc = n2.children; 
    80.     if (typeof nc === 'string') { 
    81.         if (nc !== oc) { 
    82.             el.textContent = nc; 
    83.         } 
    84.     } else if (Array.isArray(nc)) { 
    85.         if (Array.isArray(oc)) { 
    86.             // array diff 
    87.             const commonLength = Math.min(oc.length, nc.length); 
    88.             for (let i = 0; i < commonLength; i++) { 
    89.                 patch(oc[i], nc[i]); 
    90.             } 
    91.             if (nc.length > oc.length) { 
    92.                 nc.slice(oc.length).forEach(c => mount(c, el)); 
    93.             } else if (oc.length > nc.length) { 
    94.                 oc.slice(nc.length).forEach(c => { 
    95.                     el.removeChild(c.el); 
    96.                 }) 
    97.             } 
    98.         } else { 
    99.             el.innerHTML = ''
    100.             nc.forEach(c => mount(c, el)); 
    101.         } 
    102.     } 
    103.  
    104. let isMounted = false
    105. let oldTree; 
    106.  
    107. // Mount node 
    108. function mountNode(render, selector) { 
    109.     if (!isMounted) { 
    110.         mount(oldTree = render(), document.querySelector(selector)); 
    111.         isMounted = true
    112.     } else { 
    113.         const newTree = render(); 
    114.         patch(oldTree, newTree); 
    115.         oldTree = newTree; 
    116.     } 
    117.  

     

    来源:前端历劫之路内容投诉

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    软考中级精品资料免费领

    • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

      难度     813人已做
      查看
    • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

      难度     354人已做
      查看
    • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

      难度     318人已做
      查看
    • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

      难度     435人已做
      查看
    • 2024年上半年系统架构设计师考试综合知识真题

      难度     224人已做
      查看

    相关文章

    发现更多好内容

    猜你喜欢

    AI推送时光机
    位置:首页-资讯-后端开发
    咦!没有更多了?去看看其它编程学习网 内容吧
    首页课程
    资料下载
    问答资讯