寻找框架设计的平衡

Tags
Web Dev
Published
May 4, 2021
Author
HJS

Scope(服务范围)

较小的服务范围:React

优势:
  • 学习成本:提供的基本功能较少,学习成本低。
  • 生态系统:生态繁荣。很多功能官方没有实现,社区的机会就很多,很多第三方库陆续出现。
  • 维护成本:官方维护的核心代码少,维护成本低,可以投入更多精力在核心能力增强。如:React hooks、Concurrent等新概念。
劣势:
  • 技术选型:复杂业务场景下要引入新的概念,例如 CSS JS Implement,路由等,官方文档并没有提供这些,因此后续技术选型上存在分歧,需要研究大量不同的技术方案得出最佳实践,并带来持续的新概念理解成本。
  • 生态稳定:不稳定。非官方的生态意味着不稳定,缺乏统一的管理,模块之间可能出现不兼容的问题。

较大的服务范围:Angular

优势:
  • 技术选型:官方的方案足以解决绝大多数的业务场景,团队不需在技术选型上不会有分歧。
  • 生态稳定:稳定。官方做出改动时会考虑到整个生态,无需要担心兼容性问题。
劣势:
  • 学习成本:高,可能需要理解文档中所有的概念后才能开始写项目。
  • 生态系统:内置功能不适合用例时,可能会觉得不够灵活。比如希望用另一种方式做到这点,但是框架并没有将这个选择权交出去。并且对于大的服务范围来说,修改其中的一部分时,可能会对整个系统产生影响,因此进行修改很困难,生态比较弱。
  • 维护成本:官方维护成本比较高。

中等的服务范围:Vue

优势:
  • 学习成本:对于核心包来说,学习成本都比较低。
  • 技术选型:官方都有提供配套的生态插件,能够满足绝大多数业务场景。
  • 生态稳定:对于官方生态,是稳定的。
劣势:
  • 生态系统:虽然生态是按需的,官方有交出选择权,但也会极大影响社区的发展空间。
  • 维护成本:依然高,虽然生态不强求使用官方的,还还是需要持续维护。
  • 生态稳定:对于非官方的,生态依然不稳定。

渲染机制

DSL:JSX vs. Template

JSX(Render fucntion):React、Vue、Solid

  • 拥有和 JavaScript 同等的灵活性,表达力。例如动态标签名,或者 Template 渲染逻辑很复杂,希望抽离逻辑到 JavaScript 中。✔️
  • 更好 TypeScript 类型推导支持。更好的 linting、代码提示,智能补全。✔️
  • 由于足够灵活难以被静态分析,配合 Virtual DOM 方案 Diff 成本较高。 ❌

Template:Angular、Vue、Svelte

  • 更容易被静态分析,如果搭配 Virtual DOM 方案能够减少 Diff 的成本。 ✔️
  • 较差的 TypeScript 类型推导支持。因此官方通常会维护 IDE 插件来进行智能提示。 ❌
  • 失去 JavaScript 的表达力。例如无法满足动态的标签名需求。❌

Runtime Scheduling vs. AOT

Ps. 第 3 点指的是 Runtime 时是否可以和 Virtual DOM 相结合,Svelte 在编译时也有对 DOM 的抽象

Runtime Scheduling:React、Vue

React 在编译时将 JSX 编译为 Virtual DOM,而 Vue 则编译成 Render function,运行时执行返回 Virtual DOM。
  • 框架要在运行时承担更多的工作,自身的 runtime 更大。❌
  • 相对极致的 AOT,组件打包出来的 Bundle size 更小。✔️
  • 可以在 runtime 解析 JSX(render function),具有完全的灵活性。✔️
  • 可以与 Virtual DOM 相结合,享受到 Virtual DOM 的某些好处,同时也要承担 Virtual DOM 的缺点。 ❌ & ✔️

AOT:Svelte、Solid

Svelte 和 Solid 采用极致的 AOT 策略,没有任何中间表示(Virtual DOM),编译为直接的 DOM 更新。
  • 编译时根据用到的功能按需引入 runtime, 且运行时不需要承担编译等工作,runtime 更小。✔️
  • 编译时组件代码直接编译为操作 DOM 的原生 JavaScript,Bundle size 随着应用变大而提升。❌
  • 无法在 runtime 解析 JSX,语法的灵活性受到一定限制。❌
  • 完全与 Virtual DOM 绝缘。❌ & ✔️
理论上 AOT 和 JIT 最终打包出来的 bundle 体积存在一个交叉点,例如:Svelte 与 React 应用体积拐点

Virtual DOM vs. No Virtual DOM(Runtime)

  • Virtual DOM 的 Diff 有一定的开销。❌
  • 直接的细粒度 DOM 更新具有更好的性能。✔️
Ps. 对于非结构数据的变化,直接的 DOM 更新具有更好的性能。但对于结构数据变化(数组、对象),无法知道具体哪些元素发生变化,仍然需要 Diff。

常见误区

以下为尤雨溪拥护运行时使用 Virtual DOM 的理由,但目前看来理由并不是很站得住脚。

没有 Virtual DOM 就无法跨平台?

尤雨溪潜台词:Virtual DOM 就是视图的抽象,组件的渲染逻辑从 DOM 中解耦,开发者可以自定义渲染解决方案应用在其他平台。
Svelte 框架运行时不存在 Virtual DOM,但是在编译时仍然有一份对视图的抽象,开发者可以通过编译时自定义渲染器实现对跨平台的支持。例如 Svelte 现在就有浏览器环境的 DOM 渲染器,和 Node 环境的 SSR 渲染器。

没有 Virtual DOM 就无法支持 JSX、Render function?

尤雨溪潜台词:足够的灵活性难以静态分析出状态的 DOM 的绑定关系,因此只能 runtime 生成和视图对应的 Virtual DOM,通过 Diff 比较出具体发生变化的 DOM,然后再 Patch 更新 DOM。
Solid 实现了 AOT JSX 到细粒度 DOM 的更新,不过牺牲了一定的 JSX 灵活性,控制流语句引入模版式的写法。

补充链接

  • Comparison with standard Vue:尤雨溪在 petite-vue 的措辞,证实他确实有以上观点。
    • petite-vue avoids all this overhead by walking the existing DOM and attaching fine-grained reactive effects to the elements directly. The DOM is the template. This means petite-vue is much more efficient in progressive enhancement scenarios.
      This is also how Vue 1 worked. The trade-off here is that this approach is coupled to the DOM and thus not suitable for platform agnostic rendering or JavaScript SSR. We also lose the ability to work with render functions for advanced abstrations. However as you can probably tell, these capabilities are rarely needed in the context of progressive enhancement.
  • mobx-jsx:Solid 作者演示的在 JSX 中用 Mobx 细粒度的绑定 DOM 更新
  • JSX without a VDOM:Solid 作者在文档 FAQ 中直接反对尤雨溪,虽然没有那么直白

状态机制

Dirty Checking vs. Dependency tracking

对于非结构数据的变化,直接的 DOM 更新具有更好的性能。但对于结构数据变化(数组、对象等),无法知道具体哪些元素发生变化,仍然需要 Diff,要明确代码中的性能瓶颈在哪里。

Dirty checking:React、Angular

  • React 直接 VDOM 全量 Diff,除非组件使用 React.memo 才进行状态检查。
  • Angular 要全量检查组件上状态是否更改,对标记为 dirty 的分支做 Diff。

Dependency tracking:Solid、Vue

  • Solid 使用 Proxy 在 runtime 时进行细粒度的状态绑定。
  • Vue 使用 Proxy 在 runtime 时绑定状态到组件粒度,只做组件内的 Diff。

Svelte

  • Svelte 在编译时就确定了细粒度的状态绑定。

Immutable vs. Mutable

Immutable:React、Vue

immutable 可以优化脏检查性能,在 Diff 过程中进行剪枝,一般也只有在需要做脏检查的框架中推崇。

Mutable:Vue、Angular、Svelte、Solid

mutable 很棒,尤其是在表单业务中,天然的双向绑定在表单中是最好的解决方案。

AOT 的想象力

将更多的工作放到编译时做性能更高。例如 Svelte 在编译时替换状态修改代码,不用像 React 那样写代码时使用统一的入口,也不需要像 Vue 使用 Proxy 运行时 getter 绑定状态和组件的关系,并在 setter 时发出信号。既保证了 DX,也节省了 Proxy 的内存开销。

模版编译前后:

<a>{{ msg }}</a> # 模版
// 模版编译后 // 创建 DOM,并返回更新的回调函数,外部应该有绑定该回调和对应的状态 function renderMainFragment ( root, component, target ) { var a = document.createElement( 'a' ); var text = document.createTextNode( root.msg ); a.appendChild( text ); target.appendChild( a ) return { update: function ( changed, root ) { text.data = root.msg; }, teardown: function ( detach ) { if ( detach ) a.parentNode.removeChild( a ); } }; }

修改状态代码编译前后:

count += 1; // 编译前
//编译后,$$invalidate 设置该状态为修改过,触发该状态对应的回调,修改DOM count += 1; $$invalidate('count', count);
Vue 也可以参考这个做法,在编译时确定组件和状态的关系。

参考链接