Vue和React是有相似点的,但在实际应用中,两套技术解决方案还是有差异的。本文的重点在于寻找这些差异,并试图探究差异背后是否存在共同点。
1.1 模板与JSX图1:Vue 实现循环输出 span 标签图2:React 实现循环输出 span 标签
图1与图2的红框部分即为实现循环输出span标签的关键部分,从中可以看出两种明显不一样的实现方式。
图1是Vue的实现方法,这种基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层Vue实例的数据。所有Vue.js的模板都是合法的 HTML,所以能被遵循规范的浏览器和HTML解析器解析[6]。模板语法兼容了HTML的普遍性与易读性,也能为传统的HTML免去累赘,可读性提高,使用上也非常易上手。与Vue一样,Python与Angular均使用模板语法。
图2是React的实现方法,将html标签概念模糊化,将数组结构的商品列表aGoodsList转变成map结构后直接渲染出来,而不对原有的HTML标签、CSS样式、JS代码进行区分。React倡导的是JSX + inline style,即在JS中书写HTML,在HTML中嵌入css,当渲染时,React会把以
然而React的实现并非没有缺点,在工程开发中,我曾试过维护一个几千行的tsx文件,render函数混乱,代码可读性极差,完全体现了all in js的缺点。但究其根本,这并不是React的问题,而是开发者迫于时间没有就功能等对组件进行分离,从而造成难以拓展功能的局面。
总的来说,Vue与React在这一点上明显走向了两个不同的方向。如果说Vue还是那个坚持"结构、视图、逻辑分离"的好学生,那么React未免有点像坏小孩了。个人认为,Vue实际上是折中的做法,而React不过是做了一次勇敢的洗牌,呼唤大家忘记UI。
1.2 Virtual DOM
1.2.1 更新DOM是昂贵的操作
不可否认,Vue 2.0与React都有Virtual DOM的实现,此处为了便于比较,需要先介绍网页的渲染过程。图3:Webkit 内核浏览器渲染页面流程
在网页生成的时候至少会渲染一次页面,同时还很可能会多次渲染,渲染中又分重排和重绘两种,不可避免地两种都会让至少一部分DOM树重新生成[8],再次触发图3的流程。从图3中可以看见,每一次渲染还会消耗资源,造成性能低下,所以有没有一种办法可以不直接操作DOM树,而是利用一个中间体去替代DOM树发生变化,直到最后才把差异投射到DOM树上呢?
成为中间体的条件:
1.操作中间体耗费的性能要小于操作DOM耗费的
2. 中间体能复现DOM树
基于以上两点,结合浏览器的渲染过程,很明显JavaScript是满足中间体的所有条件的。一是因为以V8为首的JavaScript的解析引擎解析速度快,二是因为JavaScript的可操作性强,三则是因为其能复现DOM树。于是,JavaScript对象被选作中间体实现Virtual DOM。
1.2.2 Virtual DOM的实现步骤
a. 利用JavaScript对象模拟DOM树
b. 比较两颗DOM树的差异
非常明显,比较原始DOM树与改变后的JavaScript对象是整个算法中最核心的部分。但由于在前端开发中,跨层的变动非常少,所以Virtual DOM只比较同一层级的DOM元素。图4:Diff 算法的层级比较
c. 在真正的DOM树上实现差异
1.2.3 Virtual DOM上React与Vue的区别
React在应用的状态发生变化后,全部组件都会重新渲染,相当于重排,性能开销极大且此时Virtual DOM无法派上用场。但Vue和另一个MVVM框架Knockout一样,采用的是基于依赖收集的观测机制而非AngularJS中的"脏检测"机制,这意味着Vue会跟踪每一个组件的依赖关系。这种跟踪依赖关系的机制保障了Vue默认选择最佳状态,从而可以利用Virtual DOM。
1.3 双向数据绑定与单向数据流
当问起React是什么的时候,往往会有人回答单向数据流。当问起Vue是什么的时候,往往会有人回答双向数据绑定。那么事实上,Vue是不是真的就是双向数据绑定呢?React是不是真的就是单向数据流呢?他们之间能不能相互转化呢?
1.3.1 Vue与双向数据绑定
双向数据绑定(Two-way binding)不止在Web中有出现,在C#开发中同样地也有出现。不妨大胆地猜测,在所有前端开发中,实际上都会存在双向数据绑定。双向数据绑定不局限于框架,也不局限于语言,一般情况下只取决于是否是UI组件(向自定义组件传值的情况暂不考虑)。不论是AngularJS的ng-model还是Vue的v-model,实际上都只是一种语法糖(one-way binding与auto event binding)。.
不知道新手在接触React时是否发生过与我同样的情况,那就是习惯于利用页面的局部state刷新全页面。我认为,先上手Vue以及AngularJS的开发者在刚刚接触React时会更加容易犯这个错误,究其原因,在于Vue与AngularJS的双向绑定向开发者把状态管理隐藏在框架深处,调用者对此缺乏一种感知。最直接的例子就是,我曾因开发需要断点调试进入ElementUI框架table组件源码,发现实际上Vue与React一样都会有局部的state,一样存在所谓的setState刷新页面方法,但这个局部state并不是必须的,往往会被data与双向数据绑定代替。
反过来看,这样意味着Vue中并非所有的传递都是双向数据绑定,最明显的莫过于prop。当父组件的属性发生变化时,属性会被传递到子组件,但子组件无法把变化通过prop传递给父组件。这意味着prop是只读的,无法被修改。当强行修改prop属性时,Vue会在控制台发出警告。另一佐证就是,子组件与父组件的通信不借助外部系统,那么就只能利用观察者模式借助Vue的自定义事件系统,接收子组件的消息分发使得父组件可以接收子组件的变化。
1.3.2 React与单项数据流
单凭React而言,我个人认为在状态管理上依旧非常混乱,因为它有一个局部state给予开发者更多地空间。
如果为React注入Flux的帮助,那么Flux提倡的单向数据流确实是存在的。其实现方式本质上也是借助观察者模式,利用全局分发器为各级组件分发状态,从而触发事件,本质上还是one-way binding + auto event binding的是实现。图5:Flux+React 下数据流动方向
正如上一小节中提及的Vue将状态管理隐藏在框架内部,如果此时将上图中的状态管理(actions与dispatchers)隐藏起来,就会得到:图6:隐藏状态管理后数据流动方向
而这,正是双向数据绑定。
1.3.3 双向数据绑定与单向数据流
如上两小节讨论的那样,总的来说,单向数据流就是利用观察者模式在全局只使用一个分发器从而实现底层事件触发,双向数据绑定则是将状态管理放心地交给框架设计者实现,只需要做好底层事件触发机制即可。双向数据绑定与单向数据流的区别在于适用的组件范围与是否选择向调用者隐藏状态管理。所以不难看出,两者完全是可以转化的。
作为普通开发者也就是调用者,我们实际不必以一两个词为框架下定论,而是要试图寻找框架之间的内在联系,只有这样,才能在繁琐的业务开发中不断学习进步。我们更加需要做到因地制宜地选用合适的方式完成开发任务,在全局中利用观察者模式进行状态分发控制事件触发,在UI组件等问题上可以使用双向数据绑定以节约开发成本,提升开发效率。
*本文相关内容已经转载到公司内网,此外没有任何转载。
参考资料:
3. Stoyan Stefanov, Rendering: repaint, reflow/relayout, restyle,
5. Domenico De Felice, How browsers work,