useState
基本使用
js
// 使用方法一:初始值为基础数据类型或Object
const [state, setState] = useState(initialState);
// 使用方法二:初始值为函数
const [state, setState] = useState(() => initialState);
@param useState
接受一个参数,作为 state
的初始值,这个参数可以是任何数据类型 ,也可以是函数
@return useState
返回一个数组,数组中包括 someState
数据源和更新这个 state
的方法 setSomeState
使用
js
const [name, setName] = useState('React'); // 参数是String
const [age, setAge] = useState(9); // 参数是Number
const [features, setFeatures] = useState([{ text: 'JSX' }]); // 参数是Object
const [count, setCount] = useState(() => {
// 先执行一定的逻辑,后再返回初始值
const initialState = computedBaseCount(props);
return initialState;
}); // 参数是Function
注意事项
-
状态更新可能是异步的:React可能会延迟执行状态更新,以提高性能。因此,如果状态更新依赖于当前状态,最好使用函数形式的更新:
iniJavaScript 代码解读 复制代码 setCount(currentCount => currentCount + 1);
-
状态更新是替换而不是合并 :与类组件中的
this.setState
方法不同,更新状态变量总是替换它,而不是合并。因此,如果状态是对象或数组,需要注意手动合并或构造新的状态
当你调用
useState
时,React会在内部为当前组件保持一个状态,并在组件的每次渲染之间保持这个状态。当你通过调用状态更新函数(如上例中的setCount
)更新状态时,React会重新渲染组件,并使用最新的状态值
原理分析
1. Hooks 是如何存储状态的
React为每个组件维护了一个状态链表。每次组件渲染时,React都会按照Hooks被调用的顺序,从这个链表中读取或更新状态。这就是为什么Hooks必须在组件的最顶层调用,且不能在循环、条件语句或嵌套函数中调用的原因------这确保了每次组件渲染时Hooks的调用顺序是一致的。
2. 组件渲染和更新
当你使用useState
定义了一个状态变量和一个更新这个变量的函数后,每当状态更新函数被调用时,React会将新的状态值入队,然后触发组件的重新渲染。在组件的下一个渲染周期中,useState
会返回最新的状态值。
3. 批量更新和异步更新
React对状态更新采用了批处理和异步更新的策略来优化性能。当多个状态更新函数在React事件处理函数或生命周期方法中被同步调用时,React会将这些更新批量处理,延迟更新直到事件处理完成,然后一次性应用所有更新并重新渲染组件。这减少了不必要的渲染次数和计算,提高了应用的性能。
4. 函数式更新
如果新的状态依赖于旧的状态,推荐使用函数式更新。这是因为状态更新可能是异步的,直接使用旧状态可能会导致不一致的结果。函数式更新确保了每次更新都使用最新的状态值
源码分析
useEffect
可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
useEffect需要传递两个参数,第一个参数是逻辑处理函数,第二个参数是一个数组
js
useEffect(() => {
/** 执行逻辑 */
},[])
1、第二个参数存放变量,当数组存放变量发生改变时,第一个参数,逻辑处理函数将会被执行
2、第二个参数可以不传,不会报错,但浏览器会无限循环执行逻辑处理函数。
js
useEffect(() => {
/** 执行逻辑 */
})
3、第二个参数如果只传一个空数组,逻辑处理函数里面的逻辑会在组件挂载时执行一次 ,会在组件被销毁前执行一次,不就是相当于 componentDidMount 和 componentWillUnmount。
js
useEffect(() => {
/** 执行逻辑 */
},[])
4、第二个参数如果不为空数组,如下
js
const [a, setA] = useState(1);
const [b, setB] = useState(2);
useEffect(() => {
/** 执行逻辑 */
},[a,b])
逻辑处理函数会在组件挂载时执行一次和(a或者b变量在栈中的值发生改变时执行一次) 这是不是相当于componentDidMount 和 componentDidUpdate 的结合
5、useEffect第一个参数可以返回一个回调函数,这个回调函数将会在组件被摧毁之前和再一次触发更新时
,将之前的副作用清除掉。这就相当于componentWillUnmount。
useEffect去除副作用。我们可能会在组件即将被挂载的时候创建一些不断循环的订阅(计时器,或者递归循环)。在组件被摧毁之前,或者依赖数组的元素更新后,应该将这些订阅也给摧毁掉。
比如以下的情况(没有去除计时器,增大不必要的开销和代码风险)
js
const [time, setTime] = useState(0)
useEffect(() => {
const InterVal = setInterval(() => {
setTime(time + 1)
},1000)
},[])
利用第五点,在组件被摧毁前去除计时器。
const [time, setTime] = useState(0)
js
useEffect(() => {
const InterVal = setInterval(() => {
setTime(time + 1)
},1000)
return () => {
clearInterval(InterVal )
}
},[])
useLayoutEffect
useLayoutEffect 与 useEffect 使用方式是完全一致的,区别的是 两者的执行时机
useEffect()是在DOM更新后(浏览器重绘完成)执行的,是异步的;useLayoutEffect()是在渲染之后但在屏幕更新之前执行的,是同步的。二者均是等待jsx执行完毕后再执行,但useLayoutEffect()在useEffect()之前触发。 大部分情况下采用useEffect(),因为它的性能更好。但当你的useEffect中需要操作dom,并且会改变页面的样式,就需要使用useLayoutEffect(),否则可能会出现闪屏问题
- react 在 diff 后,会进入到 commit 阶段,准备把虚拟 DOM 发生的变化映射到真实 DOM 上
- 在 commit 阶段的前期,会调用一些生命周期方法,对于类组件来说,需要触发组件的 getSnapshotBeforeUpdate 生命周期,对于函数组件,
此时会调度 useEffect 的 create destroy 函数
,注意是调度
,不是执行
。在这个阶段,会把使用了 useEffect 组件产生的生命周期函数入列到 React 自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行
- 随后,就到了 React 把虚拟 DOM 设置到真实 DOM 上的阶段,这个阶段主要调用的函数是 commitWork,commitWork 函数会针对不同的 fiber 节点调用不同的 DOM 的修改方法,比如文本节点和元素节点的修改方法是不一样的。
- commitWork 如果遇到了类组件的 fiber 节点,不会做任何操作,会直接 return,进行收尾工作,然后去处理下一个节点,这点很容易理解,类组件的 fiber 节点没有对应的真实 DOM 结构,所以就没有相关操作,但在有了 hooks 以后,函数组件在这个阶段,
会同步调用上一次渲染时 useLayoutEffect(create, deps) create 函数返回的 destroy 函数
- 注意一个节点在 commitWokr 后,这个时候,我们已经把发生的变化映射到真实 DOM 上了
- 但由于 JS 线程和浏览器渲染线程是互斥的,因为 JS 虚拟机还在运行,即使内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上
此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执行。
- 对于 react 来说,commit 阶段是不可打断的,会一次性把所有需要 commit 的节点全部 commit 完,至此 react 更新完毕,JS 停止执行
- 浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成
- 浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生的函数
useRef
useRef Hook 的作用主要有两个:
- 多次渲染之间保证唯一值的纽带。
useRef 会在所有的 render 中保持对返回值的唯一引用。因为所有对ref
的赋值和取值拿到的都是最终的状态,并不会因为不同的 render 中存在不同的隔离。
这点我们在开头的 useEffect Hook 中就已经展示了它的示例,判断是否是由于页面更新而非首次渲染:
ts
import { useRef } from 'react';
export function useFirstMountState(): boolean {
const isFirst = useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
}
- 获取 Dom 元素,在 Function Component 中我们可以通过 useRef 来获取对应的 Dom 元素。
useContext
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
熟悉 React 中Context Api 和 Vue 中的 provide/inject Api 的同学可能会对这个钩子的作用深有体会。
假设这样一种场景:
在根级别组件上我们需要向下传递一个用户名 username 的属性给每一个子组件进行使用。
此时,如果使用 props 的方法进行层层传递那么无疑是一种噩梦。而且如果我们的 G 组件需要使用 username 但是 B、E 并不需要,如果使用 props 的方法难免在 B、E 组件内部也要显式声明 username。
React 中正是为了解决这样的场景提出来 Context Api。
可以通过 React.createContext 创建 context 对象,在跟组件中通过 Context.Provider 的 value 属性进行传递 username ,从而在 Function Component 中使用 useContext(Context) 获取对应的值。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer 接受三个参数分别是 reducer 函数、初始值 initialArg 以及一个可选的惰性初始化的 init 函数。
它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。
让我们通过一个简单的计数器例子来了解一下它的基础用法:
ts
import { useReducer } from 'react';
interface IState {
count: number;
}
interface IAction {
type: 'add' | 'subtract';
payload: number;
}
const initialState: IState = { count: 0 };
const reducer = (state: IState, action: IAction) => {
switch (action.type) {
case 'add':
return { count: state.count + action.payload };
case 'subtract':
return { count: state.count - action.payload };
default:
throw new Error('Illegal operation in reducer.');
}
};
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<h1>Hello , My name is 19Qingfeng.</h1>
<p>Counter: {state.count}</p>
<p>
<button onClick={() => dispatch({ type: 'add', payload: 1 })}>
add 1!
</button>
</p>
<p>
<button onClick={() => dispatch({ type: 'subtract', payload: 1 })}>
subtract 1!
</button>
</p>
</>
);
}
export default Counter;
这里我们创建了一个简单的 Counter 计数器组件,内部通过 useReducer 管理 couter 的状态
useMemo和useCallBack
useMemo
解决数是复杂数据类型,子组件仍会渲染的问题
使用场景:
假设以下场景,父组件在调用子组件时传递 info 对象属性,点击父组件的点击增加按钮时,发现控制台会打印出子组件被渲染的信息。
javascript
import React, { memo, useState } from 'react';
// 子组件
const ChildComp = (props:{info:{name, age}}) => {
console.log('ChildComp...',name,age);
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = { name, age };
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info}/>
</div>
);
};
export default Parent;
分析原因:
点击父组件按钮,触发父组件重新渲染;父组件渲染,const info = { name, age } 一行会重新生成一个新对象,导致传递给子组件的 props 变化,进而导致子组件重新渲染。
解决方法:
使用 useMemo 将对象属性包一层。useMemo 有两个参数:
- 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
- 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。 下面请看改进后的代码:
typescript
import React, { memo, useMemo, useState } from 'react';
// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
// 使用 useMemo 将对象属性包一层
const info = useMemo(() => ({ name, age }), [name, age]);
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info}/>
</div>
);
};
export default Parent;
useMemo
scss
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo返回一个被记忆的值 !!!。
注意传入 useMemo的依赖项,这样的话useMemo它仅会在某个依赖项改变时才重新计算 memoized 值
,这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
useCallback
包裹函数,依赖改变时返回新的函
用法
useCallback的第一个参数是函数体,第二个参数是依赖项,只有依赖项中的变量改变时,才会返回一个新的函数
ini
const myFunction=( ()=>{
函数体...
}, [])
demo案例:
紧接着上面useMemo的例子,假设需要传给子组件一个函数,如下所示,当点击父组件按钮时,发现控制台会打印出子组件被渲染的信息,说明子组件又被重新渲染了。
javascript
import React, { memo, useMemo, useState } from 'react';
// 子组件
const ChildComp = (props) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = useMemo(() => ({ name, age }), [name, age]);
const changeName = () => {
console.log('输出名称...');
};
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info} changeName={changeName}/>
</div>
);
};
export default Parent;
分析原因:
点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值
),进而导致父组件重新渲染;父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 changeName 属性发生了变化,子组件props发生变化从而导致子组件渲染;
解决方法:
修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层, useCallback 参数与 useMemo 类似
使用 useCallback 将函数包一层,useCallback 有两个参数:
- 第一个参数是个函数,返回的函数指向同一个引用,不会创建新函数;
- 第二个参数是个数组,第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,就会重新返回一个新的记忆函数提供给后面进行渲染。如果是一个空数组则是无论什么情况下该函数都不会发生改变。
javascript
import React, { memo, useCallback, useMemo, useState } from 'react';
// 子组件
const ChildComp = (props) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = useMemo(() => ({ name, age }), [name, age]);
const changeName = useCallback(() => {
console.log('输出名称...');
}, []);
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info} changeName={changeName}/>
</div>
);
};
export default Parent;
优化后分析:
用useCallback包裹后,父组件render时,包裹后的函数因为依赖项不变所以还是用记忆函数,则MemoChildComp组件中changeName并没有发生改变,那个子组件props也没有改变,也就不会进行rerender。
补充: useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
scss
useCallback(fn, inputs) 就相当于 useMemo(() => fn, inputs).
前面使用 useCallback 的例子可以使用 useMemo 进行改写:
javascript
这是通过useMemo进行改写上面useCallback这个例子。
const changeName = useMemo(() => () => {
console.log('输出名称...');
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
唯一的区别是:useCallback不会执行第一个参数函数
,而是将它返回给你,而useMemo会执行第一个函数并且将函数执行结果返回给你
。
总结: useCallback 常用记忆事件函数 ,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值