代数效应与React
React核心团队成员Sebastian Markbåge[1](React Hooks
的发明者)曾说:我们在React
中做的就是践行代数效应
(Algebraic Effects)。
那么,代数效应
是什么呢?他和React
有什么关系呢。
什么是代数效应
代数效应
是函数式编程
中的一个概念,用于将副作用
从函数
调用中分离。
接下来我们用虚构的语法
来解释。
假设我们有一个函数getTotalPicNum
,传入2个用户名称
后,分别查找该用户在平台保存的图片数量,最后将图片数量相加后返回。
function getTotalPicNum(user1, user2) {
const num1 = getPicNum(user1);
const num2 = getPicNum(user2);
return picNum1 + picNum2;
}
在getTotalPicNum
中,我们不关注getPicNum
的实现,只在乎“获取到两个数字后将他们相加的结果返回”这一过程。
接下来我们来实现getPicNum
。
"用户在平台保存的图片数量"是保存在服务器中的。所以,为了获取该值,我们需要发起异步请求。
为了尽量保持getTotalPicNum
的调用方式不变,我们首先想到了使用async await
:
async function getTotalPicNum(user1, user2) {
const num1 = await getPicNum(user1);
const num2 = await getPicNum(user2);
return picNum1 + picNum2;
}
但是,async await
是有传染性
的 —— 当一个函数变为async
后,这意味着调用他的函数也需要是async
,这破坏了getTotalPicNum
的同步特性。
有没有什么办法能保持getTotalPicNum
保持现有调用方式不变的情况下实现异步请求呢?
没有。不过我们可以虚构
一个。
我们虚构一个类似try...catch
的语法 —— try...handle
与两个操作符perform
、resume
。
function getPicNum(name) {
const picNum = perform name;
return picNum;
}
try {
getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
switch (who) {
case 'kaSong':
resume with 230;
case 'xiaoMing':
resume with 122;
default:
resume with 0;
}
}
当执行到getTotalPicNum
内部的getPicNum
方法时,会执行perform name
。
此时函数调用栈会从getPicNum
方法内跳出,被最近一个try...handle
捕获。类似throw Error
后被最近一个try...catch
捕获。
类似throw Error
后Error
会作为catch
的参数,perform name
后name
会作为handle
的参数。
与try...catch
最大的不同在于:当Error
被catch
捕获后,之前的调用栈就销毁了。而handle
执行resume
后会回到之前perform
的调用栈。
对于case 'kaSong'
,执行完resume with 230;
后调用栈会回到getPicNum
,此时picNum === 230
再次申明,
try...handle
的语法是虚构的,只是为了演示代数效应
的思想。
总结一下:代数效应
能够将副作用
(例子中为请求图片数量
)从函数逻辑中分离,使函数关注点保持纯粹。
并且,从例子中可以看出,perform resume
不需要区分同步异步。
代数效应在React中的应用
那么代数效应
与React
有什么关系呢?最明显的例子就是Hooks
。
对于类似useState
、useReducer
、useRef
这样的Hook
,我们不需要关注FunctionComponent
的state
在Hook
中是如何保存的,React
会为我们处理。
我们只需要假设useState
返回的是我们想要的state
,并编写业务逻辑就行。
function App() {
const [num, updateNum] = useState(0);
return (
<button onClick={() => updateNum(num => num + 1)}>{num}</button>
)
}
如果这个例子还不够明显,可以看看官方的Suspense Demo[2]
在Demo
中ProfileDetails
用于展示用户名称
。而用户名称
是异步请求
的。
但是Demo
中完全是同步
的写法。
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
代数效应与Generator
从React15
到React16
,协调器(Reconciler
)重构的一大目的是:将老的同步更新
的架构变为异步可中断更新
。
异步可中断更新
可以理解为:更新
在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。
这就是代数效应
中try...handle
的作用。
其实,浏览器原生就支持类似的实现,这就是Generator
。
但是Generator
的一些缺陷使React
团队放弃了他:
- 类似
async
,Generator
也是传染性
的,使用了Generator
则上下文的其他函数也需要作出改变。这样心智负担比较重。 -
Generator
执行的中间状态
是上下文关联的。
考虑如下例子:
function* doWork(A, B, C) {
var x = doExpensiveWorkA(A);
yield;
var y = x + doExpensiveWorkB(B);
yield;
var z = y + doExpensiveWorkC(C);
return z;
}
每当浏览器有空闲时间都会依次执行其中一个doExpensiveWork
,当时间用尽则会中断,当再次恢复时会从中断位置继续执行。
只考虑“单一优先级任务的中断与继续”情况下Generator
可以很好的实现异步可中断更新
。
但是当我们考虑“高优先级任务插队”的情况,如果此时已经完成doExpensiveWorkA
与doExpensiveWorkB
计算出x
与y
。
此时B
组件接收到一个高优更新
,由于Generator
执行的中间状态
是上下文关联的的,所以重新计算y
时无法复用之前已经计算出的x
,需要重新计算。
如果通过全局变量
保存之前执行的中间状态
,又会引入新的复杂度。
更详细的解释可以参考这个issue[3]
基于这些原因,React
没有采用Generator
实现协调器
。
代数效应与Fiber
Fiber
并不是计算机术语中的新名词,他的中文翻译叫做纤程
,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。
在很多文章中将纤程
理解为协程
的一种实现。在JS
中,协程
的实现便是Generator
。
所以,我们可以将纤程
(Fiber)、协程
(Generator)理解为代数效应
思想在JS
中的体现。
React Fiber
可以理解为:
React
内部实现的一套状态更新机制。支持任务不同优先级
,可中断与恢复,并且恢复后可以复用之前的中间状态
。
其中每个任务更新单元为React Element
对应的Fiber节点
。
参考资料
[1]
Sebastian Markbåge: https://github.com/sebmarkbage/
[2]
Suspense Demo: https://codesandbox.io/s/frosty-hermann-bztrp?file=/src/index.js:152-160
[3]
这个issue: https://github.com/facebook/react/issues/7942#issuecomment-254987818
- MyBatis框架(三)动态SQL,分页,二进制存入数据库图片
- Spring框架(二)反射机制, 注入, 单例模式, 自动装载, 延迟加载
- Spring框架(三) JDBCTemplate,声明式事务,自动装载(注解)
- SpringMVC 常用注解
- Spring3:AOP
- js中三目运算符和&& || 符的个人浅见
- 原生js怎么为动态生成的标签添加各种事件
- 实现图片懒加载
- SpringMVC框架(四)文件的上传下载,上下文路径
- SpringMVC框架(一)
- HTML link标签media参数
- Mybatis,Spring,SpringMVC框架面试题
- 腾讯云容器服务集群中安装 dashboard ui
- SSM框架整合项目 :投票系统
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Spring系列之aAOP AOP是什么?+xml方式实现aop+注解方式实现aop
- 线上工程启动日志不滚动了——通过查看堆栈信息排查的过程(ES批量插入)
- Java中使用方法的注意事项
- 微信小程序转发朋友圈详解
- Error: Protocol error, got "H" as reply type byte
- 树莓派基础实验33:TCRT5000红外循迹传感器实验
- 10W个Java对象有多大
- 一次线程池引发的线上故障分析
- dubbo 启动Failed to save registry store file报错
- 大数据的列式存储格式:Parquet
- springBoot 入门(六)—— 整合Spring框架开启自带的任务调度器执行任务(注解方式)
- java字节流入门(缓冲输出流)
- EsotericSoftware Kryo —— 官方(1)
- Java的I/O类库的基本架构一句话介绍
- 树莓派综合项目1:智能温度测量系统实验