基于 JSX 的动态数据绑定

开发 开发工具
在创建了新的元素对象之后,我们需要对 createElement 函数传入的后续参数进行处理,也就是为元素设置对应的属性;基本的属性包含了样式类、行内样式、标签属性、事件、子元素以及朴素的 HTML 代码等。

[[199315]]

基于 JSX 的动态数据绑定归属于笔者的 React 与前端工程化实践中的,本文中设计的引用资料参考 React 学习与实践资料索引,如果有对 JavaScript 基础语法尚存疑惑的可以参阅现代 JavaScript 开发:语法基础与实践技巧

基于 JSX 的动态数据绑定

笔者在 2016-我的前端之路: 工具化与工程化一文中提及,前端社区用了 15 年的时间来分割 HTML、JavaScript 与 CSS,但是随着 JSX 的出现仿佛事物一夕回到解放前。在 Angular、Vue.js 等 MVVM 前端框架中都是采用了指令的方式来描述业务逻辑,而 JSX 本质上还是 JavaScript,即用 JavaScript 来描述业务逻辑。虽然 JSX 被有些开发者评论为丑陋的语法,但是笔者还是秉持 JavaScript First 原则,尽可能地用 JavaScript 去编写业务代码。在前文 React 初窥:JSX 详解中我们探讨了 JSX 的前世今生与基本用法,而本部分我们着手编写简单的面向 DOM 的 JSX 解析与动态数据绑定库;本部分所涉及的代码归纳于 Ueact 库。

JSX 解析与 DOM 元素构建

元素构建

笔者在 JavaScript 语法树与代码转化实践 一文中介绍过 Babel 的原理与用法,这里我们仍然使用 Babel 作为 JSX 语法解析工具;为了将 JSX 声明转化为 createElement 调用,这里需要在项目的 .babelrc 文件中做如下配置:

  1. "plugins": [ 
  2.    "transform-decorators-legacy"
  3.    "async-to-promises"
  4.    [ 
  5.      "transform-react-jsx", { 
  6.        "pragma""createElement" 
  7.      } 
  8.    ] 
  9.  ], 

这里的 createElement 函数声明如下:

  1. /** 
  2.  * Description 从 JSX 中构建虚拟 DOM 
  3.  * @param tagName 标签名 
  4.  * @param props 属性 
  5.  * @param childrenArgs 子元素列表 
  6.  */ 
  7. export function createElement( 
  8.   tagName: string, 
  9.   props: propsType, 
  10.   ...childrenArgs: [any
  11. ) {} 

该函数包含三个参数,分别指定标签名、属性对象与子元素列表;实际上经过 Babel 的转化之后,JSX 文本会成为如下的函数调用(这里还包含了 ES2015 其他的语法转化):

  1. ... 
  2.   (0, _createElement.createElement)( 
  3.     'section'
  4.     null
  5.     (0, _createElement.createElement)( 
  6.       'section'
  7.       null
  8.       (0, _createElement.createElement)( 
  9.         'button'
  10.         { className: 'link', onClick: handleClick }, 
  11.         'Custom DOM JSX' 
  12.       ), 
  13.       (0, _createElement.createElement)('input', { 
  14.         type: 'text'
  15.         onChange: function onChange(e) { 
  16.           console.log(e); 
  17.         } 
  18.       }) 
  19.     ) 
  20.   ), 
  21. ... 

在获取到元素标签之后,我们首先要做的就是创建元素;创建元素 createElementByTag 过程中我们需要注意区分普通元素与 SVG 元素:

  1. export const createElementByTag = (tagName: string) => { 
  2. if (isSVG(tagName)) { 
  3. return document.createElementNS('http://www.w3.org/2000/svg', tagName); 
  4.   } 
  5. return document.createElement(tagName); 
  6. }; 

属性处理

在创建了新的元素对象之后,我们需要对 createElement 函数传入的后续参数进行处理,也就是为元素设置对应的属性;基本的属性包含了样式类、行内样式、标签属性、事件、子元素以及朴素的 HTML 代码等。首先我们需要对子元素进行处理:

  1. // 处理所有子元素,如果子元素为单纯的字符串,则直接创建文本节点 
  2. const children = flatten(childrenArgs).map(child => { 
  3.   // 如果子元素同样为 Element,则创建该子元素的副本 
  4. if (child instanceof HTMLElement) { 
  5. return child; 
  6.   } 
  7.  
  8. if (typeof child === 'boolean' || child === null) { 
  9.     child = ''
  10.   } 
  11.  
  12. return document.createTextNode(child); 
  13. }); 

这里可以看出,对 createElement 函数的执行是自底向上执行的,因此传入的子元素参数实际上是已经经过渲染的 HTML 元素。接下来我们还需要对其他属性进行处理:

  1. ... 
  2. // 同时支持 class 与 className 设置 
  3. const className = props.class || props.className; 
  4.  
  5. // 如果存在样式类,则设置 
  6. if (className) { 
  7.   setAttribute(tagName, el, 'class', classNames(className)); 
  8.  
  9. // 解析行内样式 
  10. getStyleProps(props).forEach(prop => { 
  11.   el.style.setProperty(prop.name, prop.value); 
  12. }); 
  13.  
  14. // 解析其他 HTML 属性 
  15. getHTMLProps(props).forEach(prop => { 
  16.   setAttribute(tagName, el, prop.name, prop.value); 
  17. }); 
  18.  
  19. // 设置事件监听,这里为了解决部分浏览器中异步问题因此采用同步写法 
  20. let events = getEventListeners(props); 
  21.  
  22. for (let event of events) { 
  23.   el[event.name] = event.listener; 
  24. ... 

React 中还允许直接设置元素的内部 HTML 代码,这里我们也需要判断是否存在有 dangerouslySetInnerHTML 属性:

  1. // 如果是手动设置 HTML,则添加 HTML,否则设置显示子元素 
  2. if (setHTML && setHTML.__html) { 
  3.   el.innerHTML = setHTML.__html; 
  4. else { 
  5.   children.forEach(child => { 
  6.     el.appendChild(child); 
  7.   }); 

到这里我们就完成了针对 JSX 格式的朴素的 DOM 标签转化的 createElement 函数,完整的源代码参考这里

简单使用

这里我们依旧使用 create-webpack-app 脚手架来搭建示例项目,这里我们以简单的计数器为例描述其用法。需要注意的是,本部分尚未引入双向数据绑定,或者说是自动状态变化更新,还是使用的朴素的 DOM 选择器查询更新方式:

  1. // App.js 
  2. import { createElement } from '../../../src/dom/jsx/createElement'
  3.  
  4. // 页面内状态 
  5. const state = { 
  6.   count: 0 
  7. }; 
  8.  
  9. /** 
  10.  * Description 点击事件处理 
  11.  * @param e 
  12.  */ 
  13. const handleClick = e => { 
  14.   state.count++; 
  15.   document.querySelector('#count').innerText = state.count
  16. }; 
  17.  
  18. export default ( 
  19.   <div className="header"
  20.     <section
  21.       <section
  22.         <button className="link" onClick={handleClick}> 
  23.           Custom DOM JSX 
  24.         </button> 
  25.         <input type="text" 
  26.           onChange={(e)=>{ 
  27.             console.log(e); 
  28.           }} 
  29.         /> 
  30.       </section
  31.     </section
  32.     <svg> 
  33.       <circle cx="64" cy="64" r="64" style="fill: #00ccff;" /> 
  34.     </svg> 
  35.     <br /> 
  36.     <span id="count" style={{ color: 'red' }}> 
  37.       {state.count
  38.     </span> 
  39.   </div> 
  40. ); 
  41.  
  42. // client.js 
  43. // @flow 
  44.  
  45. import App from './component/Count'
  46.  
  47. document.querySelector('#root').appendChild(App); 

数据绑定

当我们使用 Webpack 在后端编译 JSX 时,会将其直接转化为 JavaScript 中函数调用,因此可以自然地在作用域中声明变量然后在 JSX 中直接引用;不过笔者在设计 Ueact 时考虑到,为了方便快速上手或者简单的 H5 页面开发或者已有的代码库的升级,还是需要支持运行时动态编译的方式;本部分我们即讨论如何编写 JSX 格式的 HTML 模板并且进行数据动态绑定。本部分我们的 HTML 模板即是上文使用的 JSX 代码,不同的是我们还需要引入 babel-standalone 以及 Ueact 的 umd 模式库:

然后在本页面的 script 标签中,我们可以对模板进行渲染并且绑定数据:

  1. <script> 
  2.   var ele = document.querySelector("#inline-jsx"); 
  3.  
  4.   Ueact.observeDOM( 
  5.     ele, 
  6.     { 
  7. state: { 
  8.         count: 0, 
  9.         delta: 1, 
  10.         items: [1, 2, 3] 
  11.       }, 
  12.       methods: { 
  13.         handleClick: function () { 
  14.           this.state.count+=this.state.delta; 
  15.           this.state.items.push(this.state.count); 
  16.         }, 
  17.         handleChange:function (e) { 
  18.           let value = parseInt(e.target.value); 
  19.           if(!Number.isNaN(value)){ 
  20.             this.state.delta = value; 
  21.           } 
  22.         } 
  23.       }, 
  24.       hooks: { 
  25.         mounted: function () { 
  26.           console.log('mounted'); 
  27.         }, 
  28.         updated:function () { 
  29.           console.log('updated'); 
  30.         } 
  31.       } 
  32.     }, 
  33.     Babel 
  34.   ); 
  35. </script> 

这里我们调用 Ueact.observeDOM 函数对模板进行渲染,该函数会获取指定元素的 outerHTML 属性,然后通过 Babel 动态插件进行编译:

  1. let input = html2JSX(ele.outerHTML); 
  2.  
  3.    let output = Babel.transform(input, { 
  4.      presets: ['es2015'], 
  5.      plugins: [ 
  6.        [ 
  7.          'transform-react-jsx'
  8.          { 
  9.            pragma: 'Ueact.createElement' 
  10.          } 
  11.        ] 
  12.      ] 
  13.    }).code; 

值得一提的是,因为 HTML 语法与 JSX 语法存在一定的差异,我们获取渲染之后的 DOM 对象之后,还需要对部分元素语法进行修正;主要包括了以下三个场景:

  • 自闭合标签处理,即 <input > => <input />
  • 去除输入的 HTML 中的事件监听的引号,即 onclick="{methods.handleClick}" => onclick={methods.handleClick}
  • 移除 value 值额外的引号,即 value="{state.a}" => value={state.a}

到这里我们得到了经过 Babel 转化的函数调用代码,下面我们就需要去执行这部分代码并且完成数据填充。最简单的方式就是使用 eval 函数,不过因为该函数直接暴露在了全局作用域下,因此并不被建议使用;我们使用动态构造 Function 的方式来进行调用:

 

  1. /** 
  2.  * Description 从输入的 JSX 函数字符串中完成构建 
  3.  * @param innerContext 
  4.  */ 
  5. function renderFromStr(innerContext) { 
  6.   let func = new Function
  7.     'innerContext'
  8.     ` 
  9.      let { state, methods, hooks } = innerContext; 
  10.      let ele = ${innerContext.rawJSX} 
  11. return ele; 
  12.     ` 
  13.   ).bind(innerContext); 
  14.  
  15.   // 构建新节点 
  16.   let newEle: Element = func(innerContext); 
  17.  
  18.   // 使用指定元素的父节点替换自身 
  19.   innerContext.root.parentNode.replaceChild(newEle, innerContext.root); 
  20.  
  21.   // 替换完毕之后删除旧节点的引用,触发 GC 
  22.   innerContext.root = newEle; 

innerContext 即包含了我们定义的 State 与 Methods 等对象,这里利用 JavaScript 词法作用域(Lexical Scope)的特性进行变量传递;本部分完整的代码参考这里。

变化监听与重渲染

笔者在 2015-我的前端之路:数据流驱动的界面中讨论了从以 DOM 为核心到数据流驱动的变化,本部分我们即讨论如何自动监听状态变化并且完成重渲染。这里我们采用监听 JavaScript 对象属性的方式进行状态变化监听,采用了笔者另一个库 Observer-X,其基本用发如下:

  1. import { observe } from '../../dist/observer-x'
  2.  
  3. const obj = observe( 
  4.   {}, 
  5.   { 
  6.     recursive: true 
  7.   } 
  8. ); 
  9.  
  10. obj.property = {}; 
  11.  
  12. obj.property.listen(changes => { 
  13.   console.log(changes); 
  14.   console.log('changes in obj'); 
  15. }); 
  16.  
  17. obj.property.name = 1; 
  18.  
  19. obj.property.arr = []; 
  20.  
  21. obj.property.arr.listen(changes => { 
  22.   // console.log('changes in obj.arr'); 
  23. }); 
  24.  
  25. // changes in the single event loop will be print out 
  26.  
  27. setTimeout(() => { 
  28.   obj.property.arr.push(1); 
  29.  
  30.   obj.property.arr.push(2); 
  31.  
  32.   obj.property.arr.splice(0, 0, 3); 
  33. }, 500); 

核心即是当某个对象的属性发生变化(增删赋值)时,触发注册的回调事件;即:

  1. ... 
  2.   // 将内部状态转化为可观测变量 
  3.   let state = observe(innerContext.state); 
  4.   ... 
  5. state.listen(changes => { 
  6.     renderFromStr(innerContext); 
  7.     innerContext.hooks.updated && innerContext.hooks.updated(); 
  8.   }); 
  9.   ... 

【本文是51CTO专栏作者“张梓雄 ”的原创文章,如需转载请通过51CTO与作者联系】

戳这里,看该作者更多好文

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2012-01-09 11:26:15

Java

2016-12-14 14:29:30

Java动态绑定机制

2009-06-18 14:40:44

TreeView动态绑

2021-12-12 20:10:49

域名动态IP

2021-09-01 10:37:25

鸿蒙HarmonyOS应用

2022-02-18 08:28:49

域名公网IP

2011-07-27 08:56:32

Oracle数据库绑定变量软解析

2021-09-14 18:33:39

React 数据交互

2014-12-29 10:19:01

Java

2021-09-01 14:36:14

鸿蒙HarmonyOS应用

2011-08-22 09:34:50

Objective-C多态动态类型

2023-10-20 09:51:00

编程开发

2009-06-18 14:13:53

动态化系统OSGi

2010-07-28 13:40:44

Flex数据绑定

2009-07-28 08:24:16

GridView绑定数

2011-12-05 13:44:34

JavaSpringMVC

2010-07-28 13:31:10

Flex数据绑定

2010-07-28 13:11:13

Flex数据绑定

2012-05-29 16:22:02

SpringMVC

2023-09-26 06:54:01

点赞
收藏

51CTO技术栈公众号