React v16 源码分析 ② 设计理念

状态渲染 UI

1
UI = react(state);

React 程序设计哲学

  • 将设计好的 UI 划分为组件层级
  • 确定 UI state 的最小(且完整)表示
  • 确定 state 放置的位置
  • 添加反向数据流,低层层级组件更新高层级组件状态

使用组合而不是继承

Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

Fiber

Fiber 其实就是 Virtual DOM 的一种实现,相比于通过 React.createElement 创建的 Virtual DOM,Fiber 在此基础上添加了更多的属性,例如 return, current 等指针,用于将 Fiber 对象链接为链表。最终形成一颗树状结构,也就是 Fiber 树,他对应着真实 DOM 树的结构。

而 Fiber 对象上的属性还不止这些,还有像 updateQueue 更新队列等属性,但到目前位置知道 Fiber 是对 DOM 树的一种描述,已经足够了。而让 React 设计 Fiber 的原因,则是因为下面的协调过程。

协调 reconciler

这一概念应该是当我们对 React 执行过程深入思考的时候最容易想到的一部分,通过 JSX 创建的 Virtual DOM 如何与真实的 DOM 同步,真实 DOM 属性改变的时候,又如何被记录到 Virtual DOM 上,这个过程就叫做协调

reconciler 模块,用于处理协调相关的事务。Diff 算法也在这个期间发生。

React15 之前的协调过程是同步的,也叫 stack reconciler。

JS 的执行是单线程的,由于浏览器器触发的事件(用户交互触发的事件回调)是一个宏任务,所以会等待同步任务执行完成,在更新比较耗时的任务时,会阻塞用户的交互。

也许会考虑将耗时任务放到异步任务中执行,但最终还是会回到主线程中执行,所以比较好的解决办法就是任务分割,当其他优先级比较高的任务到来时,将正在执行的任务打断让出执行权。之后再从中断的部分开始异步执行剩下的计算。

为了将老的同步更新的架构变为异步可中断更新,所以需要一套数据结构让它既能对应真实的 dom 又能作为分隔的单元,这就是 Fiber。

Scheduler

有了 Fiber,就需要用浏览器的时间片异步执行这些 Fiber 的工作单元,浏览器有一个 api 叫做 requestIdleCallback,它可以在浏览器空闲的时候执行一些任务,我们用这个 api 执行 react 的更新,让高优先级的任务优先响应不就可以了吗,但事实是 requestIdleCallback 存在着浏览器的兼容性和触发不稳定的问题,所以我们需要用 js 实现一套时间片运行的机制,在 react 中这部分叫做 scheduler。

下面用伪代码理解一下 分割,异步执行,让出执行权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let firstFiber; // 代表Fiber树的头节点
let nextFiber = firstFiber; // 用于遍历子节点

function performUnitOfWork() {
// 处理节点相关逻辑
return nextFiber.next; // 返回下一个节点
}

function workLoop(deadline) {
while (nextFiber && !shouldYield) {
nextFiber = performUnitOfWork();
// 如果没有剩余时间处理下一个节点
// 则暂停执行,让出主线程,给优先级更高的任务
shouldYield = deadline < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);

为什么不使用统的异步控制:

  • setTimeout setTimeout 在嵌套超过 5 层之后有默认 4ms 的延时
  • requestFrameAnimation 执行时机不确定,chrome 和 firefox 是在渲染前执行,safari 是在渲染之后执行。
  • promise 微任务会在主进程执行结束后释放掉有所得微任务,不能控制什么时候需要执行。

Lane

有了异步调度,我们还需要细粒度的管理各个任务的优先级,让高优先级的任务优先执行,各个 Fiber 工作单元还能比较优先级,相同优先级的任务可以一起更新。

代数效应

(algebraic effects) 可能翻译成 可以当做参数传递的副作用 更容易理解。 它是函数式编程中的一个概念,用于将副作用从函数调用中分离。

从实用的角度上举例,假如我们有这样一段代码,其主要目的是进行一大段精妙的运算:

1
2
3
4
5
6
7
async function biz(id) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = await getInfo(infoId); // 副作用,与 server 通信
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = getData(dataId); // 副作用,非幂等操作
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
}

尽管运算逻辑很优美,但美中不足的是有两段副作用,导致它不能成为一个干净的纯函数被单元测试。而且这里会导致严重的逻辑耦合:『做什么』与『怎么做』没有拆的很干净:你的一大段计算逻辑是在处理做什么;两个副作用更关心怎么做:比如线上是接口调用,单测里是 mock 数据;但是由于这两块副作用代码,导致整个糅杂的逻辑都无法复用。直接把两个副作用传进来不就行了?

1
2
3
4
5
6
7
async function biz(id, getInfo, getData) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = await getInfo(infoId); // 副作用,与 server 通信
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = getData(dataId); // 副作用,非幂等操作
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
}

是的,这样确实可以复用,但还有一个叫函数染色的问题没有解决:明明是一大段干净的同步运算逻辑,因为 getInfo 是异步的,导致整个函数都得加个 async。而且很有可能在我单元测试里,这个 getInfo 是直接同步取内存数据,还得因此弄个 Promise……这时候如果 JS 里有这样一种语法就好了:

当函数执行到perform的时候,会被暂停,并被handle捕获,当异步执行的结果被返回,函数在继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

function biz(id) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = perform { type: 'getInfo', payload: infoId };
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = perform { type: 'getData', payload: dataId };
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
}

// 正常业务逻辑
async function runBiz() {
try {
biz();
} handle(effect) {
if (effect.type === 'getInfo') {
resume await getInfo(effect.payload);
} else if (effect.type === 'getData') {
resume await getData(effect.payload)
}
}
}

// 单元测试逻辑
function testBiz() {
try {
biz();
} handle(effect) {
if (effect.type === 'getInfo') {
resume testInfo;
} else if (effect.type === 'getData') {
resume testData;
}
}
}

分离副作用在函数编程中非常常见,redux-saga也会将副作用分离出来,只负责发起请求

1
2
3
4
5
6
7
8
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload);
yield put({ type: "SUCCESS", user: user });
} catch (err) {
yield put({ type: "ERROR" });
}
}

这样业务逻辑代码即摆脱了副作用,完成了做什么与怎么做的解耦;又完全不必担心异步副作用带来的染色问题,可以愉快的单测和复用了。Suspense 也是这种概念的延伸:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ProductResource = createResource(fetchProduct);

const Product = (props) => {
const p = ProductResource.read(
// 用同步的方式来编写异步代码!
props.id
);
return <h3>{p.price}</h3>;
};

function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Product id={123} />
</Suspense>
</div>
);
}

可以看到 ProductResource.read 完全是同步的写法,把获取数据的部分完全分离出了 Product 组件之外。在源码中, ProductResource.read 会在获取数据之前会 throw 一个特殊的 Promise, 由于 scheduler 的存在, scheduler 可以捕获这个 promise,暂停更新等数据获取之后交还执行权。ProductResource 可以是 localStorage 甚至是 redismysql 等数据库,也就是组件即服务,可能以后会有 server Component 的出现。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信