服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - 编程技术 - 一篇文章教你如何写成Strview.js之源码剖析

一篇文章教你如何写成Strview.js之源码剖析

2021-09-06 23:08前端历劫之路maomin9761 编程技术

前段时间我自己开发了一款Strview.js,它是一个可以将字符串转换为视图的JS库。在JS代码中,我们引入了Strview.js,并且我们调用了它一个createView方法,最后传入了一个对象。

一篇文章教你如何写成Strview.js之源码剖析

前言

前段时间我自己开发了一款Strview.js,它是一个可以将字符串转换为视图的JS库。什么意思呢?就像下面这段代码:

  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>Strview.js</title> 
  9. </head> 
  10.  
  11. <body> 
  12.     <div id="app"></div> 
  13.     <script src="https://cdn.jsdelivr.net/npm/strview@1.9.0/dist/strview.global.js"></script> 
  14.     <script> 
  15.         Strview.createView({ 
  16.             el: "#app"
  17.             data: { 
  18.                 msg: 'Hello World' 
  19.             }, 
  20.             template: `<p>{msg}</p>`, 
  21.         }); 
  22.     </script> 
  23. </body> 
  24.  
  25. </html> 

显示如下页面:

一篇文章教你如何写成Strview.js之源码剖析

你会看到页面上显示了一个Hello World字样,而我们看到HTML代码中除了一个ID名是app标签之外,其他标签并没有,更没有Hello World文本。这时,继续往下看,在JS代码中,我们引入了Strview.js,并且我们调用了它一个createView方法,最后传入了一个对象。我们在对象中发现了Hello World字符串,并且我们在template属性中看到它多所对应的值是一个标签,就是这个标签<p>{msg}</p>,另外,里面我们会看到使用{}包裹的msg字符。与data对象中的msg属性相对应,正好它的值为Hello World。我们现在改变下msg属性对应的值来看下页面是否发生改变。

一篇文章教你如何写成Strview.js之源码剖析

果然,发生了改变,所以我们知道Strview.js就是这么将字符串转换为视图的。

这里,我们只是简单介绍了Strview.js的简单用法,如果想继续了解其他用法的话,可以去Strview.js中文官网:

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

一篇文章教你如何写成Strview.js之源码剖析

下面的内容呢,我们将看下Strview.js源码,看它是如何实现的。

剖析源码

本篇分析Strview.js版本为1.9.0

首先,我们获取到源码,这里我们使用生产环境下的Strview.js,也就是上面实例中的这个地址:

https://cdn.jsdelivr.net/npm/strview@1.9.0/dist/strview.global.js

我们,先大体看下源码,加上空行,源码一共125行。不压缩的话,仅仅4kb。

  1. var Strview = (function (exports) { 
  2.     'use strict'
  3.  
  4.     // global object 
  5.     const globalObj = { 
  6.         _nHtml: [], 
  7.         _oHtml: [], 
  8.         _el: null
  9.         _data: null
  10.         _template: null
  11.         _sourceTemplate: null 
  12.     }; 
  13.  
  14.     // initialization 
  15.     function createView(v) { 
  16.         globalObj._data = v.data; 
  17.         globalObj._template = v.template; 
  18.         globalObj._sourceTemplate = v.template; 
  19.         globalObj._el = v.el; 
  20.         v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!"); 
  21.     } 
  22.  
  23.     // event listeners 
  24.     function eventListener(el, event, cb) { 
  25.         document.querySelector(el).addEventListener(event, cb); 
  26.     } 
  27.  
  28.     // processing simple values 
  29.     function ref() { 
  30.         return new Proxy(globalObj._data, { 
  31.             get: (target, key) => { 
  32.                 return target[key
  33.             }, 
  34.             set: (target, key, newValue) => { 
  35.                 target[key] = newValue; 
  36.                 setTemplate(); 
  37.                 return true
  38.             } 
  39.         }) 
  40.     } 
  41.  
  42.     // reactiveHandlers 
  43.     const reactiveHandlers = { 
  44.         get: (target, key) => { 
  45.             if (typeof target[key] === 'object' && target[key] !== null) { 
  46.                 return new Proxy(target[key], reactiveHandlers); 
  47.             } 
  48.             return Reflect.get(target, key); 
  49.         }, 
  50.         set: (target, key, value) => { 
  51.             Reflect.set(target, key, value); 
  52.             setTemplate(); 
  53.             return true 
  54.         } 
  55.     }; 
  56.  
  57.     // respond to complex objects 
  58.     function reactive() { 
  59.         return new Proxy(globalObj._data, reactiveHandlers) 
  60.     } 
  61.  
  62.     // update the view 
  63.     function setTemplate() { 
  64.         const oNode = document.querySelector(globalObj._el); 
  65.         const nNode = toHtml(render(globalObj._sourceTemplate)); 
  66.         compile(oNode, 'o'); 
  67.         compile(nNode, 'n'); 
  68.         if (globalObj._oHtml.length === globalObj._nHtml.length) { 
  69.             for (let index = 0; index < globalObj._oHtml.length; index++) { 
  70.                 const element = globalObj._oHtml[index]; 
  71.                 element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); 
  72.             } 
  73.         } 
  74.     } 
  75.  
  76.     // judge text node 
  77.     function isTextNode(node) { 
  78.         return node.nodeType === 3; 
  79.     } 
  80.  
  81.     // compile DOM 
  82.     function compile(node, type) { 
  83.         const childNodesArr = node.childNodes; 
  84.         for (let index = 0; index < Array.from(childNodesArr).length; index++) { 
  85.             const item = Array.from(childNodesArr)[index]; 
  86.             if (item.childNodes && item.childNodes.length) { 
  87.                 compile(item, type); 
  88.             } else if (isTextNode(item) && item.textContent.trim().length !== 0) { 
  89.                 type === 'o' ? globalObj._oHtml.push(item) : globalObj._nHtml.push(item); 
  90.             } 
  91.         } 
  92.     } 
  93.  
  94.     // string to DOM 
  95.     function toHtml(domStr) { 
  96.         const parser = new DOMParser(); 
  97.         return parser.parseFromString(domStr, "text/html"); 
  98.     } 
  99.  
  100.     // template engine 
  101.     function render(template) { 
  102.         const reg = /\{(.+?)\}/; 
  103.         if (reg.test(template)) { 
  104.             const key = reg.exec(template)[1]; 
  105.             if (globalObj._data.hasOwnProperty(key)) { 
  106.                 template = template.replace(reg, globalObj._data[key]); 
  107.             } else { 
  108.                 template = template.replace(reg, eval(`globalObj._data.${key}`)); 
  109.             } 
  110.             return render(template) 
  111.         } 
  112.  
  113.         return template; 
  114.     } 
  115.  
  116.     // exports 
  117.     exports.createView = createView; 
  118.     exports.eventListener = eventListener; 
  119.     exports.reactive = reactive; 
  120.     exports.ref = ref; 
  121.  
  122.     Object.defineProperty(exports, '__esModule', { value: true }); 
  123.  
  124.     return exports; 
  125. }({})); 

首先,我们会看到最外层定义了一个Strview变量,暴露在外面,并将一个立即执行函数(IIFE)赋予这个变量。

我们先来看下这个立即执行函数。

  1. var Strview = (function (exports) { 
  2. // ... 
  3.  
  4. }({})); 

函数中需要传一个形参exports,并且又立即传入一个空对象。

然后,我们来看下函数内的内容。

我们会看到函数中有很多变量与函数方法,那么我们就按功能来分析。

首先,我们看到了一个全局对象,全局对象中分别定义了几个属性。这样做是为了减少全局变量污染,JS可以随意定义保存所有应用资源的全局变量,但全局变量可以削弱程序灵活性,增大了模块之间的耦合性。最小化使用全局变量的一个方法是在你的应用中只创建唯一一个全局变量。

  1. // global object 
  2. const globalObj = { 
  3.     _nHtml: [], // 存放新DOM数组 
  4.     _oHtml: [], // 存放旧DOM数组 
  5.     _el: null, // 挂载DOM节点 
  6.     _data: null, // 存放数据 
  7.     _template: null, // 模板字符串 
  8.     _sourceTemplate: null // 源模板字符串 
  9. }; 

然后,我们接着看初始化阶段,这个阶段是将模板字符串转换成视图。

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

我们看到这个createView方法传入了一个参数,也就是我们之前传入的那个对象:

  1. Strview.createView({ 
  2.         el: "#app"
  3.         data: { 
  4.             msg: 'Hello World' 
  5.         }, 
  6.         template: `<p>{msg}</p>`, 
  7.     }); 

我们看到传入的对象中的属性分别赋给全局对象globalObj。在最后一行中通过判断v.el是否是真值,如果是就执行这行代码:

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

这行代码执行了insertAdjacentHTML()方法,这个方法在MDN上是这样解释它的。

  • insertAdjacentHTML() 方法将指定的文本解析为 Element 元素,并将结果节点插入到DOM树中的指定位置。它不会重新解析它正在使用的元素,因此它不会破坏元素内的现有元素。这避免了额外的序列化步骤,使其比直接使用innerHTML操作更快。

insertAdjacentHTML()方法传入的第二个参数是是要被解析为HTML或XML元素,并插入到DOM树中的DOMString,render(globalObj._template)这个方法就是返回的DOMString。

如果是假,就执行console.error("Error: Please set el property!"),在浏览器上输出错误。

既然这个用到了render(globalObj._template)这个方法,那么我们下面来看下。

  1. // template engine 
  2. function render(template) { 
  3.     const reg = /\{(.+?)\}/; 
  4.     if (reg.test(template)) { 
  5.         const key = reg.exec(template)[1]; 
  6.         if (globalObj._data.hasOwnProperty(key)) { 
  7.             template = template.replace(reg, globalObj._data[key]); 
  8.         } else { 
  9.             template = template.replace(reg, eval(`globalObj._data.${key}`)); 
  10.         } 
  11.         return render(template) 
  12.     } 
  13.  
  14.     return template; 

首先,这个render(template)方法传入了一个参数,第一个参数是模板字符串。

然后,我们进入这个方法中看一下,首先,我们定义了正则/\{(.+?)\}/,用于匹配模板字符串中的{}中的内容。如果匹配为真,就进入这个逻辑:

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

我们在第一行代码中看到了这行代码const key = reg.exec(template)[1],这里使用的是reg.exec()方法,MDN这样解释它:

  • exec() 方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null。在设置了 global 或 sticky 标志位的情况下(如 /foo/g or /foo/y),JavaScript RegExp 对象是有状态的。他们会将上次成功匹配后的位置记录在 lastIndex 属性中。使用此特性,exec() 可用来对单个字符串中的多次匹配结果进行逐条的遍历(包括捕获到的匹配),而相比之下, String.prototype.match() 只会返回匹配到的结果。

所以,通过这个方法我们取到了模板字符串中的{}中的内容,它一般是我们存取数据_data中的属性。首先,我们判断globalObj._data对象中是否有这个key,如果有我们就使用字符串替换方法replace来把对应的占位符key替换成所对应的值。下面接着进行递归,直到reg.test(template)返回为false。最终,render()方法返回处理后的template。

看完render()方法,我们来看下事件处理阶段,也就是eventListener()方法。

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

这个方法很简单,第一个参数传入DOM选择器,第二个参数传入一个事件名,第三个参数传入一个回调函数。

最后,我们来看下Strview.js的数据响应系统。

  1. // processing simple values 
  2. function ref() { 
  3.     return new Proxy(globalObj._data, { 
  4.         get: (target, key) => { 
  5.             return target[key
  6.         }, 
  7.         set: (target, key, newValue) => { 
  8.             target[key] = newValue; 
  9.             setTemplate(); 
  10.             return true
  11.         } 
  12.     }) 
  13.  
  14. // reactiveHandlers 
  15. const reactiveHandlers = { 
  16.     get: (target, key) => { 
  17.         if (typeof target[key] === 'object' && target[key] !== null) { 
  18.             return new Proxy(target[key], reactiveHandlers); 
  19.         } 
  20.         return Reflect.get(target, key); 
  21.     }, 
  22.     set: (target, key, value) => { 
  23.         Reflect.set(target, key, value); 
  24.         setTemplate(); 
  25.         return true 
  26.     } 
  27. }; 
  28.  
  29. // respond to complex objects 
  30. function reactive() { 
  31.     return new Proxy(globalObj._data, reactiveHandlers) 

上面这些代码主要是reactive()、ref()这两个方法的实现。reactive()方法是针对复杂数据的处理,比如嵌套对象以及数组。ref()方法主要是针对简单数据的处理,像是原始值与单一非嵌套对象。

它们两个都是基于Proxy代理来实现数据的拦截与响应,MDN中这样定义它。

  • Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

它们两个Proxy对象第一个参数都是我们在初始化定义的globalObj._data,第二个参数是一个通常以函数作为属性的对象。这里都定义了get()方法、set()方法,get()是属性读取操作的捕捉器,set()是属性设置操作的捕捉器。

reactive()、ref()这两个方法实现不一样的地方是reactive()方法加上了对嵌套对象判断来实现递归。

我们在set()方法中看到它们都调用了setTemplate()方法,下面,我们来看下这个方法。

  1. // update the view 
  2. function setTemplate() { 
  3.     const oNode = document.querySelector(globalObj._el); 
  4.     const nNode = toHtml(render(globalObj._sourceTemplate)); 
  5.     compile(oNode, 'o'); 
  6.     compile(nNode, 'n'); 
  7.     if (globalObj._oHtml.length === globalObj._nHtml.length) { 
  8.         for (let index = 0; index < globalObj._oHtml.length; index++) { 
  9.             const element = globalObj._oHtml[index]; 
  10.             element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); 
  11.         } 
  12.     } 

首先,我们取到初始化时挂载的DOM节点,接着我们使用toHtml()方法将render(globalObj._sourceTemplate)方法作为第一个参数传入。

我们先来看下toHtml()方法,这里的第一个参数domStr,也就是render(globalObj._sourceTemplate)。

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

toHtml()方法第一行我们实例化了一个DOMParser对象。一旦建立了一个解析对象以后,你就可以使用它的parseFromString方法来解析一个html字符串。

然后,我们回到setTemplate()方法中,变量nNode被赋值了toHtml(render(globalObj._sourceTemplate)),这里是被处理成一个DOM对象。

接着,执行compile()方法。

  1. compile(oNode, 'o'); 
  2. compile(nNode, 'n'); 

我们来看下这个compile()方法。

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

这个方法是将遍历DOM元素并把每一项存储到我们初始化定义的数组里面,分别是globalObj._oHtml和globalObj._nHtml,这个方法中用到了isTextNode()方法。

  1. // judge text node 
  2. function isTextNode(node) { 
  3.     return node.nodeType === 3; 

这个方法第一个参数是一个Node节点,如果它的nodeType属性等于3就说明这个节点是文本节点。

最后,我们又回到setTemplate()方法中,接着执行以下代码:

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

判断两个数组的长度是否一样,如果一样就遍历globalObj._oHtml,最后判断globalObj._nHtml[index].textContent是否等于globalObj._oHtml[index].textContent,如果不相等,直接将globalObj._nHtml[index].textContent赋于globalObj._OHtml[index].textContent,完成更新。

最后,将这几个定义的方法赋于传入的exports对象并返回这个对象。

  1. // exports 
  2. exports.createView = createView; 
  3. exports.eventListener = eventListener; 
  4. exports.reactive = reactive; 
  5. exports.ref = ref; 
  6.  
  7. Object.defineProperty(exports, '__esModule', { value: true }); 
  8.  
  9. return exports; 

这里,有一行代码Object.defineProperty(exports, '__esModule', { value: true }),这行代码其实也可以这么写exports.__esModule = true。表面上看就是把一个导出对象标识为一个 ES 模块。

随着 JS 不断发展和 Node.js 的出现,JS 慢慢有了模块化方案。在 ES6 之前,最有名的就是 CommonJS / AMD,AMD 就不提了现在基本不用。CommonJS 被 Node.js 采用至今,与 ES 模块共存。由于 Node.js 早期模块化方案选择了 CommonJS,导致现在 NPM 上仍然存在大量的 CommonJS 模块,JS 圈子一时半会儿是丢不掉 CommonJS 了。

Webpack 实现了一套 CommonJS 模块化方案,支持打包 CommonJS 模块,同时也支持打包 ES 模块。但是两种模块格式混用的时候问题就来了,ES 模块和 CommonJS 模块并不完全兼容,CommonJS 的 module.exports 在 ES 模块中没有对应的表达方式,和默认导出 export default 是不一样的。

而__esModule则是用来兼容 ES 模块导入 CommonJS 模块默认导出方案。

结语

至此,Strview.js的源码分析完毕。谢谢阅读~

开发版本

推荐使用StrviewCLI搭建StrviewApp项目脚手架。

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

生产版本

直接引入CDN链接,目前版本为1.9.0。

  • https://cdn.jsdelivr.net/npm/strview@1.9.0/dist/strview.global.js

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

延伸 · 阅读

精彩推荐
  • 编程技术Delphi - Indy idMessage和idSMTP实现邮件的发送

    Delphi - Indy idMessage和idSMTP实现邮件的发送

    这篇文章主要介绍了Delphi - Indy idMessage和idSMTP实现邮件的发送,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...

    JJ_JeremyWu6592020-09-22
  • 编程技术用户态 Tcpdump 如何实现抓到内核网络包的?

    用户态 Tcpdump 如何实现抓到内核网络包的?

    在网络包的发送和接收过程中,绝大部分的工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序 tcpdump 是那如何实现抓到内核态的包...

    开发内功修炼11612021-09-08
  • 编程技术让开发效率倍增的 VS Code 插件

    让开发效率倍增的 VS Code 插件

    今天来分享一些提升开发效率的实用 VS Code 插件!Better Comments 扩展可以帮助我们在代码中创建更人性化的注释,有不同形式和颜色的注释供我们选择。 ...

    前端充电宝7132022-04-21
  • 编程技术AIOps,SRE工程师手中的利器

    AIOps,SRE工程师手中的利器

    AIOps开始成为一种极为重要的站点可靠性工程工具。它能够高效吸纳观察数据、参与数据以及来自第三方工具的数据,判断系统运行状态并保证其处于最佳...

    至顶网5972021-03-08
  • 编程技术简单、好懂的Svelte实现原理

    简单、好懂的Svelte实现原理

    本文会围绕一张流程图和两个Demo讲解,正确的食用方式是用电脑打开本文,跟着流程图、Demo一边看、一边敲、一边学...

    魔术师卡颂4822021-11-10
  • 编程技术真正聪明的程序员,总有办法不加班

    真正聪明的程序员,总有办法不加班

    工作效率提升了,就可以少加班了,聪明的程序员,总会有一堆可以提升编码效率的工具?当一种工具满足不了工作需求,就去探索新的,今天纬小创就给...

    今日头条12482021-03-04
  • 编程技术2021年值得关注的React PDF 库

    2021年值得关注的React PDF 库

    今天,许多网络应用程序为其用户提供内置的PDF浏览选项。然而,选择一个并不容易,因为它们的功能远远超过显示PDF。在这篇文章中,我将评估5个React的...

    TianTianUp5232021-06-21
  • 编程技术从Context源码实现谈React性能优化

    从Context源码实现谈React性能优化

    这篇文章主要介绍Context的实现原理,源码层面掌握React组件的render时机,从而写出高性能的React组件,源码层面了解shouldComponentUpdate、React.memo、PureComponen...

    魔术师卡颂5312020-12-20