在讲解editflow相关设计之前,先来熟悉一下React Hooks。
React Hooks的函数式思维
官方文档中Hooks的例程非常简单:
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
通过useState
就可以在函数组件中“使用状态”。“状态”对于函数来说是本应是一个副作用,Hooks则为纯函数带来了使用各式各样的副作用的方案。
什么叫副作用呢?
数学里面的函数,或者纯函数式编程里面的函数,对于确定的输入/参数,函数返回/输出的结果也应当是确定的。
对于React函数式组件而言,props
是输入,return
的Virutal DOM则是输出。对于确定的props
,组件的内容和样式应当是确定不变的。
但是像React组件这种UI的组成部分,往往需要根据一些外部状态,改变其展现的界面内容或样式。也就是说,即使输入的props
保持一致,组件的输出结果仍然可能发生变化。
就像上面的例子,组件根本没有props
,即没有输入,当然也就可以认为输入是恒定的;但每次组件的按钮被单击,都会使得计数器数值增加1,也就是输出并不恒定——这说明该组件不应是纯函数组件。
组件函数内部使用了一个外部状态count
和修改外部状态的方法setCount
。他们构成了函数的副作用。
我一般倾向于将状态描述为“函数内部使用的外部状态”,而不是“函数内部的状态”,因为该状态实际上只是被函数所用,二者并没有包含或者拥有关系。
这个状态会因按钮单击而发生变化(也就是组件Example
会改变外部状态),同时<p>
的值也会随count
状态值的改变而改变(外部状态会影响Example
的行为)。如此一来,纯函数式组件就做不到了。因为“受外部状态影响”或者“改变外部状态”,这些都是副作用,纯函数不应拥有副作用。
以前,只能使用React的组件类来实现副作用;但现在有了Hooks,函数式组件也同样可以做到。
useState
使用状态,即创建/使用/修改一个外部状态。
函数式组件渲染,相当于执行一次该函数获得返回的Virtual DOM。useState
的作用简单概括一下,就是只在第一次渲染(执行函数)时,创建state
并赋初值,后续的渲染(执行函数)useState
只会返回上次state
的值。
这样一来就构成了一个外部的状态,useState
与函数的输入无关,仅仅用于初始化或获取状态的值。
当然,useState
返回的第二个参数setState
用于改变状态,允许函数拥有改变外部状态的能力。
乍一听好像函数拥有了状态了,似乎不再纯粹。但实际上还是前文提到的,函数本身并无状态。这里仅仅是注入了外部的一个状态,相当于一种输入;同时函数会根据输入,不仅仅输出Virtual DOM,还会“输出一些副作用”,并不产生副作用。简单点说,就是函数调用setState
方法并不会真的改变相应state
的值,而是对外输出一个“改变的意向/效果”。React调度器知道了它有这个意向,才会真正实现对应的效果——改变状态值。
所有的Hooks其实都是这样,并不会直接产生副作用,而是使得函数有能力对外输出产生副作用的意向。
不过,我为了表述方便,下面就不再咬文嚼字特意区分这个概念了。毕竟已经了解,就不必过于纠结字眼,能够方便理解其应用才是后文应该关心的。
useEffect
使用副作用,即,使得函数在某些情况下可以触发副作用(引起外部环境改变)。
其实按我理解,当你使用useState
返回的setState
函数时,同样也是触发了副作用。不过那个副作用,是改变了外部的React状态。
这里useEffect
可以做到的是,改变任意外部环境的状态(而不仅限于React的state变量),比如修改浏览器的标题、发起IO(如网络请求)等。
另外,useEffect
并非由程序主动触发,而是根据依赖变量值的变化触发。默认情况是每次渲染(执行函数)都会触发,但提供了依赖变量后,副作用仅在依赖变量的值发生改变时触发。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
上例中,修改document.title
是一个副作用,它仅会在count
改变时触发。指定依赖变量可以避免不必要的副作用触发,提高性能。
简单一点理解,useEffect
就是声明一些事情随着一些变量的改变而发生。
useEffect
的第一个参数即副作用函数,它还可以拥有一个返回值,返回值的类型也是一个函数。这个函数将被用于清理副作用。
每次副作用被触发时,React会首先调用上次副作用返回的清理函数以清理上次的副作用。这在使用时钟、发起网络请求等场景中十分有用。
例如:
useEffect(() => {
const timer = setTimeout(() => {
console.log("timeout");
}, 1000);
return () => clearTimeout(timer);
});
useContext
有时候我们需要一个状态或者全局的数据同时被多个子组件甚至子孙组件获得或改变,除了把状态或数据当作props
一层层传下去,还有什么办法吗?
那就是使用useContext
了!
const MyCtx = React.createContext({ val: 1 });
export default function App() {
return (
<MyCtx.Provider value={{ val: 2 }}>
<Example />
<Example />
<Example />
</MyCtx.Provider>
);
}
function Example() {
const { val } = useContext(MyCtx);
return <p>{val}</p>;
}
可以看到上例中,三个Example
都没有从props
中接受上层组件的信息,而是直接通过useContext
获取。
这样可以大大减少传递变量的代码量,让代码更加简洁清晰。
再举一个共享状态的例子:
const MyCtx = React.createContext({ val: 1 });
export default function App() {
const [val, setVal] = useState(1);
return (
<MyCtx.Provider value={[val, setVal]}>
<Example />
<Example />
<Example />
</MyCtx.Provider>
);
}
function Example() {
const [val, setVal] = useContext(MyCtx);
return (
<div>
<p>{val}</p>
<button onClick={() => setVal(i => i + 1)}>add</button>
</div>
);
}
点击任意一个Example
组件的按钮,都会调用MyCtx
这个Context
的setVal
,也就是上层App
的setVal
,也就会改变上层App
的val
状态,从而导致所有Example
组件的<p>
的值发生改变。
可见,useContext
是共享状态的利器!
useReducer
这个东西和useState
是类似的,只不过setState
这种直接替换整个state
的函数,被reducer
函数所取代。
reducer
函数形如(state, action) => newState
,接受旧状态和一个表示变换操作的action
,返回新状态。主要的不同就在于action
,也就是说reducer
可以支持更多样的改变状态的逻辑。
这个东西配合
useContext
基本上可以取代Redux库的功能。
useReducer
很好理解,所以我就不多说了,直接搬运官网的例子:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useMemo, memo, useCallback
useMemo
用于记忆值(useMemo
即use Memorized Value
)。
如果不用useMemo
,考虑一下下面的程序:
export default function App() {
const [val, setVal] = useState(1);
return (
<div>
<p>{val}</p>
<button onClick={() => setVal(i => i + 1)}>add</button>
{[...Array(100000).keys()].map((_, i) => (
<p key={i}>p{i}</p>
))}
</div>
);
}
每次单击按钮,都会使得val
发生改变从而导致App
重渲染。而每次重渲染,意味着执行App
函数,也就意味着100000
个成员的数组会被重新构造一次。所以,每次单击按钮,用户都要忍受一次UI的卡顿。
100000
个成员的数组用于生成相应数量的<p>
节点,节点内容与外部状态无关,因此完全可以将生成的结果使用useMemo
记忆下来:
export default function App() {
const [val, setVal] = useState(1);
return (
<div>
<p>{val}</p>
<button onclick={() => setVal(i => i + 1)}>add</button>
{useMemo(() => [...Array(50000).keys()].map((_, i) => (
<p key={i}>p{i}</p>
)), [])}
</div>
);
}
再次尝试,就会发现每次点击按钮后,UI的响应速度基本如常了。
可以看到这里使用useMemo
时,传递的第二个参数是[]
。这个参数是记忆值的依赖变量列表,表示记忆值应当跟随它们的变化而更新。由于这里记忆的东西与外界状态、属性均无关联,所以可以直接填空。
空依赖变量列表可以理解成,仅当组件首次渲染时触发记忆。其实前面useEffect
也同理,空依赖变量列表的useEffect
即仅首次渲染时触发的副作用。
memo
类似于useMemo
,但是是组件级别的记忆:
export default function App() {
const [val, setVal] = useState(1);
return (
<div>
<p>{val}</p>
<button onClick={() => setVal(i => i + 1)}>add</button>
<Example n={50000} />
</div>
);
}
const Example = memo(props =>
[...Array(props.n).keys()].map((_, i) => <p key={i}>p{i}</p>)
);
可以看到,Example
组件是记忆化组件,意味着它仅在props
发生改变时进行渲染,因此可以实现与前面useMemo
的例子相同的性能优化的效果。
另外,useCallback
之所以放在这一小节,是因为它和useMemo
是等价的:
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
你可能会问,构造一个函数的代价会很大吗,为什么也需要记忆?
其实useCallback
记忆函数有另外的更重要的目的,就是使得回调函数不变以避免重渲染。
还是举个例子吧:
export default function App() {
const [val, setVal] = useState(1);
return (
<div>
<p>{val}</p>
<button onClick={() => setVal(i => i + 1)}>add</button>
<Example n={50000} onClick={() => {}} />
</div>
);
}
const Example = memo(props => (
<div>
<button onClick={props.onClick}>button</button>
{[...Array(props.n).keys()].map((_, i) => (
<p key={i}>p{i}</p>
))}
</div>
));
你会发现在点击add
按钮时,UI又卡住了!
这是因为当App
重渲染时,Example
的onClick
属性对应的回调函数() => {}
会重新构造一个,虽然实现一致,但并不是上次的那个函数了。
这也就意味着Example
的props
发生了改变,所以Example
即使是memo
的组件,也仍然要重渲染。
这时候useCallback
就派上用场了,我们需要把() => {}
记忆下来:
export default function App() {
const [val, setVal] = useState(1);
const cb = useCallback(() => {}, []);
return (
<div>
<p>{val}</p>
<button onClick={() => setVal(i => i + 1)}>add</button>
<Example n={30000} onClick={cb} />
</div>
);
}
const Example = memo(props => (
<div>
<button onClick={props.onClick}>button</button>
{[...Array(props.n).keys()].map((_, i) => (
<p key={i}>p{i}</p>
))}
</div>
));
终于,一切正常了。
useRef
useRef
非常像useState
,主要区别于两点:useRef
不会触发重渲染,且是同步的。
所以,useRef
更像一个纯粹的外部状态,因为它不像useState
和React
的渲染机制挂钩,它单纯的用于在组件的生命周期内保存数据。
因此,它可以被类比成组件类的一个成员变量。
正是这种保存数据的特性,使得它可以用于引用DOM节点:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
最后还剩几个Hooks,并不常用,有兴趣直接看官网的文档吧。
https://zh-hans.reactjs.org/docs/hooks-reference.html
有人说React Hooks的原理基于函数式编程Algebraic Effects的概念,有兴趣可以了解一下。