2-3 前端框架的分类
Q:现代前端框架不仅仅是 Vue、React,还出现了像 Svelte、Solid.js 之类的框架,你觉得与这些新框架对比 React、Vue 有什么区别的?
自变量和因变量
现代前端框架有一个非常重要的特点,那就是基于状态的声明式渲染。如果要概括的话,可以使用一个公式: UI = f(state)。需要说明的是:(1) state 是当前视图的一个状态;(2) f 指的是框架内部的一个运行机制;(3) UI 指的是宿主环境的视图描述。
这里就和我们初中学过的一个数学代数知识非常类似了: x + 1 = y。 x 的变化会导致 y 的变化,x 就被称之为自变量,y 就被称之为因变量。因此类比上面的 UI 公式,state 就是自变量,state 发生变化最终会导致计算出来的 UI 也随之发生变化,那么 UI 就是因变量。
目前在 React 中有很多 Hook,例如下面的代码:
const [x, setX] = useState(0);
比如上面的代码我们就是定义了一个自变量
function App() {
const [x, setX] = useState(0);
return (<div onClick=() => {setX(x + 1)}>{x}</div>)
}
上面的 useState 这个 hook 可以看作是定义了一个自变量,自变量一变化,就会导致依赖它的因变量也发生变化,在上面的例子中,返回的 jsx 所描述的 UI 就是因变量。
因变量又可以分为两类:
- 没有副作用的因变量
- 有副作用的因变量
没有副作用的因变量
在 React 中,useMemo 就是定义一个没有副作用的因变量。
const y = useMemo(() => x * 2 + 1, [x]);
在上面的代码中,我们定义了一个没有副作用的因变量 y,y 的值取决于 x 的值,x 的值一变化,y 的值也会随着变化。
有副作用的因变量
在 React,可以使用 useEffect 来定义一个有副作用的因变量。
useEffect(() => (document.title = x), [x]);
上面的代码依赖于 x 的变化,当 x 发生变化的时候,会修改页面的标题,这就是一个副作用的操作。
那么接下来,我们来总结一下:自变量的变化,会导致三种情况的因变量发生变化。
-
- 自变量的变化,导致 UI 因变量变化。
function Counter() {
const [num, setNum] = useState(0);
return <div onClick={() => setNum(num + 1)}>{num}</div>;
}
-
- 自变量的变化,导致无副作用的因变量发生变化
function Counter() {
const [num, setNum] = useState(0);
const fixedNum = useMemo(() => num.toFixed(2), [num]);
return <div onClick={() => setNum(num + 1)}>{fixedNum}</div>;
}
-
- 自变量的变化,导致有副作用的因变量发生变化
function Counter() {
const [num, setNum] = useState(0);
useEffect(() => (document.title = num), [num]);
return <div onClick={() => setNum(num + 1)}>{num}</div>;
}
框架的分类
上面介绍了自变量和因变量, state 就是自变量, 自变量的变化直接或间接的改变了 UI,上面的公式实际上还可以分为两个步骤,这在后面的源码篇会经常用到。
-
- 根据自变量 state 计算出 UI 的变化。
-
- 根据 UI 的变化执行具体宿主环境的 API。
以前端工程师最熟悉的浏览器而言,那么第二个步骤就是执行 DOM 相关的 API,对于这个步骤来讲,不同的框架在实现上基本上是相同的,这个步骤不能作为框架分类的依据,差别主要体现在步骤一上面,步骤一也是针对各种框架进行分类的一个依据。
接下来我们一个应用示例:
该应用由三个组件组成
A 组件是整个应用的根组件,在这个组件中,有一个自变量 a,a 的变化会导致 UI 的重新渲染。
上图表示在 A 组件中引入了一个因变量 b, A 组件中的自变量 a 的改变会导致因变量 b 的改变,而这个因变量 b 又作为 prop 传递到了子组件 B 中。 B 组件中也有一个因变量 c,在该组件中还接收从父组件 A 传递过来的 props b,最终在 UI 中渲染 b + c。
在组件 C 中,接收从根组件 A 传递过来的数据 a,从而 a 变成 C 组件的一个自变量。
接下来,我们总结一下各个组件中包含的因变量:
- A 组件: 自变量 a & a 的因变量 b
- B 组件: 从 A 组件传递过来的自变量 b & 自变量 c
- C 组件: 从 A 组件传递过来的自变量 a
理清楚自变量之后,我们就可以从三个维度去整理自变量和不同维度之间的关系。
自变量与 UI 的对应关系
从 UI 层面去考虑的话,自变量的变化会导致哪些 UI 发生变化?
- a 变化导致 A 的 UI 中的 {a} 发生变化;
- a 变化导致因变量 b 的变化,导致 B 的 UI 中的 {b + c} 变化;
- a 变化导致 C 的 UI 中的 {a} 发生变化;
- a 变化导致 C 的 UI 中的 {a.toFixed(2)} 发生变化;
- c 变化导致 B 的 UI 中的 {b + c} 变化;
总共我们梳理出来的 UI 变化路径有 5 条,接下来我们要做的事情就是根据梳理出来的变化路径执行具体的 DOM 操作即可。
自变量与组件的对应关系
从组件的层面去考虑的话,自变量的变化会导致哪些组件发生变化呢?
- a 变化导致 A 组件 UI 变化;
- a 变化导致 b 变化,从而导致 B 组件的 UI 变化;
- a 变化导致组件 C 的 UI 变化;
- c 变化导致组件 B 的 UI 变化;
相较于上面自变量与 UI 的对应关系,当我们考虑自变量与组件之间的关系时,梳理出来的路径从 5 条变成了 4 条。虽然路径减少了,但是在运行的时候,需要增加额外的操作,就是确定某一个组件发生变化时,组件内部的 UI 需要发生变化的部分。例如,通过路径 4 只能明确 B 组件发生了变化,但是具体发生了什么变化,还需要组件内部进行进一步的确认。
自变量与应用的对应关系
最后,我们考虑自变量和应用之间的关系,那么路径就变成了:
- a 变化导致应用中发生 UI 变化;
- c 变化导致应用中发生 UI 变化;
整体路径从 4 条减少到 2 条,虽然路径减少了,但是要做的额外工作更多了。比如,a 的变化会导致应用中的 UI 发生变化,那么究竟是哪一部分的 UI 发生了变化?这些需要额外的进行确认。
最后我们可以总结一下,前端框架需要关注自变量和X(UI、组件、应用统称为 X)的对应关系,随着 X 的抽象层级不断下降,自变量到 UI 变化的路径条数就会增多。路径越多,则意味着前端框架在运行时消耗在“寻找自变量与 UI 对应关系”上面的时间越少。
根据上面的特点,我们就可以针对现代框架分为三大类: 元素级框架、组件级框架、应用级框架。
以常见的前端框架为例,React 属于应用级框架,Vue 属于组件级框架,而新的 Svelte、Solid.js 属于元素级框架。
最后总结:所有的现代前端框架,都有一个非常重要的特点,那就是“基于状态的声明式渲染”。概括成一个公式的话,那就是
UI = f(state)。 这一点类似于初中数学中自变量和因变量之间的关系。例如在上面的公式中,state 就是一个自变量,state 的变化会导致 UI 这个因变量发生变化。不同的框架,在根据自变量(state)的变化计算出 UI 的变化,这一步骤有所区别,和X(UI、组件、应用统称为 X)的对应关系,随着 X 的抽象层级不断下降,自变量到 UI 变化的路径条数就会增多。路径越多,则意味着前端框架在运行时消耗在“寻找自变量与 UI 对应关系”上面的时间越少。
以“以与自变量建立对应关系的抽象层级”可以作为其分类的依据,按照这个标准可以分为以下三类:
- 元素级框架:Svelte、Solid.js
- 组件级框架:Vue
- 应用级框架:React