react的思想和数据流

发布时间 2023-04-17 19:31:14作者: www159

最近忙着写前端界面,粗略讨论以下 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的状态模型,就没有这种写法。但是单页应用为了兼顾开销和延迟不得不在后期方位这些副作用。

pageUI双向绑定,是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更新