当前位置: 首页 > 科技观察

基于JSX的动态数据绑定

时间:2023-03-14 18:10:46 科技观察

JSX的动态数据绑定属于作者的React和前端工程实践。本文设计的参考资料参考React学习与实践资料索引。如果还有疑问,可以参考《现代JavaScript开发:语法基础与实战技巧》。基于JSX的动态数据绑定作者在2016-MyFront-EndRoad:ToolingandEngineering一文中提到,前端社区花了15年时间分离HTML、JavaScript和CSS,但随着JSX的出现,事情似乎又回到了原点到解放前一夜之间。Angular、Vue.js等MVVM前端框架使用指令来描述业务逻辑,而JSX本质上是JavaScript,即用JavaScript来描述业务逻辑。尽管JSX被一些开发者评论为语法丑陋,但作者仍然坚持JavaScriptFirst原则,尽可能使用JavaScript编写业务代码。在上一篇React初看:JSX详解中,我们探讨了JSX的前世今生和基本用法,这部分我们着手编写一个简单的面向DOM的JSX解析和动态数据绑定库;这部分涉及的代码总结在Ueact库中。JSX解析和DOM元素构造元素构造作者在JavaScript语法树和代码转换实践一文中介绍了Babel的原理和使用。这里我们仍然使用Babel作为JSX语法分析工具;为了将JSX语句转换为createElement调用,这里我们需要在项目的.babelrc文件中做如下配置:"plugins":["transform-decorators-legacy","async-to-promises",["transform-react-jsx",{"pragma":"createElement"}]],这里的createElement函数声明如下:/***DescriptionbuildsvirtualDOMfromJSX*@paramtagNametagname*@parampropsattribute*@paramchildrenArgs子元素列表*/exportfunctioncreateElement(tagName:string,props:propsType,...childrenArgs:[any]){}该函数包含三个参数,分别指定标签名称、属性对象和子元素列表;实际上,经过Babel的改造后,JSX文本会变成如下函数调用(这也包括ES2015其他的语法转换):...(0,_createElement.createElement)('section',null,(0,_createElement.createElement)('section',null,(0,_createElement.createElement)('button',{className:'link',onClick:handleClick},'CustomDOMJSX'),(0,_createElement.createElement)('input',{type:'text',onChange:functiononChange(e){console.log(e);}}))),...拿到元素标签后,我们首先要做的就是创建元素;创建元素createElementByTag过程中需要注意区分普通元素和SVG元素:exportconstcreateElementByTag=(tagName:string)=>{if(isSVG(tagName)){returndocument.createElementNS('http://www.w3.org/2000/svg',tagName);}returndocument.createElement(tagName);};属性处理新建一个element对象后,我们需要对createElement函数传入的后续参数进行处理,即设置相应的属性对于元素;基本属性包括样式类、内联样式、标签属性、事件、子元素和简单的HTML代码等。首先,我们需要处理子元素://处理所有子元素,如果子元素元素是简单的字符串,直接创建文本节点constchildren=flatten(childrenArgs).m??ap(child=>{//如果子元素也是Element,则创建子元素的副本if(childinstanceofHTMLElement){returnchild;}if(typeofchild==='boolean'||child===null){child='';}returndocument.createTextNode(child);});这里可以看出createElement函数的执行是自下而上执行的,所以传入的子元素参数实际上已经传递了渲染的HTML元素。接下来,我们需要处理其他属性:...//支持class和className都设置constclassName=props.class||props.className;//如果有样式类,则设置if(className){setAttribute(tagName,el,'class',classNames(className));}//内联样式解析getStyleProps(props).forEach(prop=>{el.style.setProperty(prop.name,prop.value);});//解析其他HTML属性getHTMLProps(props).forEach(prop=>{setAttribute(tagName,el,prop.name,prop.value);});//设置事件监听,这里为了解决一些异步问题浏览器所以使用同步写法letevents=getEventListeners(props);for(leteventofevents){el[event.name]=event.listener;}...React也允许直接设置元素内部的HTML代码,这里我们还需要判断是否存在dangerouslySetInnerHTML属性://如果手动设置HTML,则添加HTML,否则设置显示子元素if(setHTML&&setHTML.__html){el.innerHTML=setHTML.__html;}else{children.forEach(child=>{el.appendChild(child);});}到这里我们就完成了JSX格式的简单DOM标签转换的createElement函数,完整源码在这里。简单使用这里我们仍然使用create-webpack-app脚手架来搭建示例工程,这里我们以一个简单的计数器为例来说明它的使用方法。需要注意的是,这部分还没有介绍双向数据绑定,或者自动状态变化更新,或者简单的DOM选择器查询更新方法://App.jsimport{createElement}from'../../../src/dom/jsx/createElement';//页面中的Stateconststate={count:0};/***描述点击事件处理*@parame*/constandleClick=e=>{state.count++;document.querySelector('#count').innerText=state.count;};exportdefault(

CustomDOMJSX{console.log(e);}}/>

{state.count}
);//client.js//@flowimportAppfrom'./component/Count';document.querySelector('#root').appendChild(App);数据绑定当我们在后端使用Webpack编译JSX时,它会直接翻译成JavaScript中的函数调用,所以很自然地在作用域中声明变量,然后在JSX中直接引用于;但笔者在设计Ueact时,考虑到为了方便快速上手或简单的H5页面开发或现有代码库的升级,还是需要支持运行时动态编译;在这部分我们将讨论如何编写JSX格式的HTML模板和动态数据绑定这部分我们的HTML模板就是上面使用的JSX代码,不同的是我们还需要引入babel-standalone和Ueact的umd模式库:然后在这个页面的script标签中,我们可以渲染绑定模板数据:这里调用Ueact.observeDOM函数渲染模板,会获取指定元素的outerHTML属性,然后使用Babel动态插件编译:letinput=html2JSX(ele.outerHTML);letoutput=Babel.transform(input,{presets:['es2015'],plugins:[['transform-react-jsx',{pragma:'Ueact.createElement'}]]}).code;值得一提的是,由于HTML语法和JSX语法的差异,我们在得到渲染后的DOM对象后,需要修改一些元素语法;主要包括以下三种场景:自闭标签处理,即=>移除inputHTML中事件监听的引号,即onclick="{methods.handleClick}"=>onclick={methods.handleClick}去掉value的多余引号,即value="{state.a}"=>value={state.a}这里我们得到了Babel转换后的函数调用代码。接下来,我们需要执行这部分代码,完成数据的填充。最简单的方法是使用eval函数,但是由于该函数直接暴露在全局范围内,所以不推荐;我们使用动态构造Function的方式来调用:/***DescriptionisconstructedfrominputJSXfunctionstring*@paraminnerContext*/functionrenderFromStr(innerContext){letfunc=newFunction('innerContext',`let{state,methods,hooks}=innerContext;letele=${innerContext.rawJSX}returnere;`).bind(innerContext);//新建节点letnewEle:Element=func(innerContext);//使用指定元素的父节点替换自身innerContext.root.parentNode.replaceChild(newEle,innerContext.root);//替换后删除旧节点的引用,触发GCinnerContext.root=newEle;}innerContext包含我们定义的State和Methods等对象,这里使用特性JavaScript的词法作用域(LexicalScope)来传递变量;这部分的完整代码在这里。变更监控和重新渲染作者在2015-我的前端之路:数据流驱动接口中讨论了从DOM为中心到数据流驱动的转变。在本节中,我们将讨论如何自动监控状态变化并完成重新渲染。这里我们使用监听JavaScript对象属性的方法进行状态变化监听,使用作者的另一个库Observer-X,其基本用法如下:import{observe}from'../../dist/observer-x';constobj=observe({},{recursive:true});obj.property={};obj.property.listen(changes=>{console.log(changes);console.log('changesinobj');});obj.property.name=1;obj.property.arr=[];obj.property.arr.listen(changes=>{//console.log('changesinobj.arr');});//changesinthesingleeventloopwillbeprintoutsetTimeout(()=>{obj.property.arr.push(1);obj.property.arr.push(2);obj.property.arr.splice(0,0,3);},500);当一个对象的属性发生变化(增删改查)时,注册的回调事件被触发;即:...//将内部状态转换为可观察变量();});...【本文为专栏作家“张子雄”原创文章,如需转载请联系作者】点这里,查看该作者更多好文