一、背景介绍
目前前端项目开发普遍会基于react、vue等框架,采用数据驱动的模式,通过虚拟dom减少与真是dom的交互来提高渲染性能。使用react、vue等框架开发就需要按照它们的要求定义组件、调用组件、传递数据,转变之前的开发思路,这些框架非常完善,但也具有很高约束性。对于一些没有使用react、vue等框架开发的项目,若它们自身也有比较完善的组件定义、组件调用、数据传递的方式,且框架基本上也能满足使用。对于这样的项目若想采用数据驱动和虚拟dom渲染,是将原有框架全部否定,花很多的时间使用react、vue等框进行重构?其实只需把存在用户交互的组件调整为数据驱动和虚拟dom渲染即可。那选择混合react、vue等框架只重构部分UI组件?这样会导致在项目中维护两种不同的组件定义、调用方式。本文将介绍如何基于snabbDom.js和jsx,在不改变原框架的组件定义、组件调用、数据传递的前提下,灵活的加入数据驱动的思想,并通过虚拟dom和diff算法简化dom操作优化组件的渲染机制。
二、snabbdom的使用
snabbdom是一个轻量级的虚拟dom框架,具有高效的diff算法及灵活的扩展性,vue就融合了snabbDom并在它的基础上进行了优化与扩展。
1、创建Vnode
snabbDom的h方法用于创建Vnode对象,Vnode的属性可以用来描述真实的dom节点,而虚拟dom就是一棵以 Vnode (virtual node)对象为基础的树。h方法接受三个参数:元素标签 / 选择器、属性对象、子节点数组:
由h方法创建的Vnode对象包含的属性有:sel元素选择器、data属性对象、children子节点数组、text文本内容、elm真实dom的指针、key标识,具体格式如下:
2、对比Vnode
Vnode是真实dom在内存中的描述,要实现数据改变时视图更新,需要使用diff算法对新、老Vnode进行对比,将变动映射到真实dom上才行。目前主流的diff算法思路上大体一致,都是在深度优先遍历DFS算法基础上,结合了真实dom的特性,在对比虚拟dom树时增加了同级对比、同类别对比、列表元素结合key标识进行对比的优化,复杂度为O(n)具有很高的性能,而snabbdom在此基础上,又增加了列表元素的两端对比,对比算法更为高效。
snabbdom的diff是通过patch方法来实现的,patch方法可以对比新、老Vnode,把变动的Vnode映射到真实dom上,也可以把Vnode挂载到html上,并替换指定的dom节点。patch方法的调用方式如下:
3、兼容ie低版本
使用snabbdom的项目代码,想要在ie浏览器低版本中正常运行,需要额外做一些处理,本文只介绍部分:
(1)在snabbdom的样式处理模块中,使用了dom元素的classList属性,可以将classList的调用更改为className的字符串处理。
(2)在snabbdom的自定义数据属性处理模块中,使用了dataset,可以更改为调用setAttribute和removeAttribute。
(3)在snabbdom的事件处理模块中,绑定事件时使用了addEventListener,ie9以下需要使用attachEvent绑定事件。
三、jsx
在代码中直接调用snabbdom的h方法来描述dom结构,需要多层嵌套调用可读性差不好维护,我们可以像react一样,在代码中使用jsx描述dom结构,然后使用转译工具将jsx转换成h方法的调用。jsx是一个看起来很像XML的JavaScript语法扩展,使用jsx表示dom的结构与层次就像使用html的标签语法那样清晰,jsx的出现让“ALL-in-JS”的思想得到进一步实现。
1、jsx的转换原理
jsx转换为h方法调用的过程
编译工具(如:Babel、JSTransform)先将jsx代码解析转换为AST(抽象语法树),在通过转换插件(如plugin-transform-react-jsx、jsx-transform)对AST进行遍历,查找类型为JSXElement或者JSXFragment的节点,
转换插件会将该类型节点替换为h方法对应的内容,等AST中所有的JSXElement或者JSXFragment节点转换完成后,转换插件会将处理后的AST编译为js代码输出,此时jsx代码就被转换为js代码中的h方法的调用了:
2、jsx的转换配置
介绍两种jsx转换为h方法调用的方式及配置:
(1)在打包代码之前,通过node脚本调用jsx-transform将jsx转换为js代码,然后在打包转换后的js。而jsx-transform是一个将jsx脱离于react,可以被使用于其他非react模块中的可配置转换工具,jsx-transform的factory属性可以用于设置要转换成的方法名,具体node脚本实现如下:
(2)使用webpack进行打包构建的项目,可以在webapck配置文件中设置Babel的plugin-transform-react-jsx插件,将pragma选项设置为你想要的函数名,即可实现jsx的转换。
3、不同的转换插件带来的差异
不同转换插件转换jsx代码时,jsx的书写格式会有一些差异,比如:使用plugin-transform-react-jsx和jsx-transform,使用jsx描述嵌套多个子元素时,jsx的书写格式就不太一样。
使用不同的转换插件除了jsx的书写格式有所差异外,转换为h方法的调用时,传参类型可能会与形参类型不同,比如列表类型的子元素使用jsx-transform转换时,传递给h方法的children参数会被转换为二维数组,此时就需要增加兼容处理,让h方法兼容children为二维数组的情况。
在文中介绍的两种转换方式中,jsx的书写格式和转换后的方法参数格式都细微需要兼容处理的部分。如果想要按照自己的风格开发jsx,并让jsx转换成你想要的方法调用和传参格式,可以自己开发一个转换插件,插件的开发可以参考Babel plugins 和 jsx PrimaryExpression。
4、支持自定义组件的jsx写法?
snabbdom目前只支持将html的标签元素如:div、span、svg等标签用Vnode进行描述并更新到页面,如果想像react中那样能够使用jsx语法表示自定义组件和组件的嵌套关系。自定义组件的jsx写法:
为了保证统一的定义、调用方式,组件的整体结构设计没有改动,只是将组件中对dom结构的描述方式做了调整,由原本的js字符串拼接html标签调整为jsx语法来表示,snabbdom中的基本标签类型就能够满足我们的需求,所以本文目前没有支持自定义组件的jsx写法。若想实现这部分功能可以在h方法的基础上进行扩展,使snabbdom的h方法能够处理sel为自定义组件类型的情况,比如增加自定义组件类型的判断、自定义组件Vnode格式的定义与创建、自定义组实例的创建、描述组件间嵌套关系的数据结构的处理等等,实现思路可以参考reactjs源码分析这篇文章。
四、在业务中的应用与实践
1、原项目框架特点
项目采用了MVC的开发模式,其中Model为一个配置文件,设置每个组件的状态数据,比如组件类型、默认显示隐藏、正则规则,并控制组件的嵌套关系。View对应的就是UI组件部分。Controller控制模块负责循环遍历配置文件,根据配置文件中的组件状态数据、嵌套关系,创建组件实例,控制组件的渲染,对内存中组件对象进行维护管理。除此外还有base组件、车辆信息校验模块,其中UI组件都继承自base组件。项目框架如下:
2、为引入数据驱动和虚拟dom所做的调整
只有UI组件部分才存在用户交互,所以我们只给UI组件增加数据驱动和虚拟dom,其他层级的组件不进行改动,降低改动成本。而且我们并不打算一次性把二手车发布pc端项目中所有的UI组件进行优化,而是随着业务需求将涉及到的组件进行调整优化。
UI组件继承了base组件,统一定义了用于存储状态数据的opts对象、拼接dom字符串createElem方法、将jq对象渲染到页面上的render方法。
为支持虚拟dom需要做如下的调整:
(1)将UI组件的createElem方法调整为使用jsx描述dom结构,初始化渲染时调用createElem创建组件的Vnode。且将原本用于存储组件jquery对象的container属性,调整为存储组件的Vnode对象,在数据更新diff新老Vnode时作为老的Vnode。
(2)新增JSXComnent组件,它继承了base组件来保持与不改动组件的统一性,同时所有为了使用虚拟dom而增加的特有方法都在JSXComnent中,以免影响base组件和不改动的UI组件。使用虚拟dom的UI组件需继承JSXComnent组件来使用特有的方法,这样既保留了共性又可以灵活的增加使用虚拟dom时的特性。
(3)在JSXComnent组件中重写base的render方法,未改动的组件仍继承base,且container值仍为jq对象且渲染逻辑不变。而支持虚拟dom组件的container为Vnode,重写后的render方法逻辑调整为调用snabbdom的patch方法将Vnode挂载到页面中。
(4)在JSXComnent组件增加setOpts方法,当数据改变时调用setOpts方法更新opts对象,并调用createElem方法根据新的opts创建新的Vnode,在调用patch方法进行新老Vnode对比并将变动映射到真实的dom上。为了避免连续多次调setOpts会频繁的生成Vnode和渲染dom,需要将多次数据更新进行合并批量处理,目前这部分处理还不够完善,之后会参考react状态合并和多任务方案进行优化。最终的渲染流程如下:
3、结合了snabbdom和jsx的组件定义与使用
(1)使用虚拟dom组件的定义:
(2)组件的调用保持不变,通过new创建对象实例并初始化组件的opts属性。以车身颜色为例:
(3)组件的嵌套保持不变,仍通过配置文件控制父子组件的嵌套关系,然后通过controller组件循环遍历初始化组件。
五、总结
本文结合项目的特点基于snabbdom和jsx在项目中增加数据驱动和虚拟dom的思想来优化组件的开发,将我们从频繁的dom操作中解脱出来。由于snabbdom自身的灵活性,虽然可以便捷的接入到项目中,但接入时需要考虑组件的设计、对象数据结构等因素来制定适合自身项目的方案来达到灵活、快速接入的目的,若投入较大的接入和维护成本那就不如直接使用react、vue的框架进行开发了。希望通过本文能为大家提供一种优化组件开发的参考方案。