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

          如何寫成Strview.js之源碼剖析

          共 23430字,需瀏覽 47分鐘

           ·

          2021-09-05 17:30

          前言

          前段時間我自己開發(fā)了一款Strview.js,它是一個可以將字符串轉(zhuǎn)換為視圖的JS庫。什么意思呢?就像下面這段代碼:

          <!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>Strview.js</title>
          </head>

          <body>
              <div id="app"></div>
              <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/strview.global.js"></script>
              <script>
                  Strview.createView({
                      el"#app",
                      data: {
                          msg'Hello World'
                      },
                      template`<p>{msg}</p>`,
                  });
              
          </script>
          </body>

          </html>

          顯示如下頁面:


          你會看到頁面上顯示了一個Hello World字樣,而我們看到HTML代碼中除了一個ID名是app標(biāo)簽之外,其他標(biāo)簽并沒有,更沒有Hello World文本。這時,繼續(xù)往下看,在JS代碼中,我們引入了Strview.js,并且我們調(diào)用了它一個createView方法,最后傳入了一個對象。我們在對象中發(fā)現(xiàn)了Hello World字符串,并且我們在template屬性中看到它多所對應(yīng)的值是一個標(biāo)簽,就是這個標(biāo)簽<p>{msg}</p>,另外,里面我們會看到使用{}包裹的msg字符。與data對象中的msg屬性相對應(yīng),正好它的值為Hello World。我們現(xiàn)在改變下msg屬性對應(yīng)的值來看下頁面是否發(fā)生改變。

          此圖為動圖


          果然,發(fā)生了改變,所以我們知道Strview.js就是這么將字符串轉(zhuǎn)換為視圖的。

          這里,我們只是簡單介紹了Strview.js的簡單用法,如果想繼續(xù)了解其他用法的話,可以去Strview.js中文官網(wǎng):

          https://www.maomin.club/site/strviewjs/zh



          下面的內(nèi)容呢,我們將看下Strview.js源碼,看它是如何實現(xiàn)的。

          剖析源碼

          本篇分析Strview.js版本為1.9.0

          首先,我們獲取到源碼,這里我們使用生產(chǎn)環(huán)境下的Strview.js,也就是上面實例中的這個地址:

          https://cdn.jsdelivr.net/npm/[email protected]/dist/strview.global.js

          我們,先大體看下源碼,加上空行,源碼一共125行。不壓縮的話,僅僅4kb。

          var Strview = (function (exports{
              'use strict';

              // global object
              const globalObj = {
                  _nHtml: [],
                  _oHtml: [],
                  _elnull,
                  _datanull,
                  _templatenull,
                  _sourceTemplatenull
              };

              // initialization
              function createView(v{
                  globalObj._data = v.data;
                  globalObj._template = v.template;
                  globalObj._sourceTemplate = v.template;
                  globalObj._el = v.el;
                  v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!");
              }

              // event listeners
              function eventListener(el, event, cb{
                  document.querySelector(el).addEventListener(event, cb);
              }

              // processing simple values
              function ref({
                  return new Proxy(globalObj._data, {
                      get(target, key) => {
                          return target[key]
                      },
                      set(target, key, newValue) => {
                          target[key] = newValue;
                          setTemplate();
                          return true;
                      }
                  })
              }

              // reactiveHandlers
              const reactiveHandlers = {
                  get(target, key) => {
                      if (typeof target[key] === 'object' && target[key] !== null) {
                          return new Proxy(target[key], reactiveHandlers);
                      }
                      return Reflect.get(target, key);
                  },
                  set(target, key, value) => {
                      Reflect.set(target, key, value);
                      setTemplate();
                      return true
                  }
              };

              // respond to complex objects
              function reactive({
                  return new Proxy(globalObj._data, reactiveHandlers)
              }

              // update the view
              function setTemplate({
                  const oNode = document.querySelector(globalObj._el);
                  const nNode = toHtml(render(globalObj._sourceTemplate));
                  compile(oNode, 'o');
                  compile(nNode, 'n');
                  if (globalObj._oHtml.length === globalObj._nHtml.length) {
                      for (let index = 0; index < globalObj._oHtml.length; index++) {
                          const element = globalObj._oHtml[index];
                          element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent);
                      }
                  }
              }

              // judge text node
              function isTextNode(node{
                  return node.nodeType === 3;
              }

              // compile DOM
              function compile(node, type{
                  const childNodesArr = node.childNodes;
                  for (let index = 0; index < Array.from(childNodesArr).length; index++) {
                      const item = Array.from(childNodesArr)[index];
                      if (item.childNodes && item.childNodes.length) {
                          compile(item, type);
                      } else if (isTextNode(item) && item.textContent.trim().length !== 0) {
                          type === 'o' ? globalObj._oHtml.push(item) : globalObj._nHtml.push(item);
                      }
                  }
              }

              // string to DOM
              function toHtml(domStr{
                  const parser = new DOMParser();
                  return parser.parseFromString(domStr, "text/html");
              }

              // template engine
              function render(template{
                  const reg = /\{(.+?)\}/;
                  if (reg.test(template)) {
                      const key = reg.exec(template)[1];
                      if (globalObj._data.hasOwnProperty(key)) {
                          template = template.replace(reg, globalObj._data[key]);
                      } else {
                          template = template.replace(reg, eval(`globalObj._data.${key}`));
                      }
                      return render(template)
                  }

                  return template;
              }

              // exports
              exports.createView = createView;
              exports.eventListener = eventListener;
              exports.reactive = reactive;
              exports.ref = ref;

              Object.defineProperty(exports, '__esModule', { valuetrue });

              return exports;
          }({}));

          首先,我們會看到最外層定義了一個Strview變量,暴露在外面,并將一個立即執(zhí)行函數(shù)(IIFE)賦予這個變量。

          我們先來看下這個立即執(zhí)行函數(shù)。

          var Strview = (function (exports{
          // ...

          }({}));

          函數(shù)中需要傳一個形參exports,并且又立即傳入一個空對象。

          然后,我們來看下函數(shù)內(nèi)的內(nèi)容。

          我們會看到函數(shù)中有很多變量與函數(shù)方法,那么我們就按功能來分析。

          首先,我們看到了一個全局對象,全局對象中分別定義了幾個屬性。這樣做是為了減少全局變量污染,JS可以隨意定義保存所有應(yīng)用資源的全局變量,但全局變量可以削弱程序靈活性,增大了模塊之間的耦合性。最小化使用全局變量的一個方法是在你的應(yīng)用中只創(chuàng)建唯一一個全局變量。

          // global object
          const globalObj = {
              _nHtml: [], // 存放新DOM數(shù)組
              _oHtml: [], // 存放舊DOM數(shù)組
              _elnull// 掛載DOM節(jié)點
              _datanull// 存放數(shù)據(jù)
              _templatenull// 模板字符串
              _sourceTemplatenull // 源模板字符串
          };

          然后,我們接著看初始化階段,這個階段是將模板字符串轉(zhuǎn)換成視圖。

          // initialization
          function createView(v{
              globalObj._data = v.data;
              globalObj._template = v.template;
              globalObj._sourceTemplate = v.template;
              globalObj._el = v.el;
              v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!");
          }

          我們看到這個createView方法傳入了一個參數(shù),也就是我們之前傳入的那個對象:

          Strview.createView({
                  el"#app",
                  data: {
                      msg'Hello World'
                  },
                  template`<p>{msg}</p>`,
              });

          我們看到傳入的對象中的屬性分別賦給全局對象globalObj。在最后一行中通過判斷v.el是否是真值,如果是就執(zhí)行這行代碼:

          document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) 

          這行代碼執(zhí)行了insertAdjacentHTML()方法,這個方法在MDN上是這樣解釋它的。

          insertAdjacentHTML() 方法將指定的文本解析為 Element 元素,并將結(jié)果節(jié)點插入到DOM樹中的指定位置。它不會重新解析它正在使用的元素,因此它不會破壞元素內(nèi)的現(xiàn)有元素。這避免了額外的序列化步驟,使其比直接使用innerHTML操作更快。

          insertAdjacentHTML()方法傳入的第二個參數(shù)是是要被解析為HTML或XML元素,并插入到DOM樹中的DOMString,render(globalObj._template)這個方法就是返回的DOMString

          如果是假,就執(zhí)行console.error("Error: Please set el property!"),在瀏覽器上輸出錯誤。

          既然這個用到了render(globalObj._template)這個方法,那么我們下面來看下。

          // template engine
          function render(template{
              const reg = /\{(.+?)\}/;
              if (reg.test(template)) {
                  const key = reg.exec(template)[1];
                  if (globalObj._data.hasOwnProperty(key)) {
                      template = template.replace(reg, globalObj._data[key]);
                  } else {
                      template = template.replace(reg, eval(`globalObj._data.${key}`));
                  }
                  return render(template)
              }

              return template;
          }

          首先,這個render(template)方法傳入了一個參數(shù),第一個參數(shù)是模板字符串。

          然后,我們進入這個方法中看一下,首先,我們定義了正則/\{(.+?)\}/,用于匹配模板字符串中的{}中的內(nèi)容。如果匹配為真,就進入這個邏輯:

          const key = reg.exec(template)[1];
          if (globalObj._data.hasOwnProperty(key)) {
              template = template.replace(reg, globalObj._data[key]);
          else {
              template = template.replace(reg, eval(`globalObj._data.${key}`));
          }
          return render(template)

          我們在第一行代碼中看到了這行代碼const key = reg.exec(template)[1],這里使用的是reg.exec()方法,MDN這樣解釋它:

          exec() 方法在一個指定字符串中執(zhí)行一個搜索匹配。返回一個結(jié)果數(shù)組或 null。在設(shè)置了 global 或 sticky 標(biāo)志位的情況下(如 /foo/g or /foo/y),JavaScript RegExp 對象是有狀態(tài)的。他們會將上次成功匹配后的位置記錄在 lastIndex 屬性中。使用此特性,exec() 可用來對單個字符串中的多次匹配結(jié)果進行逐條的遍歷(包括捕獲到的匹配),而相比之下, String.prototype.match() 只會返回匹配到的結(jié)果。

          所以,通過這個方法我們?nèi)〉搅四0遄址械?code style="margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;background: rgb(248, 245, 236);color: rgb(255, 53, 2);line-height: 1.5;font-size: 90%;padding: 3px 5px;border-radius: 2px;">{}中的內(nèi)容,它一般是我們存取數(shù)據(jù)_data中的屬性。首先,我們判斷globalObj._data對象中是否有這個key,如果有我們就使用字符串替換方法replace來把對應(yīng)的占位符key替換成所對應(yīng)的值。下面接著進行遞歸,直到reg.test(template)返回為false。最終,render()方法返回處理后的template。

          看完render()方法,我們來看下事件處理階段,也就是eventListener()方法。

          // event listeners
          function eventListener(el, event, cb{
              document.querySelector(el).addEventListener(event, cb);
          }

          這個方法很簡單,第一個參數(shù)傳入DOM選擇器,第二個參數(shù)傳入一個事件名,第三個參數(shù)傳入一個回調(diào)函數(shù)。

          最后,我們來看下Strview.js的數(shù)據(jù)響應(yīng)系統(tǒng)。

          // processing simple values
          function ref({
              return new Proxy(globalObj._data, {
                  get(target, key) => {
                      return target[key]
                  },
                  set(target, key, newValue) => {
                      target[key] = newValue;
                      setTemplate();
                      return true;
                  }
              })
          }

          // reactiveHandlers
          const reactiveHandlers = {
              get(target, key) => {
                  if (typeof target[key] === 'object' && target[key] !== null) {
                      return new Proxy(target[key], reactiveHandlers);
                  }
                  return Reflect.get(target, key);
              },
              set(target, key, value) => {
                  Reflect.set(target, key, value);
                  setTemplate();
                  return true
              }
          };

          // respond to complex objects
          function reactive({
              return new Proxy(globalObj._data, reactiveHandlers)
          }

          上面這些代碼主要是reactive()ref()這兩個方法的實現(xiàn)。reactive()方法是針對復(fù)雜數(shù)據(jù)的處理,比如嵌套對象以及數(shù)組。ref()方法主要是針對簡單數(shù)據(jù)的處理,像是原始值與單一非嵌套對象。

          它們兩個都是基于Proxy代理來實現(xiàn)數(shù)據(jù)的攔截與響應(yīng),MDN中這樣定義它。

          Proxy 對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。

          它們兩個Proxy對象第一個參數(shù)都是我們在初始化定義的globalObj._data,第二個參數(shù)是一個通常以函數(shù)作為屬性的對象。這里都定義了get()方法、set()方法,get()是屬性讀取操作的捕捉器,set()是屬性設(shè)置操作的捕捉器。

          reactive()ref()這兩個方法實現(xiàn)不一樣的地方是reactive()方法加上了對嵌套對象判斷來實現(xiàn)遞歸。

          我們在set()方法中看到它們都調(diào)用了setTemplate()方法,下面,我們來看下這個方法。

          // update the view
          function setTemplate({
              const oNode = document.querySelector(globalObj._el);
              const nNode = toHtml(render(globalObj._sourceTemplate));
              compile(oNode, 'o');
              compile(nNode, 'n');
              if (globalObj._oHtml.length === globalObj._nHtml.length) {
                  for (let index = 0; index < globalObj._oHtml.length; index++) {
                      const element = globalObj._oHtml[index];
                      element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent);
                  }
              }
          }

          首先,我們?nèi)〉匠跏蓟瘯r掛載的DOM節(jié)點,接著我們使用toHtml()方法將render(globalObj._sourceTemplate)方法作為第一個參數(shù)傳入。

          我們先來看下toHtml()方法,這里的第一個參數(shù)domStr,也就是render(globalObj._sourceTemplate)。

          // string to DOM
          function toHtml(domStr{
              const parser = new DOMParser();
              return parser.parseFromString(domStr, "text/html");
          }

          toHtml()方法第一行我們實例化了一個DOMParser對象。一旦建立了一個解析對象以后,你就可以使用它的parseFromString方法來解析一個html字符串。

          然后,我們回到setTemplate()方法中,變量nNode被賦值了toHtml(render(globalObj._sourceTemplate)),這里是被處理成一個DOM對象。

          接著,執(zhí)行compile()方法。

          compile(oNode, 'o');
          compile(nNode, 'n');

          我們來看下這個compile()方法。

          // compile DOM
          function compile(node, type{
              const childNodesArr = node.childNodes;
              for (let index = 0; index < Array.from(childNodesArr).length; index++) {
                  const item = Array.from(childNodesArr)[index];
                  if (item.childNodes && item.childNodes.length) {
                      compile(item, type);
                  } else if (isTextNode(item) && item.textContent.trim().length !== 0) {
                      type === 'o' ? globalObj._oHtml.push(item) : globalObj._nHtml.push(item);
                  }
              }
          }

          這個方法是將遍歷DOM元素并把每一項存儲到我們初始化定義的數(shù)組里面,分別是globalObj._oHtmlglobalObj._nHtml,這個方法中用到了isTextNode()方法。

          // judge text node
          function isTextNode(node{
              return node.nodeType === 3;
          }

          這個方法第一個參數(shù)是一個Node節(jié)點,如果它的nodeType屬性等于3就說明這個節(jié)點是文本節(jié)點。

          最后,我們又回到setTemplate()方法中,接著執(zhí)行以下代碼:

          if (globalObj._oHtml.length === globalObj._nHtml.length) {
              for (let index = 0; index < globalObj._oHtml.length; index++) {
                  const element = globalObj._oHtml[index];
                  element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent);
              }
          }

          判斷兩個數(shù)組的長度是否一樣,如果一樣就遍歷globalObj._oHtml,最后判斷globalObj._nHtml[index].textContent是否等于globalObj._oHtml[index].textContent,如果不相等,直接將globalObj._nHtml[index].textContent賦于globalObj._OHtml[index].textContent,完成更新。

          最后,將這幾個定義的方法賦于傳入的exports對象并返回這個對象。

          // exports
          exports.createView = createView;
          exports.eventListener = eventListener;
          exports.reactive = reactive;
          exports.ref = ref;

          Object.defineProperty(exports, '__esModule', { valuetrue });

          return exports;

          這里,有一行代碼Object.defineProperty(exports, '__esModule', { value: true }),這行代碼其實也可以這么寫exports.__esModule = true。表面上看就是把一個導(dǎo)出對象標(biāo)識為一個 ES 模塊。

          隨著 JS 不斷發(fā)展和 Node.js 的出現(xiàn),JS 慢慢有了模塊化方案。在 ES6 之前,最有名的就是 CommonJS / AMD,AMD 就不提了現(xiàn)在基本不用。CommonJS 被 Node.js 采用至今,與 ES 模塊共存。由于 Node.js 早期模塊化方案選擇了 CommonJS,導(dǎo)致現(xiàn)在 NPM 上仍然存在大量的 CommonJS 模塊,JS 圈子一時半會兒是丟不掉 CommonJS 了。

          Webpack 實現(xiàn)了一套 CommonJS 模塊化方案,支持打包 CommonJS 模塊,同時也支持打包 ES 模塊。但是兩種模塊格式混用的時候問題就來了,ES 模塊和 CommonJS 模塊并不完全兼容,CommonJS 的 module.exports 在 ES 模塊中沒有對應(yīng)的表達方式,和默認導(dǎo)出 export default 是不一樣的。

          __esModule則是用來兼容 ES 模塊導(dǎo)入 CommonJS 模塊默認導(dǎo)出方案。

          結(jié)語

          至此,Strview.js的源碼分析完畢。謝謝閱讀~

          開發(fā)版本

          推薦使用StrviewCLI搭建StrviewApp項目腳手架。

          https://github.com/maomincoding/strview-app

          生產(chǎn)版本

          直接引入CDN鏈接,目前版本為1.9.0

          https://cdn.jsdelivr.net/npm/[email protected]/dist/strview.global.js

          關(guān)于作者

          作者:Vam的金豆之路。

          目前專注于研究前端技術(shù),活躍在多個技術(shù)社區(qū)。曾獲得2019年CSDN年度博客之星,CSDN博客訪問量已達到數(shù)百萬。掘金博客文章多次推送到首頁,總訪問量已達到數(shù)十萬。

          另外,我的公眾號:前端歷劫之路,公眾號持續(xù)更新最新前端技術(shù)及相關(guān)技術(shù)文章。

          歡迎關(guān)注,讓我們一起在前端道路上歷劫吧!


          點一下,邀您進去前端技術(shù)群



          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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 | 西西4444WWW无码视频 |