# hooks 给 react 生态带来了什么变化
不再需要类组件,推动了函数式编程
代码复用
简化的状态和副作用管理
简化了组件的生命周期
# usecallback 的用处
缓存函数
函数需要传给子组件的场景下,可以避免子组件渲染
# ref 的用法
基于 useRef()、React.createRef() 创建
直接操作 dom 元素
可以获取组件实例,从而调用实例方法、获取实例属性
创建一个不被渲染影响的对象,且变更也不会导致渲染;由于这个特性可以保存上一次渲染的状态,可以得到渲染原因
# useEFfect 与 useLayoutEFfect 的区别
useLayoutEFfect
,完成布局之后开始绘制之前触发(同步执行),会阻塞浏览器的绘制,确保在绘制之前完成了所有的 dom 操作- 会阻塞渲染,所以适合 操作 dom、影响布局,如:修改 dom 布局、获取元素尺寸、位置
useEffect
,完成布局和绘制之后触发(异步执行),不会阻塞浏览器绘制,允许页面在副作用执行之前将页面先显示给用户- 不会影响页面的首次绘制,所以适合做数据请求、事件监听
# useMemo 的返回值
- 返回的是一个被缓存的值,如果依赖项不变,那么不会执行重新计算,而是返回缓存值
# useMemo 的应用场景
计算量大的操作,用 useMemo 缓存,避免每次渲染都要重新计算
可以避免不必要的重新渲染,比如组件的依赖项是对象或者数组的时候,用 useMemo 缓存一下
# 函数式组件如何模拟生命周期钩子
useEffect [] 可以模拟 componentDidMount
useEffect [key] 可以模拟 componentDidUpdate
useEffect [] 设置 return 可以模拟 componentWillUnMount
React.memo 浅比较 props 可以模拟 shouldComponentUpdate
# 封装过的 hooks
window.addEventListener('resize', handleResize); 监听窗口变更,获取指定 dom 元素的 offsetHeight
new URLSearchParams(window.location.serach) 获取路由?后的参数
window.matchMedia('(prefers-color-scheme: dark)') 媒体查询监听浏览器模式改变
# 函数组件和 class 组件区别
语法不同,函数组件 没有 this,接受 props 返回 jsx 结构;class 组件,有 this,通过 this 访问 props 和 state,需要定义 render()方法
状态管理,函数组件依赖 hooks 和 useState;class 组件靠 this.state 和 this.setState()
生命周期,函数组件靠 hooks;class 组件有生命周期
性能优化,函数组件依靠 memo;class 组件靠 PureConponent
# react 的父子兄组件的渲染顺序
初次渲染(mount)的渲染顺序
- 从父组件开始,递归往下渲染子组件;如果有多个子组件,按照 jsx 的顺序依次渲染
组件更新(rerender)的渲染顺序。
父组件的 render 方法首先执行,重新渲染父组件的 jsx 内容
按声明顺序重新渲染子组件
如果子组件的属性或者状态没有改变,那么可能会跳过子组件的渲染(需要依赖 memo PureComponent shouldComponentUpdate 等优化手段)
声明周期函数的调用顺序
父 shouldComponentUpdate
父 render
子 shouldComponentUpdate
子 render
子 componentDidUpdate
父 componentDidUpdate
函数组件的调用顺序
父 render
子 render
子 useLayoutEffect
父 useLayoutEffect
子 useEffect
父 useEffect
# redux 的底层运行机制
redux
是一个状态管理工具,核心思想是将应用的状态存储在一个全局的 store 里面,然后通过单向数据流和纯函数来管理状态store
状态树,全局唯一,可以通过 getState()获取状态action
动作,所有状态更新都是通过 dispatch(action) 发送 action 来触发的。action 包含 type 和 payloda 载荷reducer
纯函数,根据传入的 action 来决定如何更改状态,接受当前状态和 action,返回新的状态dispacth
派发,将 action 发给 reducer,根据 reducer 的返回结果更新状态subscribe
订阅监听,当状态变化时候,redux 调用订阅的监听器函数,让应用响应状态的变化单向数据流
,用户通过 dispatch 发出 action,store 接受到 action 并传给 reducer,reducer 根据 action 和当前状态返回新的状态,store 更新状态并通知视图层重新渲染中间件
,在 action 发出和到达 reducer 的之前拦截,可以处理日志记录、异步操作之类的
# redux 适合场景
复杂的状态管理。适合模块多,而且大量模块状态共享 的场景。
跨组件的状态共享。避免了逐层传递 props。
可预测的状态变化。store 由 reducer 和 action 完成更改,整个流程清晰可预测。
缺点:不适合局部状态管理
# useContext 的底层运行机制
Contenx。可以将 状态或者功能 定义在 Contenx 中,子组件都可以访问这些数据,无需层层传递 props。
Provider。使用
Contenx.Provider
包裹组件树,并且提供一个 value 属性,这样子组件就可以通过 useContenx 访问这些值。useContenx hook。消费 Contenx 内的数据
# useContext 适合场景
简单状态管理
局部状态共享
优点:react 自带的 hook,不需要引入三方库;适合局部状态管理,也可以避免逐层传递 props
缺点:任何的状态变化都会导致消费了该 contenx 的组件重新渲染
# redux 为什么不会引起消费了 store 的组件重新渲染
使用
connect 高阶组件
和mapStateToProps 选择性订阅部分store
,从而实现明确声明组件使用的那一部分 store 状态connect 内部使用了浅比较,只有 props 的数据发生了变化才进行渲染
reducer,每个 reducer 只负责一部分的 store 状态,其他部分不受影响;reducer 是个纯函数,本身是没有状态的,他只负责他负责的那一部分的 store 更新成新的对象,从而使得 react 可以根据 store 的对象地址变化来确定哪部分需要更新
可以继续选择性拆分 reducer,使得每个 reducer 可以变更的 store 更小
可以使用 memo PureComponent 来避免不必要的 rerender
# useContext 为什么会引起消费其 contenx 的组件重新渲染,怎么优化
为什么会引起这个现象
- 因为用法,如下,当 user、count 任何一个改变的时候,value 都会返回一个新的对象,所以整个 MyContenx.Provider 都重新渲染了; 而且 子组件使用 useContenx 消费 value 的时候,useContenx 不支持选择性订阅
const MyContenx = createContenx();
const Contanier = () => {
const [user, setUser] = useState('');
const [count, setCount] = useState(0);
return (
<MyContenx.Provider value={{ user, setUser, count, setCount }}>
<ChildrenConponent />
</MyContenx.Provider>
);
};
优化方案
contenx 拆分
子组件使用 memo 包裹,这样只针对 props 变更的场景才会触发 rerender,这一步是针对 props 进行的优化
再将 Contenx 拆分,而且对应的 value 值用 useMemo,比如:将 user(因为 user 不会经常改变)的 contenx 提取到最外面,count 放在里面一层,这样的话 count 对象地址变更不会触发 user 的对象地址变更,从而不会导致其他 contenx 的 rerender; 使用 useMemo 的原因是其中一个 value 变更,其他的 contenx 的 value 地址不变,从而使得使用 useContenx 消费的值也不变,从而避免 rerender
缺点:value 太多的情况下,拆分的工作量很大,维护起来很复杂
useReducer
- 先看下 useReducer 的用法
const MyContenx = createContenx() const initStae = { user: { name: 'xx' }, count: 0 } const reducer = (state, action) => { switch(action.type) { case 'setUser': return { ...state, user: action.payload } case 'setCount' return { ...state, count: action.payload } default: return state } } const ContainerEl = () => { const [state, dispatch] = useReducer(reducer, initState) const countValue = useMemo(() => ({ state.count, dispatch }), [state.count, dispatch]) const userValue = useMemo(() => ({ state.user, dispatch }), [state.user, dispatch]) return ( <MyContenx.Provider value={{ countValue, userValue }}> <ChildrenConponent /> </MyContenx.Provider> ) }
使用 useReducer 集中管理状态,每次更新值更新对应部分的 value;使用 useMemo 缓存不同类型的 value;使用 memo 优化子组件避免 props 引起不必要的 rerender
这样 Provider 的 value 即使是一个内联对象,但是其内部对象的对象地址是 useMemo 包裹的,这样的话其子组件使用 useContenx 消费的时候就不会 rerender 了
selector 模式
状态选择性订阅。selector 模式让组件只订阅它关心的状态片段,而不是整个 contenx 的 value;当某个片段的状态更新的时候,只有其依赖的组件 rerender,其他组件不受影响
状态的深层次比较或自定义比较。selector 模式下可以对状态进行深层次比较(在自定义 hook 内进行),只有实际值变化,组件才会 rerender,这样可以避免是组件的引用地址变化了导致的 rerender
用法。实际原理是:子组件通过自定义 hook,不再需要在子组件内部使用 useContenx(),这样就避免了其他 value 变化引起的 useContenx()变化,从而实现选择性订阅
const MyContenx = createContenx(); const ContanierEl = () => { const [count, setCount] = useState(0); const [user, setUser] = useState({ name: 'xx', age: 18 }); return ( <MyContenx.Provider value={{ user, setUser, count, setCoun }}> <ChildrenComponent /> </MyContenx.Provider> ); }; // 定义自 selector hook const useCount = () => { const { count, setCount } = useContenx(MyContenx); return { count, setCount }; }; const useUser = () => { const { user, setUser } = useContenx(MyContenx); return { user, setUser }; }; // 子组件 const ChildrenComponet = () => { const { count, setCount } = useCount; // ... };