Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。
1 2 3
| function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
|
函数组件容易阅读和测试,没有状态或生命周期。因此可以让我们快速的写一个渲染 UI 的组件,这也与 React 推崇函数式编程的思想契合。
在 React v16.8 推出之前函数组件只能简单的渲染组件,没有自身的状态,Hooks 的到来改变了这一现状,它赋予函数组件更多的能力。
通过 useState 让函数组件拥有了自身的状态。useEffect 让函数组件能够执行包含副作用的行为。
Capture Value
首先说明 class 组件是不具备 Capture Value 特性的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class ClassCount extends Component { state = { count: 0, };
timer() { setTimeout(() => { console.log(`点击 3 秒后的 count: ${this.state.count}`); }, 3000); }
render() { return ( <button onClick={() => { this.setState({ count: this.state.count + 1 }); this.timer(); }} > class 组件点击了 {this.state.count} 次 </button> ); } }
|
上面的代码中,每次点击后 timer 输出的值和当前组件 state 的值是一致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function FunctionCount() { const [count, setCount] = useState(0);
const timer = () => { setTimeout(() => { console.log(`function 组件 3 秒后的 count: ${count}`); }, 3000); };
return ( <button onClick={() => { setCount(count + 1); timer(); }} > function 组件点击了 {count} 次 </button> ); }
|
在函数组件里,每次点击后 timer 输出的值和当前组件中的 state 不一致,这是因为函数组件他就是一个普通函数,react 每次渲染都会重新执行一遍这个函数,这样就导致,对于函数内部来说每次渲染都有自己的 Props 与 State。
每次渲染都有自己的 Props 与 State
可以认为每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Counter() { const [count, setCount] = useState(0);
return ( <button onClick={() => { setCount(count + 1); }} > function 组件点击了 {count} 次 </button> ); }
|
当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Counter() { const count = 0; return <div>{count}</div>; }
function Counter() { const count = 1; return <div>{count}</div>; }
|
React 仅仅只是在渲染输出中插入了 count 这个数字, 这个数字由 React 提供。当 setCount 的时候,React 会带着一个不同的 count 值再次调用组件。然后,React 会更新 DOM 以保持和渲染输出一致。
这里关键的点在于任意一次渲染中的 count 常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的 count 值独立于其他渲染。
每一次渲染都有它自己的事件处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function Counter() { const [count, setCount] = useState(0);
function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); }
return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ); }
|
当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。对于 handleAlertClick 也是这样
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
| function Counter() { const count = 0;
function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); }
return (...); }
function Counter() { const count = 01;
function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); }
return (...); }
|
React 每次渲染都会创建新的 handleAlertClick, 而对于 handleAlertClick 来讲,每次他访问到的 count 都是一个全新的常量。
在任意一次渲染中,props 和 state 是始终保持不变的。如果 props 和 state 在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的 count 值。
每次渲染都有它自己的 Effects
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Counter() { const [count, setCount] = useState(0);
useEffect(() => { console.log(`useEffect 执行, count: ${count}`); });
return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }
|
useEffect 和事件处理函数一样,每次渲染都被重新执行,传入 useEffect 的回调函数也被重新创建,所有内部引用的所有 state 都是独属于本次渲染的。
错误设置依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Counter4() { const [count, setCount] = useState(0);
useEffect(() => { const id = setInterval(() => { console.log('Counter4 setInterval', count); setCount(count + 1); }, 1000);
return () => clearInterval(id); }, []);
return <h1>count: {count}</h1>; }
|
上面的代码中的 setInterval 会持续执行,但是 count 的值保持不变,原因就是第一次执行时 setCount 触发重新渲染,但是由于 useEffect 的依赖是[],effect 不会再重新运行,它后面每一秒都会调用 setCount(0 + 1)
设置依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function Counter5() { const [count, setCount] = useState(0);
useEffect(() => { console.log('Counter5 useEffect');
const id = setInterval(() => { console.log('Counter5 setInterval'); setCount(count + 1); }, 1000);
return () => clearInterval(id); }, [count]);
return <h1>count: {count}</h1>; }
|
上面的代码已经可以正确运行了,缺点是 count 每次改变都会重置 setInterval
仅指定行为,不依赖具体的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function Counter6() { const [count, setCount] = useState(0);
useEffect(() => { console.log('Counter6 useEffect');
const id = setInterval(() => { console.log('Counter6 setInterval'); setCount(c => c + 1); }, 1000);
return () => clearInterval(id); }, []);
return <h1>count: {count}</h1>; }
|
附 useState, useEffect 的简单实现
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 35 36 37 38 39 40
| import { render } from '../index';
let memoizedState = [];
let cursor = 0;
export function useState(initialValue) { memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor; function setState(newState) { if (typeof newState === 'function') { memoizedState[currentCursor] = newState(memoizedState[currentCursor]); } else { memoizedState[currentCursor] = newState; }
render(); cursor = 0; }
cursor++;
return [memoizedState[currentCursor], setState]; }
export function useEffect(callback, depArray) { const hasNoDeps = !depArray; const deps = memoizedState[cursor]; const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true; if (hasNoDeps || hasChangedDeps) { setTimeout(callback); memoizedState[cursor] = depArray; } cursor++; }
|