0%

React Hook

  React 16.8版本开始,引入了Hooks机制。Hooks机制允许在无class的情况下使用state以及其他React特性。在React中,钩子函数是用来让函数组件拥有局部状态和其他React特性(例如生命周期)的特殊函数。Hooks的出现使得React的代码变得简洁、易于维护。

钩子函数:某个阶段触发的回调函数。 例:vue的生命周期函数就是钩子函数
常用的钩子函数:

  1. useState【维护状态】
  2. useEffect【完成副作用操作】
  3. useContext【使用共享状态】
  4. useReducer【类似redux】
  5. useCallback【缓存函数】
  6. useMemo【缓存值】
  7. useRef【访问DOM】
  8. useImperativeHandle【使用子组件暴露的值/方法】
  9. useLayoutEffect【完成副作用操作,会阻塞浏览器绘制】

useState

普通更新/函数式更新 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Index = () => {
const [count, setCount] = useState(0);
const [obj, setObj] = useState({ id: 1 });
return (
<>
{/* 普通更新 */}
<div>count:{count}</div>
<button onClick={() => setCount(count + 1)}>add</button>

{/* 函数式更新 */}
<div>obj:{JSON.stringify(obj)}</div>
<button
onClick={() =>
setObj((prevObj) => ({ ...prevObj, ...{ id: 2, name: "张三" } }))
}
>
merge
</button>
</>
);
};

useEffect

useEffet 我们可以理解成它替换了 componentDidMount, componentDidUpdate, componentWillUnmount 这三个生命周期,但是它的功能还更强大。
包含3个生命周期的代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
useEffect(
() => {
// 这里的代码块 等价于 componentDidMount
// do something...

// return的写法 等价于 componentWillUnmount(可选)
return () => {
// do something...
};
},
// 依赖列表,当依赖的值有变更时候,执行副作用函数,等价于 componentDidUpdate
[ xxx,obj.xxx ]
);


注意:依赖列表是灵活的,有三种写法

  1. 当数组为空 [ ],表示不会因为页面的状态改变而执行回调方法【即仅在初始化时执行,componentDidMount】
  2. 当这个参数不传递,表示页面的任何状态一旦变更都会执行回调方法
  3. 当数组非空,数组里的值一旦有变化,就会执行回调方法

执行时机:

● 在浏览器完成渲染(包括 DOM 更新和屏幕绘制)之后异步执行。
● 不会阻塞浏览器的绘制,因此用户体验更流畅。
● 适用于大多数副作用,如数据获取、事件订阅、设置定时器等。

场景1:

我依赖了某些值,但是我不要在初始化就执行回调方法,我要让依赖改变再去执行回调方法
我们这里有用到了 useRef 这个钩子:

1
2
3
4
5
6
7
8
9
10
const 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 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树 的逐层传递 props

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
const 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
51
const 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
2
3
4
5
6
7
// 除非 `a` 或 `b` 改变,否则不会变
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[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
31
const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const NumberProcessor = () => {
const [numbers,setNumbers] = useState([1, 2, 3, 4, 5]); // 固定数组
const [count, setCount] = useState(0);

// 用 useMemo 缓存计算结果(仅当 numbers 变化时才重新计算)
const sum = useMemo(() => {
console.log("开始计算总和"); // 用于观察计算次数
return numbers.reduce((total, num) => total + num, 0);
}, [numbers]);

return (
<div>
<button onClick={() => {
setCount(count + 1);
// setNumbers([...numbers, numbers.length + 1]);
}}>
点击次数:{count}
</button>
<p>数组总和:{sum}</p>
</div>
);
};

useRef

我们用它来访问DOM,从而操作DOM,如点击按钮聚焦文本框:

1
2
3
4
5
6
7
8
9
10
11
12
const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Index = () => {
const inputEl = useRef(null);
const handleFocus = () => {
inputEl.current.focus();
};
return (
<>
<Child ref={inputEl} />
<button onClick={handleFocus}>Focus</button>
</>
);
};

const Child = forwardRef((props, ref) => {
return <input ref={ref} />;
});

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
33
const 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 同步修改的场景。

赏包辣条吃吧