<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 35331字,需瀏覽 71分鐘

           ·

          2021-06-22 00:06

          為什么使用Virtual DOM

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

          • 為了簡化DOM的復(fù)雜操作于是出現(xiàn)了各種MVVM框架,MVVM框架解決了視圖和狀態(tài)的同步問題

          • 為了簡化視圖的操作我們可以使用模板引擎,但是模板引擎沒有解決跟蹤狀態(tài)變化的問題,于是Virtual DOM出現(xiàn)了

          • Virtual DOM的好處是當(dāng)狀態(tài)改變時不需要立即更新DOM,只需要創(chuàng)建一個虛擬樹來描述DOM,Virtual DOM內(nèi)部將弄清楚如何有效的更新DOM(利用Diff算法實現(xiàn))。

          Virtual DOM的特性

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

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

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

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

          <!DOCTYPE html>
          <html lang="en">

          <head>
              <meta charset="UTF-8">
              <meta http-equiv="X-UA-Compatible" content="IE=edge">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>vdom</title>
              <style>
                  .main {
                      color#00008b;
                  }
                  .main1{
                      font-weight: bold;
                  }
              
          </style>
          </head>

          <body>
              <div id="app"></div>
              <script src="./vdom.js"></script>
              <script>
                  function render({
                      return h('div', {
                          style: useObjStr({
                              'color''#ccc',
                              'font-size''20px'
                          })
                      }, [
                          h('div', {}, [h('span', {
                              onClick() => {
                                  alert('1');
                              }
                          }, '文本'), h('a', {
                              href'https://www.baidu.com',
                              class'main main1'
                          }, '點擊')
                          ]),
                      ])
                  }
                  
                  // 頁面改變
                  function render1({
                      return h('div', {
                          style: useStyleStr({
                              'color''#ccc',
                              'font-size''20px'
                          })
                      }, [
                          h('div', {}, [h('span', {
                              onClick() => {
                                  alert('1');
                              }
                          }, '文本改變了')
                          ]),
                      ])
                  }

                  // 首次加載
                  mountNode(render, '#app');

                  // 狀態(tài)改變
                  setTimeout(()=>{
                      mountNode(render1, '#app');
                  },3000)
              
          </script>
          </body>

          </html>

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

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

          第一步

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

          // Mount node
          function mountNode(render, selector{

          }

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

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

           function h(tag, props, children{
              return { tag, props, children };
          }

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

          我們需要這樣操作。我們在mountNode方法內(nèi)封裝一個mount方法,將傳給mountNode方法的參數(shù)經(jīng)過處理傳給mount方法。

          // Mount node
          function mountNode(render, selector) {
            mount(render(), document.querySelector(selector))
          }

          接著,我們定義一個mount方法。

          function mount(vnode, container{
              const el = document.createElement(vnode.tag);
              vnode.el = el;
              // props
              if (vnode.props) {
                  for (const key in vnode.props) {
                      if (key.startsWith('on')) {
                          el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{
                              passive:true
                          })
                      } else {
                          el.setAttribute(key, vnode.props[key]);
                      }
                  }
              }
              if (vnode.children) {
                  if (typeof vnode.children === "string") {
                      el.textContent = vnode.children;
                  } else {
                      vnode.children.forEach(child => {
                          mount(child, el);
                      });
                  }
              }
              
              container.appendChild(el);
          }

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

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

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

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

          頁面正常顯示。

          第二步

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

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

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

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

          function patch(n1, n2{
             
          }

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

          相比之前,我們加上了對是否掛載節(jié)點進行了判斷。如果沒有掛載的話,就直接調(diào)用mount方法掛載節(jié)點。否則,調(diào)用patch方法進行差異更新。

          let isMounted = false;
          let oldTree;

          // Mount node
          function mountNode(render, selector{
              if (!isMounted) {
                  mount(oldTree = render(), document.querySelector(selector));
                  isMounted = true;
              } else {
                  const newTree = render();
                  patch(oldTree, newTree);
                  oldTree = newTree;
              }

          }

          那么下面我們將主動看下patch方法,這也是在這個庫中最復(fù)雜的方法。

          function patch(n1, n2{
              // Implement this
              // 1. check if n1 and n2 are of the same type
              if (n1.tag !== n2.tag) {
                  // 2. if not, replace
                  const parent = n1.el.parentNode;
                  const anchor = n1.el.nextSibling;
                  parent.removeChild(n1.el);
                  mount(n2, parent, anchor);
                  return
              }

              const el = n2.el = n1.el;

              // 3. if yes
              // 3.1 diff props
              const oldProps = n1.props || {};
              const newProps = n2.props || {};
              for (const key in newProps) {
                  const newValue = newProps[key];
                  const oldValue = oldProps[key];
                  if (newValue !== oldValue) {
                      if (newValue != null) {
                          el.setAttribute(key, newValue);
                      } else {
                          el.removeAttribute(key);
                      }
                  }
              }
              for (const key in oldProps) {
                  if (!(key in newProps)) {
                      el.removeAttribute(key);
                  }
              }
              // 3.2 diff children
              const oc = n1.children;
              const nc = n2.children;
              if (typeof nc === 'string') {
                  if (nc !== oc) {
                      el.textContent = nc;
                  }
              } else if (Array.isArray(nc)) {
                  if (Array.isArray(oc)) {
                      // array diff
                      const commonLength = Math.min(oc.length, nc.length);
                      for (let i = 0; i < commonLength; i++) {
                          patch(oc[i], nc[i]);
                      }
                      if (nc.length > oc.length) {
                          nc.slice(oc.length).forEach(c => mount(c, el));
                      } else if (oc.length > nc.length) {
                          oc.slice(nc.length).forEach(c => {
                              el.removeChild(c.el);
                          })
                      }
                  } else {
                      el.innerHTML = '';
                      nc.forEach(c => mount(c, el));
                  }
              }
          }

          我們從patch方法入?yún)㈤_始,兩個參數(shù)分別是在mountNode方法中傳進來的舊節(jié)點oldTree和新節(jié)點newTree,首先我們進行對新舊節(jié)點的標簽進行對比。

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

            if (n1.tag !== n2.tag) {
                  // 2. if not, replace
                  const parent = n1.el.parentNode;
                  const anchor = n1.el.nextSibling;
                  parent.removeChild(n1.el);
                  mount(n2, parent, anchor);
                  return
              }

          所以,我們重新修改下mount方法。我們看到我們只是加上了對anchor參數(shù)是否為空的判斷。

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

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

          function mount(vnode, container, anchor{
              const el = document.createElement(vnode.tag);
              vnode.el = el;
              // props
              if (vnode.props) {
                  for (const key in vnode.props) {
                      if (key.startsWith('on')) {
                          el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{
                              passive:true
                          })
                      } else {
                          el.setAttribute(key, vnode.props[key]);
                      }
                  }
              }
              if (vnode.children) {
                  if (typeof vnode.children === "string") {
                      el.textContent = vnode.children;
                  } else {
                      vnode.children.forEach(child => {
                          mount(child, el);
                      });
                  }
              }
              if (anchor) {
                  container.insertBefore(el, anchor);
              } else {
                  container.appendChild(el);
              }
          }

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

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

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

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

          else if (Array.isArray(nc)) {
                  if (Array.isArray(oc)) {
                      // array diff
                      const commonLength = Math.min(oc.length, nc.length);
                      for (let i = 0; i < commonLength; i++) {
                          patch(oc[i], nc[i]);
                      }
                      if (nc.length > oc.length) {
                          nc.slice(oc.length).forEach(c => mount(c, el));
                      } else if (oc.length > nc.length) {
                          oc.slice(nc.length).forEach(c => {
                              el.removeChild(c.el);
                          })
                      }
                  } else {
                      el.innerHTML = '';
                      nc.forEach(c => mount(c, el));
                  }
              }

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

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

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

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

          頁面如下所示。


          源碼

          index.html

          <!DOCTYPE html>
          <html lang="en">

          <head>
              <meta charset="UTF-8">
              <meta http-equiv="X-UA-Compatible" content="IE=edge">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>vdom</title>
              <style>
                  .main {
                      color#00008b;
                  }
                  .main1{
                      font-weight: bold;
                  }
              
          </style>
          </head>

          <body>
              <div id="app"></div>
              <script src="./vdom.js"></script>
              <script>
                  function render({
                      return h('div', {
                          style: useObjStr({
                              'color''#ccc',
                              'font-size''20px'
                          })
                      }, [
                          h('div', {}, [h('span', {
                              onClick() => {
                                  alert('1');
                              }
                          }, '文本'), h('a', {
                              href'https://www.baidu.com',
                              class'main main1'
                          }, '點擊')
                          ]),
                      ])
                  }
                  
                  // 頁面改變
                  function render1({
                      return h('div', {
                          style: useStyleStr({
                              'color''#ccc',
                              'font-size''20px'
                          })
                      }, [
                          h('div', {}, [h('span', {
                              onClick() => {
                                  alert('1');
                              }
                          }, '文本改變了')
                          ]),
                      ])
                  }

                  // 首次加載
                  mountNode(render, '#app');

                  // 狀態(tài)改變
                  setTimeout(()=>{
                      mountNode(render1, '#app');
                  },3000)
              
          </script>
          </body>

          </html>

          vdom.js

           // vdom ---
           function h(tag, props, children{
              return { tag, props, children };
          }

          function mount(vnode, container, anchor{
              const el = document.createElement(vnode.tag);
              vnode.el = el;
              // props
              if (vnode.props) {
                  for (const key in vnode.props) {
                      if (key.startsWith('on')) {
                          el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{
                              passive:true
                          })
                      } else {
                          el.setAttribute(key, vnode.props[key]);
                      }
                  }
              }
              if (vnode.children) {
                  if (typeof vnode.children === "string") {
                      el.textContent = vnode.children;
                  } else {
                      vnode.children.forEach(child => {
                          mount(child, el);
                      });
                  }
              }
              if (anchor) {
                  container.insertBefore(el, anchor);
              } else {
                  container.appendChild(el);
              }
          }

          // processing strings
          function useStyleStr(obj{
              const reg = /^{|}/g;
              const reg1 = new RegExp('"',"g");
              const str = JSON.stringify(obj);
              const ustr = str.replace(reg, '').replace(','';').replace(reg1,'');
              return ustr;
          }

          function patch(n1, n2{
              // Implement this
              // 1. check if n1 and n2 are of the same type
              if (n1.tag !== n2.tag) {
                  // 2. if not, replace
                  const parent = n1.el.parentNode;
                  const anchor = n1.el.nextSibling;
                  parent.removeChild(n1.el);
                  mount(n2, parent, anchor);
                  return
              }

              const el = n2.el = n1.el;

              // 3. if yes
              // 3.1 diff props
              const oldProps = n1.props || {};
              const newProps = n2.props || {};
              for (const key in newProps) {
                  const newValue = newProps[key];
                  const oldValue = oldProps[key];
                  if (newValue !== oldValue) {
                      if (newValue != null) {
                          el.setAttribute(key, newValue);
                      } else {
                          el.removeAttribute(key);
                      }
                  }
              }
              for (const key in oldProps) {
                  if (!(key in newProps)) {
                      el.removeAttribute(key);
                  }
              }
              // 3.2 diff children
              const oc = n1.children;
              const nc = n2.children;
              if (typeof nc === 'string') {
                  if (nc !== oc) {
                      el.textContent = nc;
                  }
              } else if (Array.isArray(nc)) {
                  if (Array.isArray(oc)) {
                      // array diff
                      const commonLength = Math.min(oc.length, nc.length);
                      for (let i = 0; i < commonLength; i++) {
                          patch(oc[i], nc[i]);
                      }
                      if (nc.length > oc.length) {
                          nc.slice(oc.length).forEach(c => mount(c, el));
                      } else if (oc.length > nc.length) {
                          oc.slice(nc.length).forEach(c => {
                              el.removeChild(c.el);
                          })
                      }
                  } else {
                      el.innerHTML = '';
                      nc.forEach(c => mount(c, el));
                  }
              }
          }

          let isMounted = false;
          let oldTree;

          // Mount node
          function mountNode(render, selector{
              if (!isMounted) {
                  mount(oldTree = render(), document.querySelector(selector));
                  isMounted = true;
              } else {
                  const newTree = render();
                  patch(oldTree, newTree);
                  oldTree = newTree;
              }

          }

          關(guān)于作者

          作者:Vam的金豆之路。曾獲得2019年CSDN年度博客之星,CSDN博客訪問量已達到數(shù)百萬。掘金博客文章多次推送到首頁,總訪問量已達到數(shù)十萬。

          另外,我的公眾號:前端歷劫之路,公眾號持續(xù)更新最新前端技術(shù)及相關(guān)技術(shù)文章。歡迎關(guān)注我的公眾號,讓我們一起在前端道路上歷劫吧!Go!


          ??歡迎關(guān)注,關(guān)注后我拉你進前端學(xué)習(xí)大佬群,每天都能成長一點點~
          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本韩国一区二区三区在线观看 | 亚洲AV无码成人精品涩涩麻豆 | 成人中文字幕在线视频 | 美女被干操的视频网站 | 性爱国产一区 |