React 16.8版本开始,引入了Hooks机制。Hooks机制允许在无class的情况下使用state以及其他React特性。在React中,钩子函数是用来让函数组件拥有局部状态和其他React特性(例如生命周期)的特殊函数。Hooks的出现使得React的代码变得简洁、易于维护。
钩子函数:某个阶段触发的回调函数。 例:vue的生命周期函数就是钩子函数
常用的钩子函数:
- useState【维护状态】
- useEffect【完成副作用操作】
- useContext【使用共享状态】
- useReducer【类似redux】
- useCallback【缓存函数】
- useMemo【缓存值】
- useRef【访问DOM】
- useImperativeHandle【使用子组件暴露的值/方法】
- useLayoutEffect【完成副作用操作,会阻塞浏览器绘制】
useState
普通更新/函数式更新 state
1 | const Index = () => { |
useEffect
useEffet 我们可以理解成它替换了 componentDidMount, componentDidUpdate, componentWillUnmount 这三个生命周期,但是它的功能还更强大。
包含3个生命周期的代码结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14useEffect(
() => {
// 这里的代码块 等价于 componentDidMount
// do something...
// return的写法 等价于 componentWillUnmount(可选)
return () => {
// do something...
};
},
// 依赖列表,当依赖的值有变更时候,执行副作用函数,等价于 componentDidUpdate
[ xxx,obj.xxx ]
);
注意:依赖列表是灵活的,有三种写法
- 当数组为空 [ ],表示不会因为页面的状态改变而执行回调方法【即仅在初始化时执行,componentDidMount】
- 当这个参数不传递,表示页面的任何状态一旦变更都会执行回调方法
- 当数组非空,数组里的值一旦有变化,就会执行回调方法
执行时机:
● 在浏览器完成渲染(包括 DOM 更新和屏幕绘制)之后异步执行。
● 不会阻塞浏览器的绘制,因此用户体验更流畅。
● 适用于大多数副作用,如数据获取、事件订阅、设置定时器等。
场景1:
我依赖了某些值,但是我不要在初始化就执行回调方法,我要让依赖改变再去执行回调方法
我们这里有用到了 useRef 这个钩子:1
2
3
4
5
6
7
8
9
10const firstLoad = useRef(true);
useEffect(() => {
if (firstLoad.current) {
firstLoad.current = false;
return;
}
// do something...
}, [ xxx ]);
场景2:
我有一个getData的异步请求方法,我要让其在初始化调用且点击某个按钮也可以调用
这个组件只要一有更新触发了render, getData 的就会重新被定义,此时的引用不一样,会导致useEffect运行。
这个是影响性能的行为,我们用 useCallback 钩子来缓存它来提高性能:1
2
3
4
5
6
7
8
9
10
11
12
13
14// ...
const getData = useCallback(async () => {
const data = await xxx({ id: 1 });
setDetail(data);
}, []);
useEffect(() => {
getData();
}, [getData]); // 需要 useEffect 需要添加getData依赖
const handleClick = () => {
getData();
};
// ...
useContext
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树 的逐层传递 props1
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
36const obj = {
value: 1
};
const obj2 = {
value: 2
};
const ObjContext = React.createContext(obj);
const Obj2Context = React.createContext(obj2);
const App = () => {
return (
<ObjContext.Provider value={obj}>
<Obj2Context.Provider value={obj2}>
<ChildComp />
</Obj2Context.Provider>
</ObjContext.Provider>
);
};
// 子级
const ChildComp = () => {
return <ChildChildComp />;
};
// 孙级或更多级
const ChildChildComp = () => {
const obj = useContext(ObjContext);
const obj2 = useContext(Obj2Context);
return (
<>
<div>{obj.value}</div>
<div>{obj2.value}</div>
</>
);
};
useReducer
在某些场景下,useReducer 会比 useState 更适用,当state逻辑较复杂。我们就可以用这个钩子来代替useState,它的工作方式犹如 Redux,看一个例子: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
41
42
43
44
45
46
47
48
49
50
51const initialState = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
const reducer = (state: any, { type, payload }: any) => {
switch (type) {
case "add":
return [...state, payload];
case "remove":
return state.filter((item: any) => item.id !== payload.id);
case "update":
return state.map((item: any) =>
item.id === payload.id ? { ...item, ...payload } : item
);
case "clear":
return [];
default:
throw new Error();
}
};
const List = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
List: {JSON.stringify(state)}
<button
onClick={() =>
dispatch({ type: "add", payload: { id: 3, name: "周五" } })
}
>
add
</button>
<button onClick={() => dispatch({ type: "remove", payload: { id: 1 } })}>
remove
</button>
<button
onClick={() =>
dispatch({ type: "update", payload: { id: 2, name: "李四-update" } })
}
>
update
</button>
<button onClick={() => dispatch({ type: "clear" })}>clear</button>
</>
);
};
暴露出去的 type 可以让我们更加的了解,当下我们正在做什么事
useCallback
1 | // 除非 `a` 或 `b` 改变,否则不会变 |
动手滑到上面,已经有提到了一个例子,说到了 useCallback ,算是一个场景, 我们都知道它可以用来缓存一个函数。
接下来我们讲讲另一个场景。
前面讲的话:react中只要父组件的 render 了,那么默认情况下就会触发子组 的 render,react提供了来避免这种重渲染的性能开销的一些方法: React.PureComponent、React.memo ,shouldComponentUpdate()
让我们来耐心看一个例子,当我们子组件接受属性是一个方法的时候,如: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
31const Index = () => {
const [count, setCount] = useState(0);
const getList = (n) => {
return Array.apply(Array, Array(n)).map((item, i) => ({
id: i,
name: "张三" + i
}));
};
return (
<>
<Child getList={getList} />
<button onClick={() => setCount(count + 1)}>count+1:{count}</button>
</>
);
};
const Child = ({ getList }) => {
console.log("child-render");
return (
<>
{getList(10).map((item) => (
<div key={item.id}>
id:{item.id},name:{item.name}
</div>
))}
</>
);
};
当点击“count+1”按钮,发生了这样子的事:
1 | 父组件render > 子组件render > 子组件输出 "child-render" |
为了避免子组件做没必要的渲染,这里用了React.memo,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// ...
const Child = React.memo(({ getList }) => {
console.log("child-render");
return (
<>
{getList(10).map((item) => (
<div key={item.id}>
id:{item.id},name:{item.name}
</div>
))}
</>
);
});
// ...
我们不假思索的认为,当我们点击“count+1”时,子组件不会再重渲染了。但现实 是,还是依然会渲染,这是为什么呢? 答:Reace.memo只会对props做浅比较,也就是父组件重新render之后会传入 不同引用的方法 getList,浅比较之后不相等,导致子组件还是依然会渲染。
这时候,useCallback 就可以上场了,它可以缓存一个函数,当依赖没有改变的时候,会一直返回同一个引用。如:1
2
3
4
5
6
7
8
9
10// ...
// 使用 useCallback 缓存 getList 函数
const getList = useCallback((n) => {
return Array.apply(Array, Array(n)).map((item, i) => ({
id: i,
name: "张三" + i
}));
}, []);
// ...
总结:如果子组件接受了一个方法作为属性,我们在使用 React.memo 这种避免子组件做没必要的渲染时候,就需要用 useCallback 进行配合,否则 React.memo 将无意义。
useMemo
与 vue 的 computed 类似,主要是用来避免在每次渲染时都进行一些高开销的计算,缓存计算数据的值。举个简单的例子。
不管页面 render 几次,时间戳都不会被改变,因为已经被被缓存了,除非依赖改变。1
2
3
4
5// ...
const getNumUseMemo = useMemo(() => {
return `${+new Date()}`;
}, []);
// ...
1 | const NumberProcessor = () => { |
useRef
我们用它来访问DOM,从而操作DOM,如点击按钮聚焦文本框:1
2
3
4
5
6
7
8
9
10
11
12const Index = () => {
const inputEl = useRef(null);
const handleFocus = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={handleFocus}>Focus</button>
</>
);
};
注意:返回的 ref 对象在组件的整个生命周期内保持不变。 它类似于一个 class 的实例属性,我们利用了它这一点。 动手滑到上面再看上面看那个有 useRef 的例子。
刚刚举例的是访问DOM,那如果我们要访问的是一个组件,操作组件里的具体DOM呢?我们就需要用到 React.forwardRef 这个高阶组件,来转发ref,如:
1 | const Index = () => { |
useImperativeHandle
useImperativeHandle 可以让我们在父组件调用到子组件暴露出来的属性/方法。如: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
33const Index = () => {
const inputEl = useRef();
useEffect(() => {
console.log(inputEl.current.someValue);
// test
}, []);
return (
<>
<Child ref={inputEl} />
<button onClick={() => inputEl.current.setValues((val) => val + 1)}>
累加子组件的value
</button>
</>
);
};
// 1、forwardRef 来建立连接
const Child = forwardRef((props, ref) => {
const inputRef = useRef();
const [value, setValue] = useState(0);
// 2、用 useImperativeHandle 来定义通过这个连接可以访问什么
useImperativeHandle(ref, () => ({
setValue,
someValue: "test"
}));
return (
<>
<div>child-value:{value}</div>
<input ref={inputRef} />
</>
);
});
总结:类似于vue在组件上用 ref 标志,然后 this.$refs.xxx 来操作dom或者调用子组件值/方法,只是react把它“用两个钩子来表示”。
useLayoutEffect
在所有的 DOM 变更之后同步调用effect。可以使用它来读取 DOM 布局并同步 触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新,也就是说它会阻塞浏览器绘制。所以尽可能使用 useEffect 以避免阻 塞视觉更新。
执行时机:
● 在React 完成 DOM 更新之后,但在浏览器进行绘制(paint)之前同步执行。
● 它的代码执行会阻塞浏览器的绘制。
● 适用于需要在用户看到更新前立即读取 DOM 布局信息或进行 DOM 同步修改的场景。