最近忙着写前端界面,粗略讨论以下 react 的函数式编程思想和组件通信的应对思路。
纯函数和副作用
函数式编程中函数是一等公民。一个函数的返回值只取决于输入参数时,那么这个函数的行为是确定
的,我们称之为纯函数
。那么反过来,如果函数的输入参数相同,而返回值不确定,那么该函数就是有副作用的
,是不纯的
。举几个例子
// 纯函数
const add = (a) => a + 1;
// 有副作用,修改了全局变量,且返回值依赖全局变量。
let a = 0;
const add = (b) => b + a++;
// `read`有副作用,文件就相当于`全局变量`,而write可以随时修改这个`全局变量`
const write = (path) => sync(path);
const read = (path) => fileOf(path);
// `fetchUser`有副作用,因为`putUser`可以修改服务端的用户数据,
// 每次调用`fetUser`的结果一定是不一致的
const putUser(userForm) => fetch("put:/some/uri", userForm);
const fetchUser(userForm) => fetch("/some/uri", userForm);
// 有副作用
// 当用户操作了input,那么函数输出的UI和之前的UI就是不一样的。
function Component() {
const [state, setState] = useState(init);
return (
<input
value={state}
onCange={(e.target) => setState(e.target.value)}
/>
)
}
// useState实际上是设置了一个全局变量
let _state;
function useState(init) {
if(!_state) = _state = init;
return [_state, setState];
}
function setState(val) {
if(_state !== val) _state = val;
render(Component);
}
function Component() {
return (
<input
value={getter}
onCange={(e.target) => setter(e.target.value)}
/>
)
}
总结几个副作用:
IO
磁盘和内存就相当于代码上下文的全局变量,当涉及到同一块内存的读和写,必定会有副作用
网络
服务器的数据库就像于代码上下文的全局变量,当涉及到同一个表项的读和写,必定会有副作用
UI
用户操作的 UI 组件需要一个全局状态模型来记录 UI 中显示的状态,当涉及到同一个状态的读和写,必定会有副作用。
因此我们可以轻易总结出来,当函数内修改的变量超过了函数的生命周期,那么该函数一定是有副作用的。
读副作用
react
中的每一个state
都是全局生命周期变量,对每次产生的UI
都有副作用。但是在函数组件内部执行的过程中,需要和外部的全局变量同步,那么就需要 useEffect(),下面是例子
function Pagination() {
const [page, setPage] = useState(1);
const [content, setContent] = useState<string>(init);
useEffect(() => {
const body = fetch(`/fetch/content?offset=${page}`);
setContent(body.content);
}, [page])
return (
<p>{content}</p>
{pageIndex.map((index) => (
<div onClick={() => setPage(index)}>
{index}
</div>
))}
)
}
理论上如果一开始就获取所有的内容,那么副作用就从服务器转移到了react的状态模型,就没有这种写法。但是单页应用为了兼顾开销和延迟不得不在后期方位这些副作用。
page
和UI
双向绑定,是UI
副作用变量。我们需要通过page
来和服务器同步content
,那么这里的content
则是一个缓冲区,一个临时变量,即读副作用。上边的写法经过编译将产生如下代码:
let _page;
function usePage(init)...
function setPage(val)...
let _content; // 应该是局部缓冲区,但是被用作全局。
...
function Pacination() {
...
}
理性的写法应当如下:
function Pagination() {
const [page, setPage] = useState(1);
let content = ""; // 缓冲区内置。
useEffect(async () => {
const body = await fetch(`/fetch/content?offset=${page}`);
content = body.content;
render(Pagination) // 由于useEffect 在render函数之后执行,
// 我们需要自己render
}, [page])
...
}
开销
这时候肯定有人会问:setContent 修改和 setPage 为何不能合并呢?答案:得益于react
的虚拟dom
,每次render
是差值更新而非全量更新。因此多次渲染的开销理论上很少。
全局统一状态
这时候就有人想到了,如果我将 page 和 content 合体,放在一个大 state 里,那么每次不就能只setState
一次了吗?这就是 redux 等状态管理库的由来。通过将UI
不可更改的state
(或者依赖的state
,通常为局部缓冲区)包装起来。
读写副作用
即读和写的变量都在react
外部,考虑到一个读写副作用的例子:
function UserSettings() {
const [username, setUsername] = useState("");
useEffect(() => {
const body = fetch("some/uri");
setUsername(body.username);
}, []);
return (
<>
<input
value={username}
onChange={(e) => setUsername(e.currentTarget.value)}
/>
<button onClick={() => {
fetch(`put:some/uri?username=${username}`);
}} />
</>
)
}
显然这里的username
不仅和UI
双向绑定,还和服务器
双向绑定。但是显然UI
和服务器是不同步的,因为即使我们本地修改了username
并点击了按钮,如果服务器没有更新的的话,那么刷新之后username
又变了回去。我的看法是UI
端无需对后端的一致性负责,username
只能负责读缓冲区,而不能负责同步。因此大可不必纠结单页应用的一致性比多页应用负担大。
单向数据流
react
通过组件化来分解大量缓冲区state
的耦合。
读副作用
拿上边的pagination举例
function Pagination() {
const [page, setPage] = useState(1);
const [content, setContent] = useState<string>(init);
useEffect(() => {
const body = fetch(`/fetch/content?offset=${page}`);
setContent(body.content);
}, [page])
return (
<PageContent page={page} />
{pageIndex.map((index) => (
<div onClick={() => setPage(index)}>
{index}
</div>
))}
)
}
function PageContent({ page }) {
const [content, setContent];
useEffect(() => {
const body = fetch(`/fetch/content?offset=${page}`);
setContent(body.content);
}, [page]);
return <p>{content}</p>
}
这样page就成了函数参数,我们将PageContent改造成了一个纯函数,缓冲区就被下方到这个组件中。page更新
标签:content,const,思想,react,副作用,state,UI,数据流,page From: https://www.cnblogs.com/www159/p/17323988.html