当前位置:首页 > 通信资讯 > 正文

virtual DOM(virtual dom有什么好处)

virtual DOM(virtual dom有什么好处)

为什么使用Virtual DOM

  • 手动操作DOM比较麻烦。还需要考虑浏览器兼容性问题,虽然有JQuery等库简化DOM操作,但是随着项目的复杂DOM操作复杂提升。
  • 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM框架解决了视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了
  • Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,Virtual DOM内部将弄清楚如何有效的更新DOM(利用Diff算法实现)。

Virtual DOM的特性

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

实现一个基础的Virtual DOM库

我们可以仿照snabbdom库https://github.com/snabbdom/snabbdom.git自己动手实现一款迷你版Virtual DOM库。

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

  1. <!DOCTYPEhtml>
  2. <htmllang="en">
  3. <head>
  4. <metacharset="UTF-8">
  5. <metahttp-equiv="X-UA-Compatible"content="IE=edge">
  6. <metaname="viewport"content="width=device-width,initial-scale=1.0">
  7. <title>vdom</title>
  8. <style>
  9. .main{
  10. color:#00008b;
  11. }
  12. .main1{
  13. font-weight:bold;
  14. }
  15. </style>
  16. </head>
  17. <body>
  18. <divid="app"></div>
  19. <scriptsrc="./vdom.js"></script>
  20. <script>
  21. functionrender(){
  22. returnh('div',{
  23. style:useObjStr({
  24. 'color':'#ccc',
  25. 'font-size':'20px'
  26. })
  27. },[
  28. h('div',{},[h('span',{
  29. onClick:()=>{
  30. alert('1');
  31. }
  32. },'文本'),h('a',{
  33. href:'https://www.baidu.com',
  34. class:'mainmain1'
  35. },'点击')
  36. ]),
  37. ])
  38. }
  39. //页面改变
  40. functionrender1(){
  41. returnh('div',{
  42. style:useStyleStr({
  43. 'color':'#ccc',
  44. 'font-size':'20px'
  45. })
  46. },[
  47. h('div',{},[h('span',{
  48. onClick:()=>{
  49. alert('1');
  50. }
  51. },'文本改变了')
  52. ]),
  53. ])
  54. }
  55. //首次加载
  56. mountNode(render,'#app');
  57. //状态改变
  58. setTimeout(()=>{
  59. mountNode(render1,'#app');
  60. },3000)
  61. </script>
  62. </body>
  63. </html>

我们在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. //Mountnode
  2. functionmountNode(render,selector){
  3. }

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

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

  1. functionh(tag,props,children){
  2. return{tag,props,children};
  3. }

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

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

  1. //Mountnode
  2. functionmountNode(render,selector){
  3. mount(render(),document.querySelector(selector))
  4. }

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

  1. functionmount(vnode,container){
  2. constel=document.createElement(vnode.tag);
  3. vnode.el=el;
  4. //props
  5. if(vnode.props){
  6. for(constkeyinvnode.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(typeofvnode.children==="string"){
  18. el.textContent=vnode.children;
  19. }else{
  20. vnode.children.forEach(child=>{
  21. mount(child,el);
  22. });
  23. }
  24. }
  25. container.appendChild(el);
  26. }

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

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

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

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

页面正常显示。

virtual DOM(virtual dom有什么好处)

第二步

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

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

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

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

  1. functionpatch(n1,n2){
  2. }

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

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

  1. letisMounted=false;
  2. letoldTree;
  3. //Mountnode
  4. functionmountNode(render,selector){
  5. if(!isMounted){
  6. mount(oldTree=render(),document.querySelector(selector));
  7. isMounted=true;
  8. }else{
  9. constnewTree=render();
  10. patch(oldTree,newTree);
  11. oldTree=newTree;
  12. }
  13. }

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

  1. functionpatch(n1,n2){
  2. //Implementthis
  3. //1.checkifn1andn2areofthesametype
  4. if(n1.tag!==n2.tag){
  5. //2.ifnot,replace
  6. constparent=n1.el.parentNode;
  7. constanchor=n1.el.nextSibling;
  8. parent.removeChild(n1.el);
  9. mount(n2,parent,anchor);
  10. return
  11. }
  12. constel=n2.el=n1.el;
  13. //3.ifyes
  14. //3.1diffprops
  15. constoldProps=n1.props||{};
  16. constnewProps=n2.props||{};
  17. for(constkeyinnewProps){
  18. constnewValue=newProps[key];
  19. constoldValue=oldProps[key];
  20. if(newValue!==oldValue){
  21. if(newValue!=null){
  22. el.setAttribute(key,newValue);
  23. }else{
  24. el.removeAttribute(key);
  25. }
  26. }
  27. }
  28. for(constkeyinoldProps){
  29. if(!(keyinnewProps)){
  30. el.removeAttribute(key);
  31. }
  32. }
  33. //3.2diffchildren
  34. constoc=n1.children;
  35. constnc=n2.children;
  36. if(typeofnc==='string'){
  37. if(nc!==oc){
  38. el.textContent=nc;
  39. }
  40. }elseif(Array.isArray(nc)){
  41. if(Array.isArray(oc)){
  42. //arraydiff
  43. constcommonLength=Math.min(oc.length,nc.length);
  44. for(leti=0;i<commonLength;i++){
  45. patch(oc[i],nc[i]);
  46. }
  47. if(nc.length>oc.length){
  48. nc.slice(oc.length).forEach(c=>mount(c,el));
  49. }elseif(oc.length>nc.length){
  50. oc.slice(nc.length).forEach(c=>{
  51. el.removeChild(c.el);
  52. })
  53. }
  54. }else{
  55. el.innerHTML='';
  56. nc.forEach(c=>mount(c,el));
  57. }
  58. }
  59. }

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

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

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

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

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

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

  1. functionmount(vnode,container,anchor){
  2. constel=document.createElement(vnode.tag);
  3. vnode.el=el;
  4. //props
  5. if(vnode.props){
  6. for(constkeyinvnode.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(typeofvnode.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. }
  30. }

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

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

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

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

  1. elseif(Array.isArray(nc)){
  2. if(Array.isArray(oc)){
  3. //arraydiff
  4. constcommonLength=Math.min(oc.length,nc.length);
  5. for(leti=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. }elseif(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库就这样完成了。

页面如下所示。

virtual DOM(virtual dom有什么好处)

源码

index.html

  1. <!DOCTYPEhtml>
  2. <htmllang="en">
  3. <head>
  4. <metacharset="UTF-8">
  5. <metahttp-equiv="X-UA-Compatible"content="IE=edge">
  6. <metaname="viewport"content="width=device-width,initial-scale=1.0">
  7. <title>vdom</title>
  8. <style>
  9. .main{
  10. color:#00008b;
  11. }
  12. .main1{
  13. font-weight:bold;
  14. }
  15. </style>
  16. </head>
  17. <body>
  18. <divid="app"></div>
  19. <scriptsrc="./vdom.js"></script>
  20. <script>
  21. functionrender(){
  22. returnh('div',{
  23. style:useObjStr({
  24. 'color':'#ccc',
  25. 'font-size':'20px'
  26. })
  27. },[
  28. h('div',{},[h('span',{
  29. onClick:()=>{
  30. alert('1');
  31. }
  32. },'文本'),h('a',{
  33. href:'https://www.baidu.com',
  34. class:'mainmain1'
  35. },'点击')
  36. ]),
  37. ])
  38. }
  39. //页面改变
  40. functionrender1(){
  41. returnh('div',{
  42. style:useStyleStr({
  43. 'color':'#ccc',
  44. 'font-size':'20px'
  45. })
  46. },[
  47. h('div',{},[h('span',{
  48. onClick:()=>{
  49. alert('1');
  50. }
  51. },'文本改变了')
  52. ]),
  53. ])
  54. }
  55. //首次加载
  56. mountNode(render,'#app');
  57. //状态改变
  58. setTimeout(()=>{
  59. mountNode(render1,'#app');
  60. },3000)
  61. </script>
  62. </body>
  63. </html>

vdom.js

  1. //vdom---
  2. functionh(tag,props,children){
  3. return{tag,props,children};
  4. }
  5. functionmount(vnode,container,anchor){
  6. constel=document.createElement(vnode.tag);
  7. vnode.el=el;
  8. //props
  9. if(vnode.props){
  10. for(constkeyinvnode.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(typeofvnode.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. //processingstrings
  36. functionuseStyleStr(obj){
  37. constreg=/^{|}/g;
  38. constreg1=newRegExp('"',"g");
  39. conststr=JSON.stringify(obj);
  40. constustr=str.replace(reg,'').replace(',',';').replace(reg1,'');
  41. returnustr;
  42. }
  43. functionpatch(n1,n2){
  44. //Implementthis
  45. //1.checkifn1andn2areofthesametype
  46. if(n1.tag!==n2.tag){
  47. //2.ifnot,replace
  48. constparent=n1.el.parentNode;
  49. constanchor=n1.el.nextSibling;
  50. parent.removeChild(n1.el);
  51. mount(n2,parent,anchor);
  52. return
  53. }
  54. constel=n2.el=n1.el;
  55. //3.ifyes
  56. //3.1diffprops
  57. constoldProps=n1.props||{};
  58. constnewProps=n2.props||{};
  59. for(constkeyinnewProps){
  60. constnewValue=newProps[key];
  61. constoldValue=oldProps[key];
  62. if(newValue!==oldValue){
  63. if(newValue!=null){
  64. el.setAttribute(key,newValue);
  65. }else{
  66. el.removeAttribute(key);
  67. }
  68. }
  69. }
  70. for(constkeyinoldProps){
  71. if(!(keyinnewProps)){
  72. el.removeAttribute(key);
  73. }
  74. }
  75. //3.2diffchildren
  76. constoc=n1.children;
  77. constnc=n2.children;
  78. if(typeofnc==='string'){
  79. if(nc!==oc){
  80. el.textContent=nc;
  81. }
  82. }elseif(Array.isArray(nc)){
  83. if(Array.isArray(oc)){
  84. //arraydiff
  85. constcommonLength=Math.min(oc.length,nc.length);
  86. for(leti=0;i<commonLength;i++){
  87. patch(oc[i],nc[i]);
  88. }
  89. if(nc.length>oc.length){
  90. nc.slice(oc.length).forEach(c=>mount(c,el));
  91. }elseif(oc.length>nc.length){
  92. oc.slice(nc.length).forEach(c=>{
  93. el.removeChild(c.el);
  94. })
  95. }
  96. }else{
  97. el.innerHTML='';
  98. nc.forEach(c=>mount(c,el));
  99. }
  100. }
  101. }
  102. letisMounted=false;
  103. letoldTree;
  104. //Mountnode
  105. functionmountNode(render,selector){
  106. if(!isMounted){
  107. mount(oldTree=render(),document.querySelector(selector));
  108. isMounted=true;
  109. }else{
  110. constnewTree=render();
  111. patch(oldTree,newTree);
  112. oldTree=newTree;
  113. }
  114. }

原文链接:https://mp.weixin.qq.com/s/590sJLkpQ76KB1tuN_5Bhw

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。