自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

開發(fā) 前端
手動操作DOM比較麻煩。還需要考慮瀏覽器兼容性問題,雖然有JQuery等庫簡化DOM操作,但是隨著項目的復雜DOM操作復雜提升。

[[406455]]

為什么使用Virtual DOM

  • 手動操作DOM比較麻煩。還需要考慮瀏覽器兼容性問題,雖然有JQuery等庫簡化DOM操作,但是隨著項目的復雜DOM操作復雜提升。
  • 為了簡化DOM的復雜操作于是出現(xiàn)了各種MVVM框架,MVVM框架解決了視圖和狀態(tài)的同步問題
  • 為了簡化視圖的操作我們可以使用模板引擎,但是模板引擎沒有解決跟蹤狀態(tài)變化的問題,于是Virtual DOM出現(xiàn)了
  • Virtual DOM的好處是當狀態(tài)改變時不需要立即更新DOM,只需要創(chuàng)建一個虛擬樹來描述DOM,Virtual DOM內部將弄清楚如何有效的更新DOM(利用Diff算法實現(xiàn))。

Virtual DOM的特性

  1. Virtual DOM可以維護程序的狀態(tài),跟蹤上一次的狀態(tài)。
  2. 通過比較前后兩次的狀態(tài)差異更新真實DOM。

實現(xiàn)一個基礎的Virtual DOM庫

我們可以仿照snabbdom庫https://github.com/snabbdom/snabbdom.git自己動手實現(xiàn)一款迷你版Virtual DOM庫。

首先,我們創(chuàng)建一個index.html文件,寫一下我們需要展示的內容,內容如下:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.  
  4. <head> 
  5.     <meta charset="UTF-8"
  6.     <meta http-equiv="X-UA-Compatible" content="IE=edge"
  7.     <meta name="viewport" content="width=device-width, initial-scale=1.0"
  8.     <title>vdom</title> 
  9.     <style> 
  10.         .main { 
  11.             color: #00008b; 
  12.         } 
  13.         .main1{ 
  14.             font-weight: bold; 
  15.         } 
  16.     </style> 
  17. </head> 
  18.  
  19. <body> 
  20.     <div id="app"></div> 
  21.     <script src="./vdom.js"></script> 
  22.     <script> 
  23.         function render() { 
  24.             return h('div', { 
  25.                 style: useObjStr({ 
  26.                     'color''#ccc'
  27.                     'font-size''20px' 
  28.                 }) 
  29.             }, [ 
  30.                 h('div', {}, [h('span', { 
  31.                     onClick: () => { 
  32.                         alert('1'); 
  33.                     } 
  34.                 }, '文本'), h('a', { 
  35.                     href: 'https://www.baidu.com'
  36.                     class: 'main main1' 
  37.                 }, '點擊'
  38.                 ]), 
  39.             ]) 
  40.         } 
  41.          
  42.         // 頁面改變 
  43.         function render1() { 
  44.             return h('div', { 
  45.                 style: useStyleStr({ 
  46.                     'color''#ccc'
  47.                     'font-size''20px' 
  48.                 }) 
  49.             }, [ 
  50.                 h('div', {}, [h('span', { 
  51.                     onClick: () => { 
  52.                         alert('1'); 
  53.                     } 
  54.                 }, '文本改變了'
  55.                 ]), 
  56.             ]) 
  57.         } 
  58.  
  59.         // 首次加載 
  60.         mountNode(render, '#app'); 
  61.  
  62.         // 狀態(tài)改變 
  63.         setTimeout(()=>{ 
  64.             mountNode(render1, '#app'); 
  65.         },3000) 
  66.     </script> 
  67. </body> 
  68.  
  69. </html>

我們在body標簽內創(chuàng)建了一個id是app的DOM元素,用于被掛載節(jié)點。接著我們引入了一個vdom.js文件,這個文件就是我們將要實現(xiàn)的迷你版Virtual DOM庫。最后,我們在script標簽內定義了一個render方法,返回為一個h方法。調用mountNode方法掛載到id是app的DOM元素上。h方法中數(shù)據(jù)結構我們是借鑒snabbdom庫,第一個參數(shù)是標簽名,第二個參數(shù)是屬性,最后一個參數(shù)是子節(jié)點。還有,你可能會注意到在h方法中我們使用了useStyleStr方法,這個方法主要作用是將style樣式轉化成頁面能識別的結構,實現(xiàn)代碼我會在最后給出。

思路理清楚了,展示頁面的代碼也寫完了。下面我們將重點看下vdom.js,如何一步一步地實現(xiàn)它。

第一步

我們看到index.html文件中首先需要調用mountNode方法,所以,我們先在vdom.js文件中定義一個mountNode方法。

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

接著,我們會看到mountNode方法第一個參數(shù)是render方法,render方法返回了h方法,并且看到第一個參數(shù)是標簽,第二個參數(shù)是屬性,第三個參數(shù)是子節(jié)點。

那么,我們接著在vdom.js文件中再定義一個h方法。

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

還沒有結束,我們需要根據(jù)傳入的三個參數(shù)tag、props、children來掛載到頁面上。

我們需要這樣操作。我們在mountNode方法內封裝一個mount方法,將傳給mountNode方法的參數(shù)經(jīng)過處理傳給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); 

第一個參數(shù)是調用傳進來的render方法,它返回的是h方法,而h方返回一個同名參數(shù)的對象{ tag, props, children },那么我們就可以通過vnode.tag、vnode.props、vnode.children取到它們。

我們看到先是判斷屬性,如果屬性字段開頭含有,on標識就是代表事件,那么就從屬性字段第三位截取,利用addEventListenerAPI創(chuàng)建一個監(jiān)聽事件。否則,直接利用setAttributeAPI設置屬性。

接著,再判斷子節(jié)點,如果是字符串,我們直接將字符串賦給文本節(jié)點。否則就是節(jié)點,我們就遞歸調用mount方法。

最后,我們將使用appendChildAPI把節(jié)點內容掛載到真實DOM中。

頁面正常顯示。

第二步

我們知道Virtual DOM有以下兩個特性:

  1. Virtual DOM可以維護程序的狀態(tài),跟蹤上一次的狀態(tài)。
  2. 通過比較前后兩次的狀態(tài)差異更新真實DOM。

這就利用到了我們之前提到的diff算法。

我們首先定義一個patch方法。因為要對比前后狀態(tài)的差異,所以第一個參數(shù)是舊節(jié)點,第二個參數(shù)是新節(jié)點。

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

下面,我們還需要做一件事,那就是完善mountNode方法,為什么這樣操作呢?是因為當狀態(tài)改變時,只更新狀態(tài)改變的DOM,也就是我們所說的差異更新。這時就需要配合patch方法做diff算法。

相比之前,我們加上了對是否掛載節(jié)點進行了判斷。如果沒有掛載的話,就直接調用mount方法掛載節(jié)點。否則,調用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方法入?yún)㈤_始,兩個參數(shù)分別是在mountNode方法中傳進來的舊節(jié)點oldTree和新節(jié)點newTree,首先我們進行對新舊節(jié)點的標簽進行對比。

如果新舊節(jié)點的標簽不相等,就移除舊節(jié)點。另外,利用nextSiblingAPI取指定節(jié)點之后緊跟的節(jié)點(在相同的樹層級中)。然后,傳給mount方法第三個參數(shù)。這時你可能會有疑問,mount方法不是有兩個參數(shù)嗎?對,但是這里我們需要傳進去第三個參數(shù),主要是為了對同級節(jié)點進行處理。

  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參數(shù)是否為空的判斷。

如果anchor參數(shù)不為空,我們使用insertBeforeAPI,在參考節(jié)點之前插入一個擁有指定父節(jié)點的子節(jié)點。insertBeforeAPI第一個參數(shù)是用于插入的節(jié)點,第二個參數(shù)將要插在這個節(jié)點之前,如果這個參數(shù)為 null 則用于插入的節(jié)點將被插入到子節(jié)點的末尾。

如果anchor參數(shù)為空,直接在父節(jié)點下的子節(jié)點列表末尾添加子節(jié)點。

  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方法。如果新舊節(jié)點的標簽相等,我們首先要遍歷新舊節(jié)點的屬性。我們先遍歷新節(jié)點的屬性,判斷新舊節(jié)點的屬性值是否相同,如果不相同,再進行進一步處理。判斷新節(jié)點的屬性值是否為null,否則直接移除屬性。然后,遍歷舊節(jié)點的屬性,如果屬性名不在新節(jié)點屬性表中,則直接移除屬性。

分析完了對新舊節(jié)點屬性的對比,接下來,我們來分析第三個參數(shù)子節(jié)點。

首先,我們分別定義兩個變量oc、nc,分別賦予舊節(jié)點的children屬性和新節(jié)點的children屬性。如果新節(jié)點的children屬性是字符串,并且新舊節(jié)點的內容不相同,那么就直接將新節(jié)點的文本內容賦予即可。

接下來,我們看到利用Array.isArray()方法判斷新節(jié)點的children屬性是否是數(shù)組,如果是數(shù)組的話,就執(zhí)行下面這些代碼。

  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.     } 

我們看到里面又判斷舊節(jié)點的children屬性是否是數(shù)組。

如果是,我們取新舊子節(jié)點數(shù)組的長度兩者的最小值。然后,我們將其循環(huán)遞歸patch方法。為什么取最小值呢?是因為如果取的是他們共有的長度。然后,每次遍歷遞歸時,判斷nc.length和oc.length的大小,循環(huán)執(zhí)行對應的方法。

如果不是,直接將節(jié)點內容清空,重新循環(huán)執(zhí)行mount方法。

這樣,我們搭建的迷你版Virtual DOM庫就這樣完成了。

頁面如下所示。

源碼

index.html

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.  
  4. <head> 
  5.     <meta charset="UTF-8"
  6.     <meta http-equiv="X-UA-Compatible" content="IE=edge"
  7.     <meta name="viewport" content="width=device-width, initial-scale=1.0"
  8.     <title>vdom</title> 
  9.     <style> 
  10.         .main { 
  11.             color: #00008b; 
  12.         } 
  13.         .main1{ 
  14.             font-weight: bold; 
  15.         } 
  16.     </style> 
  17. </head> 
  18.  
  19. <body> 
  20.     <div id="app"></div> 
  21.     <script src="./vdom.js"></script> 
  22.     <script> 
  23.         function render() { 
  24.             return h('div', { 
  25.                 style: useObjStr({ 
  26.                     'color''#ccc'
  27.                     'font-size''20px' 
  28.                 }) 
  29.             }, [ 
  30.                 h('div', {}, [h('span', { 
  31.                     onClick: () => { 
  32.                         alert('1'); 
  33.                     } 
  34.                 }, '文本'), h('a', { 
  35.                     href: 'https://www.baidu.com'
  36.                     class: 'main main1' 
  37.                 }, '點擊'
  38.                 ]), 
  39.             ]) 
  40.         } 
  41.          
  42.         // 頁面改變 
  43.         function render1() { 
  44.             return h('div', { 
  45.                 style: useStyleStr({ 
  46.                     'color''#ccc'
  47.                     'font-size''20px' 
  48.                 }) 
  49.             }, [ 
  50.                 h('div', {}, [h('span', { 
  51.                     onClick: () => { 
  52.                         alert('1'); 
  53.                     } 
  54.                 }, '文本改變了'
  55.                 ]), 
  56.             ]) 
  57.         } 
  58.  
  59.         // 首次加載 
  60.         mountNode(render, '#app'); 
  61.  
  62.         // 狀態(tài)改變 
  63.         setTimeout(()=>{ 
  64.             mountNode(render1, '#app'); 
  65.         },3000) 
  66.     </script> 
  67. </body> 
  68.  
  69. </html> 

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.  

 

責任編輯:姜華 來源: 前端歷劫之路
相關推薦

2022-05-06 19:42:53

DOM

2020-10-12 08:56:47

Virtual dom

2021-05-26 05:22:09

Virtual DOMSnabbdom虛擬DOM

2021-07-04 10:07:04

Virtual DO閱讀源碼虛擬DOM

2021-01-11 07:51:16

DOM對象節(jié)點樹

2021-06-25 06:47:38

VueVue2.x迷你版響應式原理

2021-01-18 07:15:22

虛擬DOM真實DOMJavaScript

2024-10-15 09:48:56

2022-01-26 14:29:04

區(qū)塊鏈加密貨幣技術

2014-05-26 16:16:59

Shadow DomWeb Compone

2011-12-18 12:36:59

摩托

2021-02-02 07:37:39

NextTickvueDOM

2018-06-26 14:29:44

LinuxUnix不同

2023-12-01 15:39:13

Linux操作系統(tǒng)

2021-09-06 10:45:18

XDRMDR

2019-10-14 10:09:33

Wi-Fi 6Wi-Fi無線網(wǎng)絡

2012-07-25 15:45:28

ERPSCM

2024-02-26 07:36:09

lockJava語言

2022-09-14 09:45:15

指標標簽

2010-09-28 11:11:23

XML DOMHTML DOM
點贊
收藏

51CTO技術棧公眾號