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

手写简单的前端框架:vdom渲染和jsx编译

时间:2023-03-20 22:07:20 科技观察

作为前端工程师,前端框架几乎天天用,需要好好掌握。可以通过能否实现来判断对某项技术的掌握程度。手写一个前端框架,对于更好的掌握是很有帮助的。现代前端框架经过多年的迭代已经变得非常复杂,梳理其实现原理也变得困难重重。所以想写一个最简单版的前端框架,帮助大家理清思路。一个完整的前端框架涉及的内容还是很多的,我们一步步来,本文将实现vdom的渲染。vdom的全称是virtualdom,用于以声明方式描述页面。许多现代前端框架都是基于vdom的。前端框架负责将vdom转化为对真实dom的增删改查,即vdom的渲染。那么vdom是什么样子的呢?它是如何渲染的?DOM主要是元素、属性和文本,vdom也是一样,其中元素是{type,props,children}结构,文本是字符串和数字。比如这样一个vdom:{type:'ul',props:{className:'list'},children:[{type:'li',props:{className:'item',style:{background:'blue',color:'#fff'},onClick:function(){alert(1);}},children:['aaaa']},{type:'li',道具:{className:'item'},children:['bbbbddd']},{type:'li',props:{className:'item'},children:['cccc']}]}不难看出它描述了一个ul元素,它有三个li子元素,第一个子元素有一个style样式和一个onClick事件。前端框架通过这样的对象结构来描述界面,然后渲染到dom中。如何渲染这样的对象结构?很明显,递归是用来区别对待不同类型的。如果是文本类型,则使用document.createTextNode创建一个文本节点。如果是元素类型,则使用document.createElement创建元素节点。元素节点也有属性需要处理,递归渲染子节点。所以,vdom的渲染逻辑是这样的:。类型));for(constchildofvdom.children){render(child,dom);}for(constpropinvdom.props){setAttribute(dom,prop,vdom.props[prop]);}返回dom;}判断text是string和number:functionisTextVdom(vdom){returntypeofvdom=='string'||typeofvdom=='number';}判断element是object,type是标签名的字符串:functionisElementVdom(vdom){returntypeofvdom=='object'&&typeofvdom.type=='string';}元素创建完成后,如果有父节点需要挂载到父节点,组装成dom树:constmount=parent?(el=>parent.appendChild(el)):(el=>el);所以,完整的渲染函数看起来像这样:constrender=(vdom,parent=null)=>{constmount=parent?(el=>parent.appendChild(el)):(el=>el);如果(isTextVdom(vdom)){returnmount(document.createTextNode(vdom));}elseif(isElementVdom(vdom)){constdom=mount(document.createElement(vdom.type));for(constchildofvdom.children){render(child,dom);}for(constpropinvdom.props){setAttribute(dom,prop,vdom.props[props]);}返回dom;}};其中,元素的dom还需要设置属性,比如上面vdom中的style和onClick属性。对象合并后设置为style,onClick属性为事件监听器,用addEventListener设置,其余属性用setAttribute设置。constsetAttribute=(dom,key,value)=>{if(typeofvalue=='function'&&key.startsWith('on')){consteventType=key.slice(2).toLowerCase();dom.addEventListener(事件类型,值);}elseif(key=='style'&&typeofvalue=='object'){Object.assign(dom.style,value);}elseif(typeofvalue!='object'&&typeofvalue!='function'){dom.setAttribute(key,value);}}这样,vdom的渲染逻辑就完成了。使用上面的vdom渲染试试效果:render(vdom,document.getElementById('root'));vdom渲染成功!总结:《vdom会递归渲染,根据不同的类型,元素和文本会分别渲染使用createTextNode和createElement递归创建dom组装在一起,元素还需要设置属性,样式,事件监听器等属性是分别通过addEventListener、setAttribute等API进行设置。”“通过不同的API创建dom和设置属性,这就是vdom的渲染过程。”但是vdom写起来太麻烦,没有人会直接写vdom,通常是通过比较友好的DSL(领域特定语言)编写,然后编译成vdom,比如jsx和template,这里我们使用jsx方法,因为可以直接用babel编译将jsx编译成vdom上面的vdom改成jsx这样写:constjsx=alert(2)}>aaabbbbccccrender(jsx,document.getElementById('root'));显然比直接写vdom紧凑很多,但是需要编译一次。配置babel编译jsx:module.exports={presets:[['@babel/preset-react',{pragma:'createElement'}]]}编译后的产物是这样的:constjsx=createElement("ul",{className:"list"},createElement("li",{className:"item",style:{background:'blue',color:'pink'},onClick:()=>alert(2)},"aaa"),createElement("li",{className:"item"},"bbbb"),createElement("li",{className:"item"},"cccc"));render(jsx,document.getElementById('root'));为什么不直接vdom,而是一些函数呢?因为会有一次性执行的过程,可以放一些动态逻辑,比如从数据中取值:constdata={item1:'bbb',item2:'ddd'}constjsx=alert(2)}>aaa{data.item1}{data.item2}会编译成:constdata={item1:'bbb',item2:'ddd'};常量x=createElement("ul",{className:"list"},createElement("li",{className:"item",style:{background:'blue',color:'pink'},onClick:()=>alert(2)},"aaa"),createElement("li",{className:"item"},data.item1),createElement("li",{className:"item"},data.item2));这个叫做render函数,它执行的返回值是vdom。render函数名称之所以为createElement是因为我们在上面的babel配置中指定pragma为createElement。render函数是生成vdom,所以实现很简单:constcreateElement=(type,props,...children)=>{return{type,props,children};};改成jsx后测试一下渲染:渲染成功!我们在vdom的基础上更进一步,通过jsx写一些动态逻辑,然后编译成render函数,执行后生成vdom。这样比直接写vdom简单,可以做更灵活的vdom生成逻辑。代码已经上传到github:https://github.com/QuarkGluonPlasma/frontend-framework-exercize总结手写前端框架是更好掌握它最直接的方法,我们会逐步实现一个功能齐全的前端——端框架。在这篇文章中,我们实现了vdom的渲染。vdom是描述界面的对象。它的渲染是通过createElement、createTextNode等API递归创建组装dom元素、文本等的过程。元素节点还需要设置属性,比如style、事件监听器,其他属性会通过不同的API来设置。虽然最后是vdom的渲染,但是开发时不会直接写vdom,而是通过jsx描述页面,然后编译成render函数,执行后生成vdom。这样写起来更简洁,支持动态逻辑。(jsx编译使用babel,可以指定render函数的名称。)vdom渲染和jsx是前端框架的基础。组件等其他功能都是在此基础上实现的。在下一篇文章中,我们将实现组件的渲染。