响应式数据间同步的场景 技术总是由业务需求驱动的。酷家乐的核心业务之一是提供户型的 3D 渲染功能,并为此实现了一整套在线设计工具供用户编辑户型、建模、装修设计等。这一系列工具将各种户型、家具等 3D 模型渲染出来,并且在此之前都是由 Flash 技术实现。而随着浏览器技术的发展,Flash 工具已经接近生命周期的结束,于是一套新的基于 HTML5 实现的工具开始被提上日程,最终经过技术调研确立以类 React 方法的 Virtual DOM 技术驱动 canvas 中的 3D 渲染。于是有了如下的业务场景:
原始数据与视图框架之间的沟壑:中间数据结构的必要性随着前端工程复杂度越来越高,数据管理也越来越容易变得混乱,框架应运而生,但不同框架对数据格式或多或少都有限制。业务特性对数据格式有要求,视图框架对数据格式也有要求,当两个要求冲突时,问题就来了。
比如 React 视图框架中,为了达到最佳渲染效率,需要数据是 immutable 形式;对应的 Redux 数据管理框架,需要数据是树状结构、可 JSON 化。但在 3D 渲染的业务场景中,为了便于计算,数据存储为互相引用的图状节点形式,不可 JSON 化,不可 immutable 化。格式冲突使得原始数据无法直接应用到视图框架中。
为此,需要在原始数据格式和选定的视图框架之间创建中间数据结构作为连接,使其适应视图框架对于数据格式的要求。
这个等同于视图数据的中间数据结构使得:
新的问题:中间数据和原始数据间的同步 响应式解决方案引入中间数据解决了关键性问题,但是如何保持原始数据到中间数据的同步变成了另一个问题。
这个数据间同步的问题其实类似于数据-视图的同步问题,从一种结构持续同步到另一种结构。
简单的 MVC 做法(Backbone,Dojo,Ember 等):
初始化:定义转换函数根据数据初始化视图
更新:根据数据改动,人工更新视图
其中的问题在于更新流程完全人工维护,并且需要人工保证更新产出结果与初始化产出结果一致,容易出错。于是有了现在的响应式的视图概念,比如 React :
数据到视图的转换(同步)函数仅指定一次,初始化和更新都交由框架自行解决。在此基础上,数据和视图在任意时刻都是同步的,数据变更,则视图同步变更。开发者仅关系数据源,不再关系视图如何更新。
同样的,回到中间数据和原始数据的同步上,也应当是响应式,使得引入中间数据结构的同时,不会引入同步的维护成本。
如何实现响应式数据同步?响应式数据同步的目标应当有:
响应式:同一份转换函数,自动应用初始化和更新流程;保证任意时刻同样的原始数据,同步函数得出同样的结果数据
最小局部更新:每次触发更新同步时,仅同步已更改的数据,不修改未更改的数据(以支持 React PureRender 特性)
支持额外无关数据的混入:允许在转换函数中指定其他任意数据,并在同步过程中保留这些数据(以承载交互数据)
弱化新概念的引入
实现可拆分为如下几个部分:
依赖分析以及数据更新监听依赖分析和监听的关键在于监管数据的获取和修改,ES5 提供了 getter/setter 来隐式的监管属性的读写操作(弊端是定义 getter/setter 必须提前知晓属性列表,无法监管动态新增的属性,除非显式约束属性的读写方式)。ES2017 的 Proxy 方式走得更远(有兼容性问题),可以监管任意的已知或未知属性的读写操作(乃至方法调用等诸多行为,但这里我们只关心数据的读写)。
利用这些特性监管属性读写后,就可以进一步作依赖分析和更新通知。
getter/setter 或 Proxy 可以提供细粒度的数据读写监管,但通常业务并不需要过细的读写操作粒度,可以根据原始数据的特征做优化调整,比如仅定义某一级的数据作为依赖源,仅监管该级数据读写;人为修饰数据获取和数据更新函数,人工指定依赖和更新源。借此缩小监听范围,提高效率。
数据转换函数转换函数定义了原始数据到结果数据的绑定关系。
与 React 之类的 数据-视图 绑定一样,转换关系定义之后,实际执行分为相对简单的初始化过程以及较为复杂的局部更新过程。
局部更新过程的要求:
以下方法都是为了优化局部更新,提升最终效率。
局部更新调优:父子节点独立更新
当原始数据[户型]
和[家具]
更新时,最终仅单独触发结果数据中的[户型]
和[家具]
节点自身数据的同步。[房间]
和[墙面]
的转换函数不会被调用。
这与 React 的 数据-视图 的同步过程有很大区别。React 采用的策略是从当前节点往下整颗树都会重新调用转换函数。
局部更新调优:更新排序在同步过程中,父节点的同步结果可能会影响子节点,反过来则不会。假如父节点删除了子节点,那么子节点的同步操作就是不必要的。
同步过程中,按树从顶向下的顺序更新节点,可以避免无用更新。
局部更新调优:数组
数组在同步操作中是个特殊的对象,无论是 React 还是 Vue 都对数组元素的同步更新做了特殊的优化处理。
数组元素有些特性:
根据数组元素的特性,更新时需要做到:
做到以上几点,数据同步的准确性和性能就可以得到保障。
数据同步绑定的语法定义显然不可能仅仅为了两种数据间的同步,让开发者去使用 React 或 Vue 之类的视图框架所定义的 数据-视图 绑定语法。(实际上也是不能,假如能用 Virtual DOM 的方式描述原始数据到结果数据的绑定,并做到最小化的局部更新,显然已经可以用类似的方式直接将原始数据映射到视图)
最终的语法定义尽量保持写法不变,前后对比如下:
总结引入中间数据之后,我们得到了:
响应式是个不错的解决方式,引入中间数据的同时并不会带来过多的代价,因为大多数情况下使用者只需要关心原始数据更改(就好像在 React 视图框架中我们只关心如何修改数据,不关心视图如何更新)。
同时数据间同步的解决方案的适用面广,它只是做同步这一件事,完全可以适用于其他情况,做任意两层之间数据格式不匹配的桥梁。