Hooks 的 Capture Value 特性

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(() => {
// timer 打印的 count 永远是上一次 render 中的值
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
// react 第一次渲染时
function Counter() {
const count = 0;
return <div>{count}</div>;
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
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
//  react 第一次渲染
function Counter() {
const count = 0;

function handleAlertClick() {
setTimeout(() => {
// count === 0
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 01;

// 创建了新的 handleAlertClick
function handleAlertClick() {
setTimeout(() => {
// count === 1
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(() => {
// count 永远是 0
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';

// hooks 存放在这个数组
let memoizedState = [];
// 当前 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();
// 每更新一次都需要将_index归零,才不会不断重复增加 memoizedState
cursor = 0;
}

// cursor 加 1
cursor++;

// 返回当前 state
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++;
}

react 中的常用类型

React.FC | React.FunctionComponent

表示函数组件的类型

1
const MyComponent: React.FC<Props> = function(p) {...}

React.Component

表示类组件的类型

1
class MyComponent extends React.Component<Props, State> { ...

React.ComponentType

表示(React.FC | React. Component)的联合的类型-在 HOC 中使用

1
2
3
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ...

React.ComponentProps

获取指定组件 XXX 的 Props 类型(警告: 不能使用静态声明的默认道具和通用道具)

1
type MyComponentProps = React.ComponentProps<typeof MyComponent>;

React.ReactElement | JSX.Element

类型表示 DOM 组件(例如

)或自定义的组件(例如 )

1
const elementOnly: React.ReactElement = <div /> || <MyComponent />;

React.ReactNode

表示任何可能类型的 React 节点( ReactElement + 基本 JS 类型)

1
2
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;
const Component = ({ children: React.ReactNode }) => ...

React.CSSProperties

JSX 中输入表示样式对象的类型

1
2
const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...

React.HTMLProps

表示指定 HTML 元素的类型-用于扩展 HTML 元素

1
2
3
const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... }

<Input about={...} accept={...} alt={...} ... />

React.ReactEventHandler

表示泛型事件处理函数-用于声明处理事件的函数

1
2
3
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }

<input onChange={handleChange} ... />

React.XXXEvent

表示更具体的事件。 常见的事件: ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent.

1
2
3
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }

<div onMouseMove={handleChange} ... />

在上面的代码中反应。 鼠标事件  React.MouseEvent  是鼠标事件的类型,这个事件发生在  HTMLDivElement  上

react hooks 初步

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。 React Hooks 就是那些钩子。

react 约定, hook 一律使用 use 前缀命名,便于识别
常用 hook

useState()
useContext()
useEffect()
useReducer()
useState

useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

1
2
3
4
function Button() {
const [count, setCount] = useState(0);
return <button onClick={setCount(count + 1)}>+1</button>;
}

useState()接受初始值,返回一个数组,数组第一个是当前状态,第二个是更新状态的函数。

useContext

useContext()用于组件间共享状态。

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
// 创建一个 context 对象
const ctxDep = {name: '王花花'}
const Ctx = React.creatContext(ctxDep)
// 在组件中使用
function App() {
return (
// 下级组件使用 useContext 获取的值是 ctxDep.name
<Ctx.Provider value={ctxDep.name}>
<Sub />
<Sub2 />
</Ctx.Provider>
)
}
function Sub() {
// useContext 获取的是距离当前组件最近的 <MyContext.Provider> 的 value prop 。
// 此处的就是 ctxDep.name
const name = useContext(Ctx)
return (
<div>name: {name}</div>
)
}
function Sub2() {
const name = useContext(Ctx)
...
}

useEffect

useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。

1
2
3
useEffect(() => {
// Async Action
}, [dependencies]);

useEffect()接受两个参数。第一个是一个函数,操作的代码放在里面(常见的是异步操作)。第二个是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行 useEffect()。 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect

useEffect 清除操作

在 useEffect 中返回一个函数去执行清除操作, React 会在组件卸载的时候执行清除操作。React 会在执行当前 effect 之前对上一个 effect 进行清除。

1
2
3
4
5
6
useEffect(() => {
...
return function cleanFun() {
...
}
}, [])

useReducer

useReducers()钩子用来引入 Reducer 功能。升级版 useState

1
const [state, dispatch] = useReducer(reducer, initialState);

useReducer()接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是与其配套的发送 action 的 dispatch 函数。

使用 useReducer()接受的 reducer 形式如(state, action) => newState

惰性初始化

使用 useReducer 的第三个参数来设置初始值

1
2
3
const initialState = 1;
// init 是函数,返回 {count: 1}
const [state, dispatch] = useReducer(reducerCount, initialState, init);

如果 useReducer 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。

useMemo

用于性能优化,useMemo 返回一个值。

1
const memoizedValue = useMemo(() => dosomething(a, b), [a, b]);

useMemo 会在渲染期执行,但除了第一次外,只会在依赖项发生改变后才会重新执行。

useCallback

useCallback()返回一个函数, 在依赖值没有发生改变时不触发重新渲染。

1
2
3
4
5
6
// 仅当 count 的值发生改变时,memoCallback 才触发组件重新渲染
function Foo() {
const [count, setCount] = useState(0);
const memoizedHandleClick = useCallback(() => console.log(`点击: ${count}`), [count]);
return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}

useRef

1
2
3
4
5
const refContainer = useRef(initialValue);
return (
// 此时 refContainer.current 指向下面这个 button
<button ref={refContainer}></button>
);

useLayoutEffect

效果 useEffect 相同,但会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

react 生命周期

挂载阶段

React.js 将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为组件的挂载。

  1. constructor: 所有关于组件自身的状态的初始化工作都会放在这里面去做

  2. static getDerivedStateFromProps(nextProps, prevState): 一个静态方法,所以不能在这个函数里面使用 this,这个函数有两个参数 props 和 state,分别指接收到的新参数和当前的 state 对象,这个函数会返回一个对象用来更新当前的 state 对象,如果不需要更新可以返回 null。

    该函数会在挂载时,接收到新的 props,调用了 setState 和 forceUpdate 时被调用

  3. UNSAFE_componentWillMount:组件挂载开始之前,也就是在组件调用  render  方法之前调用。进行组件的启动工作,例如 Ajax 数据拉取、定时器的启动。

  4. render:React 中最核心的方法,一个组件中必须要有这个方法

    返回的类型有以下几种:

    • 原生的 DOM,如 div
    • React 组件
    • Fragment(片段)
    • Portals(插槽)
    • 字符串和数字,被渲染成 text 节点
    • Boolean 和 null,不会渲染任何东西

    render 函数是纯函数,里面只做一件事,就是返回需要渲染的东西,不应该包含其它的业务逻辑,如数据请求,对于这些业务逻辑请移到 componentDidMount 和 componentDid Update 中

  5. componentDidMount:组件挂载完成以后,也就是 DOM 元素已经插入页面后调用。

更新阶段

除了挂载阶段,还有一种“更新阶段”就是  setState  导致 React.js 重新渲染组件并且把组件的变化应用到 DOM 元素上的过程,这是一个组件的变化过程

  1. UNSAFE_componentWillReceiveProps(nextProps):组件从父组件接收到新的  props  之前调用。
  2. static getDerivedStateFromProps
  3. shouldComponentUpdate(nextProps, nextState):你可以通过这个方法控制组件是否重新渲染。如果返回  false  组件就不会重新渲染。这个生命周期在 React.js 性能优化上非常有用。
  4. UNSAFE_componentWillUpdate():组件开始重新渲染之前调用。
  5. render()
  6. getSnapshotBeforeUpdate(prevProps, prevState):这个方法在 render 之后,componentDidUpdate 之前调用,有两个参数 prevProps 和 prevState,表示之前的属性和之前的 state,这个函数有一个返回值,会作为第三个参数传给 componentDidUpdate,如果你不想要返回值,请返回 null,不写的话控制台会有警告。
  7. componentDidUpdate():组件重新渲染并且把更改变更到真实的 DOM 以后调用。

卸载阶段

componentWillUnmount:组件对应的 DOM 元素从页面中删除之前调用。组件从页面上销毁的时候,有时候需要一些数据的清理,例如定时器的清理,就会放在这里面去做。

参考:

React v16.4.0:你可能并不需要派生状态(Derived State)

对 React v16.4 生命周期的理解

react 学习笔记

  • _自定义的组件都必须要用大写字母开头,普通的 HTML 标签都用小写字母开头_。

  • 没有经过特殊处理的话,这些  on\*  的事件监听只能用在普通的 HTML 的标签上,而不能用在组件标签上

  • React.js 的事件监听方法需要手动  bind  到当前实例,这种模式在 React.js 中非常常用。

  • 当我们要改变组件的状态的时候,不能直接用  this.state = xxx  这种方式来修改,如果这样做 React.js 就没办法知道你修改了组件的状态,它也就没有办法更新页面。所以,一定要使用 React.js 提供的  setState  方法,它接受一个对象或者函数作为参数

  • React.js 内部会把 JavaScript 事件循环中的消息队列的同一个消息中的  setState  都进行合并以后再重新渲染组件。

  • 可以通过给组件添加类属性  defaultProps  来配置默认参数。

  • props  一旦传入,你就不可以在组件内部对它进行修改。但是你可以通过父组件主动重新渲染的方式来传入新的  props,从而达到更新的效果。

  • 没有  state  的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)

  • 对于用表达式套数组罗列到页面上的元素,都要为每个元素加上  key  属性,这个  key  必须是每个元素唯一的标识


受控组件:

React.js 认为所有的状态都应该由 React.js 的 state 控制,只要类似于<input />、<select />、<textarea>这样的输入控件被设置了  value  值,那么它们的值永远以被设置的值为准。值不变,value  就不会变化。

在 React.js 当中必须要用  setState  才能更新组件的内容,所以我们需要做的就是:监听输入框的  onChange  事件,然后获取到用户输入的内容,再通过  setState  的方式更新  state  中的  username,这样  input  的内容才会更新。


子组件传递消息给父组件

父组件只需要通过  props  给子组件传入一个回调函数。子组件调用  props  中的回调函数并且将  state  传入该函数即可。

举例:


状态提升

当某个状态被多个组件依赖或者影响的时候,就把该状态提升到这些组件的最近公共父组件中去管理,用  props  传递数据或者函数来管理这种依赖或着影响的行为。

对于不会被多个组件依赖和影响的状态(例如某种下拉菜单的展开和收起状态),一般来说只需要保存在组件内部即可,不需要做提升或者特殊的管理。


操作 DOM

1. 使用回调: 在元素上加一个  ref  属性,这个属性值是一个函数,这个节点挂载到页面上后,这个 dom 节点会作为参数传给这个函数

1
2
// 把这个 div 保存到 this 上
<div ref={div => (this.div = div)}></div>

然后我们就可以在  componentDidMount中 或之后使用这个 DOM 元素

2. 使用 React.createRef()

1
2
3
4
5
6
7
8
9
10
11
// 在 constructor 中声明
this.third = React.createRef();
// 在 render 函数中:
<input type="text" defaultValue="Third" ref={this.third} />;
// 获取 ref
this.third.current;

// 在 render 函数里面
<input type="text" defaultValue="First" ref="first" />;
// 获取 ref
this.refs.first;
  • _能不用  ref  就不用_。特别是要避免用  ref  来做 React.js 本来就可以帮助你做到的页面自动更新的操作和事件监听。多余的 DOM 操作其实是代码里面的“噪音”,不利于我们理解和维护。
  • 组件标签也可以加上  ref,此时获取的是组件实例。

props.children

组件标签也能像普通的 HTML 标签那样编写内嵌的结构

1
2
3
4
5
6
7
8
render() {
return (
<Card>
<Test />
</Card>
)
}
// Card 组件中能通过 this.props.children 获取到 <Test />

React.js 默认就支持这种写法,所有嵌套在组件中的 JSX 结构都可以在组件内部通过  props.children  获取到,props.children  是一个数组,React.js 就是把我们嵌套的 JSX 元素一个个都放到数组当中。


PropTypes

引入  prop-types

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class a extends Component {
static propTypes = {
username: PropTypes.string.isRequired,
...
PropTypes.array
PropTypes.bool
PropTypes.func
PropTypes.number
PropTypes.object
PropTypes.string
PropTypes.node
PropTypes.element
}
}

通过  PropTypes  给组件的参数做类型限制,可以在帮助我们迅速定位错误,这在构建大型应用程序的时候特别有用;另外,给组件加上  propTypes,也让组件的开发、使用更加规范清晰。