一个大基调,函数组件的每一次更新,就是把函数组件重新执行,这样每次会产生新的闭包,而闭包中所有创建函数的操作,都会重新创建新的堆内存。
useState
setState 是直接赋值,而不是局部替换
js
const [obj, setObj] = useState({ name: '颜酱', age: 18 });
setObj({ name: '换一个名字' });
上面的语句,这样 set 之后,obj 的值会变成{name:'换一个名字'}
,而没有 age 字段。
所以对象赋值,需要拿到之前的,然后覆盖
js
const [obj, setObj] = useState({ name: '颜酱', age: 18 });
setObj({ ...obj, name: '换一个名字' });
组件需要多个属性的时候,【推荐尽量分开写 state】,而不是用一个 obj 涵盖所有。
setState 是异步的
js
const [name, setName] = useState('颜酱');
setName('花花');
// 是 颜酱,因为更新是异步的
console.log(name);
上面的输出是颜酱
,setName 修改值不是同步操作,所以 name 仍然是旧值。、
useState 的 set 操作会有一个更新队列
setXX 之后,并不会立马更新视图,而是先进去队列,然后批次修改,更新视图
js
import { useState } from 'react';
export default function Counter() {
console.log('渲染');
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(1);
function handleClick() {
setCount(count + 1);
setCount1(count1 + 1);
}
return <button onClick={handleClick}>You pressed me {count} times</button>;
}
以上代码,初始进来,打印一次渲染
,点击按钮之后,虽然有两次 setXXX,但其是在一个队列里,只更新视图一次,所以,也只打印一次渲染
如果需要同步更新,使用flushSync
js
function handleClick() {
flushSync(() => {
setCount(count + 1);
});
setCount1(count1 + 1);
}
flushSync
,会直接截断当前队列,然后更新。 所以点击按钮之后,会打印 2 次渲染
useState 的闭包逻辑
js
import { useState } from 'react';
export default function Counter() {
console.log('渲染');
const [count, setCount] = useState(0);
function handleClick() {
for (let i = 0; i < 10; i++) {
setCount(count + 1);
}
}
return <button onClick={handleClick}>数字 {count}</button>;
}
点击一次按钮之后,打印了几次渲染
,count 是多少?
count 初始是 0,点击按钮之后,handleClick 执行,注意,这里形成上下文,count 是 0,在 handleClick 的上级作用域
- 循环第一次,count 往上寻,所以是 0,setCount(0+1)
- 循环第二次,count 往上寻,所以是 0,setCount(0+1)
- ... 其实循环多少次多一样,最后一次循环结束,队列开始执行,只执行一次 setCount(1) 视图更新,重新执行 Counter,所以只打印一次
渲染
,count 是 1
如果循环体里加上flushSync
呢?
js
function handleClick() {
for (let i = 0; i < 10; i++) {
flushSync(() => {
setCount(count + 1);
});
}
}
点击一次按钮之后,打印了几次渲染
,count 是多少?
count 初始是 0,点击按钮之后,handleClick 执行,注意,这里形成上下文,count 是 0,在 handleClick 的上级作用域
- 循环第一次,count 往上寻,所以是 0,setCount(0+1),因为是 flushSync,所以截断队列,视图更新,重新执行 Counter,所以此时打印一次
渲染
,count 是 1 - 循环第二次,count 往上寻,所以是 0,setCount(0+1),因为是 flushSync,所以截断队列,但是因为值是 1 同 上一次,所以视图并不更新
- ...
所以只打印一次渲染
,count 是 1。也有可能会打印 2 次渲染,因为视图还没有完全渲染,所以有了第二轮。
换成setTimeout
呢
js
function handleClick() {
for (let i = 0; i < 10; i++) {
setTimeout(() => {
setCount(count + 1);
}, i * 1000);
}
}
其实也一样。
那怎么修改循环体内的代码,让其变成 10 呢
setState使用函数参数
上面的答案就是
js
function handleClick() {
for (let i = 0; i < 10; i++) {
setCount((prev) => prev + 1);
}
}
点击按钮之后,handleClick 执行
- 循环第一次,prev 是 0,setCount(0+1),进入队列
- 循环第二次,prev 是 1,setCount(1+1),进入队列
- ...
- 循环第十次,prev 是 9,setCount(9+1),进入队列
队列执行,合并成最后一次,setCount(10),视图更新,所以此时打印一次渲染
,count 是 10
初始 state 的计算逻辑复杂的话,用函数赋值
js
import { useState } from 'react';
export default function Counter() {
/** 这一段都是count初始值的逻辑 */
let init = 0;
for (let i = 0; i < 10; i++) {
init++;
}
/** 这一段都是count初始值的逻辑 */
const [count, setCount] = useState(init);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>数字 {count}</button>;
}
上面这样有什么不好呢?
点击按钮,更新 count,视图更新,Counter 重新执行,此时,这段逻辑又重新走了一遍,但是因为初始值只有第一次生效,所以这里执行没有任何意义。怎么让其只执行一次呢。
把函数放进参数里,返回值是初始值就可以啦,这样值更新的时候,并不会走这里的逻辑
js
const [count, setCount] = useState(() => {
let init = 0;
for (let i = 0; i < 10; i++) {
init++;
}
return init;
});
useState 的简单源码理解
简单写下 useState 的实现,帮助理解使用逻辑:
js
let state;
function useState(value) {
const isFirst = state === undefined;
const isFunc = typeof value === 'function';
// 第一次执行useState的时候,state肯定没有赋值过,如果参数是函数,那就用函数返回值赋值,否则直接赋值
if (isFirst) {
state = isFunc ? value() : value;
}
// setState是个函数
const setState = (newValue) => {
const isFunc = typeof newValue === 'function';
const prev = state;
// 如果参数是个函数,将上一次的值传过去,执行函数,否则直接赋值
state = isFunc ? newValue(prev) : newValue;
// 如果新值和旧值是一样的,就不需要视图更新了,否则通知视图更新
if (newValue === prev) return;
// 通知视图更新(视图更新的逻辑里,组件函数会重新执行)
};
return [state, setState];
}
useEffect
useEffect 常用的四种情况:
js
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(1);
useEffect(() => {
console.log('@1 初始渲染和每次重新渲染都会执行', count);
});
useEffect(() => {
console.log('@2 只有初始渲染执行,注意后面的空依赖', count);
}, []);
useEffect(() => {
console.log('@3 初始渲染和每次依赖项count变更重新渲染后执行,注意后面的依赖', count);
}, [count]);
useEffect(() => {
return () => {
console.log(
'@4 每次依赖项count变更重新渲染后,React 将首先使用旧值运行 此cleanup 函数',
count,
);
};
}, [count]);
function handleClick() {
setCount(count + 1);
}
function handleClick1() {
setCount(count1 + 1);
}
return (
<>
<button onClick={handleClick}>数字 {count}</button>
<button onClick={handleClick1}>数字 {count1}</button>
</>
);
}
useEffect(fn)
,初始渲染和每次重新渲染,fn 都会执行,雷同于mounted
和updated
useEffect(fn,[])
,只有初始渲染,fn 会执行,雷同于mounted
useEffect(fn,[count])
,初始渲染和依赖项 count 变更重新渲染后,fn 会执行useEffect(()=>fn,[count])
,依赖项 count 变更重新渲染后,React 将首先使用旧值运行 fn。在组件从 DOM 中移除后,React 将最后一次运行 fn 函数,这个类似destroyed
。
请求数据
如果你这么请求数据的话
js
useEffect(async () => {
let data = await apiData();
console.log(data);
});
那就会报错,因为这个 async 函数会返回Promise实例
,而 useEffect 如果有返回值的话必须返回一个函数。
请求数据,简单点就是 async 用新函数包下执行,或者 then.
js
useEffect(() => {
const apiData = async () => {
let data = await apiData();
console.log(data);
};
apiData();
});
useLayoutEffect
先理解下视图更新的步骤:
- 基于 babel-preset-react-app 把 JSX 编译为 createElement 格式
- 把 createElement 执行,创建出 virtualDoM
- 基于 root.render 方法把 virtuaLDOM 变为真实 DOM 对象 「DOM-DIFFJ 【useLayoutEffect 阻塞第四步操作,先去执行 Effect 链表中的方法「同步操作」
seEffect 第四步操作和 Effect 链表中的方法执行,是同时进行的「异步操作」】 - 浏览器渲染和绘制真实 DOM 对象
useLayoutEffect 其实不是很常用,根据上面的描述,其useEffect
的差别在于:
useLayoutEffect
会阻塞浏览器渲染真实 DOM,优先执行 Effect 链表中的 callback;useEffect
不会阻塞浏览器渲染真实 DOM,在渲染真实 DOM 的同时,去执行 Effect 链表中的 cal1back;uselayoutEffect
设置的callback
要优先于useEffect
中的去执行!- 在两者设置的 callback 中,依然可以获取 DOM 元素「原因:真实 DOM 对象已经创建了,区别只是浏览器是否渲染」
如果在 callback 函数中又修改了状态值「视图又要更新」
- useEffect:浏览器肯定是把第一次的真实已经绘制了,再去渲染第二次真实 DOM
- uselayoutEffect:浏览器是把两次真实 DOM 的渲染,合并在一起渲染的
js
useEffect(() => {
console.log('useEffect '); //第二个输出
}, [num]);
useLayoutEffect(() => {
console.log('useLayoutEffect'); //第一个输出
}, [num]);
useRef 和 useImperativeHandle
useRef
主要获取真实 DOM、子组件实例。
举一个子组件的例子,子组件用forwardRef useImperativeHandle
改装下
js
// 子组件
import { forwardRef } from 'react';
const Child = forwardRef(({ value, onChange }, ref) => {
return <input value={value} onChange={onChange} ref={ref} />;
});
// 父组件
const Parent = () => {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
};
const Child = React.forwardRef(function Child(props, ref) {
let [text, setText] = useState('475');
const submit = () => {};
useImperativeHandle(ref, () => {
//在这里返回的内容,都可以被父组件的REF对象获取到
return {
text,
submit,
};
});
return (
<div className="child-box">
<span>哈哈哈</span>
</div>
);
});
const Demo = function Demo() {
let x = useRef(null);
useEffect(() => {
console.log(x.current.text);
});
return (
<div className="demo">
<Child ref={x} />
</div>
);
};
useMemo
useMemo 和vue的computed
非常相似。
js
import { useState, useMemo } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(1);
const handleClick = useCallback(() => {
setCount(count + 1);
});
const handleClick1 = useCallback(() => {
setCount(count1 + 1);
});
const sum = useMemo(()=>{
return count + count1
},[count,count1])
return (
<>
<button onClick={handleClick}>数字 {count}</button>
<button onClick={handleClick1}>数字 {count}</button>
<div>{sum}</div>
</>
);
}
total = useMemo(fn,[count])
,初始渲染执行 fn,后期依赖项 count 发生变化才会执行 fn。fn 的结果返回给total
。 useMemo 是优化操作,如果某个值需要复杂的逻辑计算,建议这样操作,减少不必要的运算,提高组件更新速度。
useCallback
每次组件更新的时候,组件函数内,所有的函数都会重新创建。如果不希望内部函数每次都重新创建,就用useCallback
包起来。
js
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
上面的只有依赖项发生变化的时候,handleSubmit 函数才会被重新创建。 但是不建议每个内部函数都用useCallback
包起来,因为useCallback
处理的逻辑和缓存机制本身也消耗时间,其代价可能不比创建新的函数小。
适合场景:父组件将内部函数传递给子组件的时候
js
const Child = React.memo(() => {
console.log('子组件渲染');
return <div>子组件</div>;
});
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
});
return (
<>
<button onClick={handleClick}>数字 {count}</button>
<Child handleClick={handleClick} />
</>
);
}
注意两点:
- handleClick 用 useCallback 包起来,然后传给子组件
- 子组件函数用
React.memo
包起来
只有这样,点击按钮的时候,子组件才不会每次都重新渲染。useCallback
保证函数的引用地址一致。React.memo
判断如果新老属性一致的话,则不更新子组件。