Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • Plan 计划
  • typescript-utility

  • single-spa源码

  • qiankun源码

  • webpack

  • axios

  • solid

    • 开始上手
    • 计划跟踪
    • 渲染

      • 渲染原理之组件结构与 JSX 编译
        • 目录
        • JSX 再认识
  • vite源码

  • jquery源码

  • snabbdom

  • am-editor

  • html2canvas

  • express

  • acorn源码

  • immutable.js源码

  • web
  • solid
  • 渲染
jonsam
2022-09-07
目录

渲染原理之组件结构与 JSX 编译

# 目录

  • 目录
  • JSX 再认识
    • React 中的 JSX
    • SolidJS 中的 JSX
    • 基于 VDOM 的框架之特性
    • 为什么使用 VDOM?
    • VDOM 的局限性
    • VDOM 造成的富应用化
    • SolidJS 的富组件化
    • 响应式系统与组件的解耦

# JSX 再认识

# React 中的 JSX

SolidJS 和 React 一样也是采用了 JSX 来作为组件的定义语言。JSX 我们都很熟悉了,在 React 之中,JSX 被作为是 ReactElement 的描述,每一个节点被 babel 编译为 createElement 的语法糖,类似于 h 函数。这可能给我们带来了一些固定思维,认为 JSX 就是用来描述节点的树形结构的,其实不然,JSX 作为一种组件的描述语言,它能够为编译工具提供足够的灵活性,也就是说,他也可以达到类似于 SFC 的静态分析的效果。

# SolidJS 中的 JSX

我们先来看下 SolidJS 中 JSX 的用法:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app")!);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这是官方的一个案例,参见:Solid Playground (opens new window),注意 COMPILE MODE 选择 Client side rendering ,即 CSR。

可以看到,SolidJS 可以提供 React 类似的语法。SolidJS 部分灵感来源于 React,但是其原理却与 React 与天壤之别,更进一步说,SolidJS 的响应式系统更接近于 Vue,而其去除 VDOM、细粒度更新的特性则更接近于 Svelte。

现在我们来看下上述的代码在 babel-preset-solid (opens new window) 的编译下的产物(output):

import { render, createComponent, delegateEvents, insert, template } from 'solid-js/web';
import { createSignal } from 'solid-js';

const _tmpl$ = /*#__PURE__*/template(`<button type="button"></button>`, 2);

function Counter() {
  const [count, setCount] = createSignal(0);

  const increment = () => setCount(count() + 1);

  return (() => {
    const _el$ = _tmpl$.cloneNode(true);

    _el$.$$click = increment;

    insert(_el$, count);

    return _el$;
  })();
}

render(() => createComponent(Counter, {}), document.getElementById("app"));

delegateEvents(["click"]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

最显著的特点是,JSX 并不创建类似于嵌套的 h 函数所构成的 VDOM 产物,而是更接近于一种基于 DOM 模板的静态分析。我们可以将上述的代码块归纳成如下的几个部分:

  • DOM 静态模板
  • 响应式系统
  • 事件委托系统

DOM 静态模板即 _tmpl$ ,这是组件初始化时静态的 DOM 节点,通过 document.createElement("template") 的方法创建 DOM 节点,参见:template (opens new window)。处理静态模板是在编译时处理的,并且通过静态提升的方式进行优化。

响应式系统即以 createSignal 为代表的响应式 API 所构成的内容,目的是以发布订阅模式思想为指导以代理方式为方法实现响应式,在 “信号” 发生变化时触发收集到的副作用,并且更新 DOM 节点。

事件委托系统即 delegateEvents ,参见 delegateEvents (opens new window),目的是通过事件委托的方法提升事件处理的效率,事件则统一委托在 document 上。这一点与 React 类似,不过 React 已经将事件委托节点由 document 改成了 FiberRoot 上,参考:React 17 attaches events to the root DOM container instead of the document node (opens new window)。事件委托在 document 当然会有一些安全隐患,因为 document 是 React 应用所无法完全控制的范围,但是委托到 FiberRoot(即 container)上也会存在一些问题,比如连续的拖拽事件将会出现拖拽对象无法跟随鼠标的情况,在 React 中利用原生 mousemove 实现拖拽功能便会遇到这一问题。

在后文的解析之中我们会详细的探讨这几个部分的内容,但是现在,我们不妨转换视角,思考下为什么 SolidJS 需要这么做,以及这么做能够有什么好处?要分析这个问题,我们先从 VDOM 框架说起,探讨一下基于 VDOM 原理的框架之共同特性。

# 基于 VDOM 的框架之特性

基于 VDOM 的框架在组件的编译上会具有一些共性,简述如下:

  • 嵌套的 h 函数。
  • render 函数在每次渲染时执行。

所谓 h 函数,即为 “渲染函数”,是指通过数据结构描述的方式获取虚拟节点的函数。如 React 中的 createElement 、vue 中的 _createElementVNode (参见:Vue SFC Playground (opens new window)) 等。

所谓 render 函数,是能够表征组件的视图、事件、状态的函数,通过执行 render 函数,可以获取最新状态的虚拟节点树。如 React 类组件的 Class.render 函数、函数式组件本身,vue 组件的 setup 函数。

我们可以深入地考虑为什么基于 VDOM 的框架会具有这样的特性,其实不难理解:

  • 渲染函数为 VNode 的创建提供了便捷性,模板的解析依赖渲染函数。
  • 创建状态和数据随时间变化的组件。VDOM 以及 DIFF 算法的目的就是在状态发生变化时通过对比 VDOM 来更新视图,因此组件必须能够表征出组件最新的状态和视图。

参考:

  • 渲染机制 | Vue.js (opens new window)
  • 渲染函数 & JSX | Vue.js (opens new window)

# 为什么使用 VDOM?

决定了 SolidJS 的 JSX 解析与 React 大有不同的原因是因为 SolidJS 去除了 VDOM 层,这导致了 SolidJS 中响应式系统的策略的改变。VDOM 的思路来源于 React,目的是提升节点 DIFF 的效率并且降低成本。为什么要使用 VDOM 呢?这就不得不谈到 VDOM 所具有的优势:

  • 降低 DIFF 成本。直接 DIFF DOM 节点的成本简直无法想象,而 VDOM 作为 JavaScript 可控的数据结构,则可大大降低 DIFF 的成本。同时,一系列的优化手段也可使 DIFF 成本进一步降低,如更高效的 DIFF 算法,React 的 EffectTag 处理或者 Vue 的模板静态分析、静态提升等手段。
  • 构建节点防腐层。框架不可能只使用 Web 这一种使用场景,在适配各种不同的渲染场景时,VDOM 则可作为防腐层存在,适配不同的节点渲染规则。
  • 声明式的、状态驱动的 UI 开发体验( declarative, state-driven UI development )。无论是 React 还是 Vue 都强调自己的这两个特性,当然这与 VDOM 机制不可分离。这使得我们在编写组件时更高效的处理状态的变化和视图的呈现,而不用关注视图更新的时机和原理。

# VDOM 的局限性

同时 VDOM 自然也带来了一些局限性,如:

  • 大量的 DIFF (包括无效的 DIFF)造成成本的提升。
  • 提升 DIFF 效率的复杂性构成应用的效率瓶颈。

无论是应用级别的 DIFF 还是组件级别的 DIFF,其实都提升了响应式系统的复杂性,因为响应式的更新是离散的、细粒度的,而 VDOM 的方式则无疑将这种更新的影响范围扩散到整个应用或者组件,这种影响范围的放大造成我们想要细粒度的 DOM 更新变得异常复杂。您可以从下图中体会这一点:

vdom_update

参考:

  • Svelte Blog: Virtual DOM is pure overhead (opens new window)
  • React vs. Svelte: The War Between Virtual and Real DOM | by Keshav Kumaresan | Bits and Pieces (opens new window)
  • Incremental vs Virtual DOM. Will Incremental DOM Replace Virtual… | by Chameera Dulanga | Bits and Pieces (opens new window)

# VDOM 造成的富应用化

VDOM 会使应用变得更 “重”,这在很大程度上是因为 VDOM 的粗粒度所造成的。我们可以进一步思考 VDOM 应用具体会重在哪里:

  • VDOM 系统和 DIFF 算法,VDOM 系统包含了渲染函数和模板、VNode 的内存占用、DIFF 算法及其繁琐的优化策略、VNode 转化为 DOM 节点变更的 mutation 操作等。
  • VDOM 的 “庞大的” 运行时。VDOM 思想本质上还是在实践轻编译重运行的理念,因此庞大的 VDOM 系统必然会包含在运行时中,成为线上产品的一大 “负担”。

VDOM 的粗粒度是一种权衡( tradeoff ),VDOM 获得了更多的节点控制权,却在一定程度上违背了细粒度更新的规则,因为无论 VDOM 系统是应用层面的还是组件层面的,都要为细粒度更新 DOM 的优化而付出巨大的代价。同时,VDOM 也将响应式系统和组件进行了捆绑,因为我们总是认为状态是属于组件的,但是从整个应用的更新来看,响应式系统是完全可以与组件进行解耦的。后文会详述这一问题。

这种让应用程序越来越重的倾向,我称之为 “富应用化”。

# SolidJS 的富组件化

SolidJS 去除了 VDOM 系统,这使得离散的、细粒度的状态变化可以直接通过响应式系统与细粒度的 DOM 更新进行对应,无论这种 DOM 更新是可调度的还是无调度的,这在一定程度上解决了 VDOM 所造成的的应用的 overhead 。

我们可以思考去除 VDOM 所带来的直接影响:

  • 更轻量的应用运行时,更高效的组件更新效率。去除 VDOM 将直接减少运行时的体积,没有了 DIFF 的过程也将大大提升组件更新的效率。
  • 更轻的框架成本,更少的框架倾入性,可插拔的框架模块。相比于 React,solidjs 要轻量的多,我们可以从源码中直接体会到。没有 VDOM,框架对于节点将减少很多的倾入性,这将使得 DOM 的更新更加贴近于原生的 DOM 操作。框架模块之间可以相互解耦,如响应式系统、事件系统、组件结构等,这将使得框架的可插拔性更好,可以轻易地与其他框架配合使用。
  • “重编译而轻运行” 的趋势。将更多的成本转移到编译时,减轻运行时的负担。

这一切都将导致 SolidJS 的 “富组件化” 的倾向!

所谓 “富组件化”,即是将应用的成本中心由 “应用” 转移到 “组件” 中。我们可以观察上文 SolidJS JSX 的编译结果,以推测去除 VDOM 后的 SolidJS 是如何实现 DOM 的更新的。

毫无疑问,这种因状态更新而更新 DOM 的成本是转移到了组件上。在 SolidJS 中,响应式系统在注入组件时,会将更新 DOM 节点作为副作用的一部分。显然这部分的副作用虽然有着良好的性能,但由于其复杂性,成本也会随着项目的规模增长而变得更大。如果单独从组件的角度来看,组件已经具有了

# 响应式系统与组件的解耦

参考:

  • Sawtaytoes/reactjs-solidjs-bridge: Render Solid.js components in React.js and visa versa. (opens new window)
  • Solid.js feels like what I always wanted React to be | TypeOfNaN (opens new window)
  • SolidJS: Reactivity to Rendering - JavaScript inDepth (opens new window)
编辑 (opens new window)
上次更新: 2022/09/14, 18:44:08
计划跟踪
开始阅读

← 计划跟踪 开始阅读→

最近更新
01
计划跟踪
09-06
02
开始上手
09-06
03
带着原理重读 React 官方文档(一)
08-23
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式