【理论】React Hooks全解
2022-02-16 本文已影响0人
darkTi
- Hook 是 React 16.8 的新增特性,所以使用Hook 时要保证React版本在16.8及以上。
npm info react versions
,用来获取react所有的版本号;- 有了Hook我们就不需要在
class
里面去声明状态了
useState
- 具体内容可阅读上一节
useReducer
1、基础用法
- 用来践行Flux/Redux的思想
- 步骤:
①创建初始值initalState;
②创建所有操作reducer(state, action);
③传给useReducer,得到读写API;
④调用写({type: '操作类型'}); - 可看下面代码:
import React, { useReducer } from "react";
import ReactDOM from "react-dom";
const initFormValue = {
name: "",
age: 18,
nationality: "汉族"
};
function reducer(state, action) {
switch (action.type) {
case "patch":
return { ...state, ...action.formData };
case "reset":
return initFormValue;
default:
throw new Error();
}
}
function App() {
const [formData, dispatch] = useReducer(reducer, initFormValue);
const onReset = () => {
dispatch({ type: "reset" });
};
return (
<div>
<form onReset={onReset}>
<div>
姓名
<input
value={formData.name}
onChange={(e) =>
dispatch({ type: "patch", formData: { name: e.target.value } })
}
/>
</div>
<div>
年龄
<input
value={formData.age}
onChange={(e) =>
dispatch({ type: "patch", formData: { age: e.target.value } })
}
/>
</div>
<div>
民族
<input
value={formData.nationality}
onChange={(e) =>
dispatch({
type: "patch",
formData: { nationality: e.target.value }
})
}
/>
</div>
<div>
<button type="submit">提交</button>
<button type="reset">重置</button>
</div>
<hr />
{JSON.stringify(formData)}
</form>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
2、用useReducer代替Redux
- 步骤
①将数据集中在一个store对象里;
②再将所有操作集中在reducer里;
③创建一个Context,const Context = React.createContext(null)
,一般都要传个null;
④将reducer、store传给useReducer,得到读和写API,const [state, dispatch] = useReducer(reducer, store)
;
⑤将第四步的内容放到第三步的Context里;
⑥用Context.Provider将Context提供给所有它所包住的组件,<Context.Provider value={{state, dispatch}}>...</Context.Provider>
;
⑦各个组件用useContext获取读写API,const {state, dispatch} = useContext(Context)
,这里的Context是第三步里创建的Context;
总的来说,useReducer是useState的复杂版;
useContext
1、上下文
- 上下文就是局部的全局变量;其实也就是作用域啦~能在哪个范围内使用;
2、使用方法
①先创建上下文,const C = React.createContext(null)
;
②使用<C.Provider value={}></C.Provider>
圈定作用域;
③在作用域内使用useContext(C)
来使用上下文;
3、useContext不是响应式的
- useContext改变数据的时候是自顶向下逐级改变而做到的(每次都会重新渲染一遍App),不是监听这个数据变化,通知这个组件去变化的(vue3是用这种方法,响应式变化数据);
useEffect
1、副作用
- 对环境的改变就叫副作用;比如修改document.title;
- 实际上叫afterRender更贴切一些,因为是每次render后执行;
2、用途
①useEffect(()=>{}, [])
,在第一次变化后执行,相当于componentDidMount;
②useEffect(()=>{}, [n])
,只有在n变化的时候才执行(包含第一次渲染),相当于componentDidUpdate;
③useEffect(()=>{})
,任何一个变量变化就执行;
④useEffect(()=>{.... return () => {}})
,通过添加一个return来执行组件要消失时刻的操作,相当于componentWillUnmount;
上面这几种用途可同时存在;
可同时使用多个useEffect,它们之间是不冲突的;且是按照顺序来执行;
useLayoutEffect
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const [value, setValue] = useState(0);
// 这里再换成useLayoutEffect看有什么不同呢
useEffect(() => {
document.querySelector("#x").innerText = `value: 1000`;
}, [value]);
return (
<div id="x" onClick={() => setValue(0)}>
value: {value}
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
1、含义
- useEffect是在浏览器渲染完成后执行;
- useLayoutEffect是在浏览器渲染前执行;
- 看上面代码,当为useEffect时,可以看到页面上,会先显示0再很快地换为1000;当换为useLayoutEffect时,你就看不到0的显示,会直接显示为1000;
2、特点
- useLayoutEffect总是比useEffect先执行;
- 如果要使用useLayoutEffect,那么useLayoutEffect里的任务最好是影响了layout;
3、总结
- 所以为了用户体验,最好用useEffect;(useLayoutEffect会影响到用户看到画面变化的时间,因为用户就是想先看到画面啊!!!所以为什么非得要 useLayoutEffect在中间截胡一下,还要延迟用户看到画面的时间呢!)
useMemo
1、React.Memo()
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child data={m} />
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码");
return <div>child: {props.data}</div>;
}
- 看上面代码,每次更新n的时候,即使m没有变化,Child组件也会跟着渲染一边,这多耗CPU啊,所以得想一个办法,m不变化的时候Child组件不跟着渲染;所以可以用
React.Memo()
把Child组件包起来,看下面代码~
const Child = React.memo((props) => {
console.log("child 执行了");
console.log("假设这里有大量代码");
return <div>child: {props.data}</div>;
});
2、useMemo()用法
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(1);
const onClick = () => {
setN(n + 1);
};
const onClickChild = () => {
console.log("点击child 了");
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child data={m} onClick={onClickChild} />
</div>
);
}
const Child = React.memo((props) => {
console.log("child 执行了");
console.log("假设这里有大量代码");
return <div onClick={props.onClick}>child: {props.data}</div>;
});
- 有一个bug,当你给Child组件添加了一个监听函数后,在改变n时,Child组件又跟着一起变了!!!这是因为App在运行时,onClickChild函数每次都会生成一个新的函数,新旧函数地址不一样!!!
- 那怎么办呢?用React.useMemo()把onClickChild函数包起来!!!
const onClickChild = React.useMemo(() => {
return () => {
console.log(m, "点击child 了");
};
}, [m]);
3、useMemo()的特点
- 它的第一个参数是
() => value
,这个value可能是个函数也可能是个对象; - 第二个参数是依赖[m, n];
- 当依赖变化的时候就计算出新的value,如果依赖没有变化,就还用之前的value;
- 这不就跟vue2的computed很像了嘛!!!
注意!如果你的value是个函数,你就要这么写,
React.useMemo(()=> (x)=> console.log(x))
,这是一个返回函数的函数,是不是很难用,所以就有了useCallback~~~
4、useCallback
- 可以把最外层的那个函数省略掉,
React.useCallback((x)=> console.log(x), [m])
等价于React.useMemo(()=> (x)=> console.log(x), [m])
useRef
1、目的
- 当我们需要一个值,它需要在组件不断render的时候保持不变;
- 初始化:
const count = React.useRef(0)
; - 读取:
count.current
; - 为什么需要current呢?因为这样就能保证每次渲染后的useRef都是同一个值(因为只有引用可以做到);
2、count.current的值变化时不会自动render
- 如果想让count.current的值变化时可以自动render:
①调用一下setN();
②用vue3,vue3的ref改变会自动render,详见文档
forwardRef
1、什么时候用到它呢
- 如果你希望一个函数组件支持ref属性,那么你就需要拿forwardRef把这个函数组件给包起来;
- 当你要向你的子组件传递ref引用时,就要用到forwardRef(初步理解,使用过程中有其他感悟再来添加~~);
- 因为props不包含ref;那为什么props不包含ref呢?因为大多数时间都用不到;
- 文档
useImperativeHandle
- 名字起得不好,应该叫setRef;
- 具体可看文档和PPT上的例子;
useState / useReducer ===> 它们里面的n每次都变;
useMemo / useCallback ===> 当依赖[m]变的时候,返回的值就变化;
useRef ===> 它的值永远不变;
自定义Hook
- 封装数据操作,return出来增删改查的接口,例子
- 项目中尽量使用自定义Hook;
Stale Closure
- Stale Closure:过时的闭包;用来描述你的函数引用的变量是之前产生的变量;
- 看下这篇文章的几段代码,你就明白了;
总结
- useContext,用来把读写接口给整个页面用;
- useReducer,专门给Redux的用户设计的,我们甚至可以不用它;
- useMemo,和React.Memo配合使用;
- forwardRef并不是一个Hook;