一、React hooks 不能在哪些地方使用?
它可以让我们不编写类组建的情况下 使用 状态和生命周期等功能
1 hooks规则的限制
他们必须在函数组件的最顶层使用,不能放在条件语句中、循环或嵌套函数中,
- 这是因为React依赖于Hooks的顺序调用
- 单链表的每个hook节点没有名字或者key,因为除了它们的顺序,我们无法记录它们的唯一性。
- 因此为了确保某个Hook是它本身,我们不能破坏这个链表的稳定性。
二、React18 那些新特性
1. concurrent 新特性实现的基础
1.1 是什么
Concurrent React(并发模式的React) 是React框架的一项功能,旨在提高应用程序的性能和用户体验。它是自React 16开始引入的一组特性,通过使用协调器(coordinator)和调度器(scheduler)来实现
1.2 为什么要用concurrent
- 传统的React渲染是基于递归的,意味着在处理组件更新时,React会一直执行下去,直到完成整个组件树的渲染。这种方式在大型组件树或复杂的更新情况下可能会导致阻塞主线程,影响应用程序的响应性和性能。ReactDom.render('root' as element)
- Concurrent React引入了一种新的渲染模式,即可中断的渲染。它允许React在渲染过程中执行中断和恢复操作,使得浏览器能够在渲染期间执行其他高优先级的任务,例如用户交互或动画。 createRoot('root' as element)
1.3. concurrent 的特点
1. 异步渲染: Concurrent React能够将渲染工作分解为多个优先级较低的任务,并根据任务的优先级以适当的方式调度它们。这样可以避免长时间的阻塞,提高应用程序的响应性。
2. 优先级调度: Concurrent React引入了任务调度器(scheduler),它根据任务的优先级来决定何时执行任务。通过定义不同任务的优先级,可以确保高优先级任务优先执行,从而更好地响应用户操作。
3. 中断和恢复: 在渲染过程中,Concurrent React允许React在执行任务时中断并恢复。这使得浏览器能够在必要时执行其他任务,提高了应用程序的流畅性和性能。
4. 延迟加载: Concurrent React还提供了延迟加载组件的能力。可以将某些组件标记为“懒加载”,只有在需要时才会开始加载和渲染,从而减少初始加载时间和资源消耗。
2. setState
2.1 改变的地方
在React18以前,可以把setState放在promises、setTimeout或者原生事件中实现同步。
但是以前的React的批量更新是依赖于合成事件的,到了React18之后,state的批量更新不再与合成事件有直接关系,而是自动批量处理。
2.1.1 自动批量处理 Automatic Batching / setState
// 以前: 这里的两次setState并没有批量处理,React会render两次
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);
// React18: 自动批量处理,这里只会render一次
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);
2.1.2 但是如果你有一些其它理由或者需要应急,想要同步setState,这个时候可以使用flushSync
// import { flushSync } from "react-dom";
changeCount = () => {
const { count } = this.state;
flushSync(() => {
this.setState({
count: count + 1,
});
});
console.log("改变count", this.state.count); //sy-log
};
// <button onClick={this.changeCount}>change count 合成事件</button>
3. Suspense
子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 fallback,当所有子组件异步阻塞取消后才会正常渲染。
<Suspense fallback={<Spinner />}>
<Comments />
</ Suspense>
// 子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 fallback,当所有子组件异步阻塞取消后才会正常渲染。
class Suspense extends React.Component {
state = {
promise: null
}
componentDidCatch(e) {
if (e instanceof Promise) {
this.setState({
promise: e
}, () => {
e.then(() => {
this.setState({
promise: null
})
})
})
}
}
render() {
const { fallback, children } = this.props
const { promise } = this.state
return <>
{ promise ? fallback : children }
</>
}
}
4. transition
- Urgent updates 紧急更新,指直接交互,通常指的用户交互。如点击、输入等。这种更新一旦不及时,用户就会觉得哪里不对。
- Transition updates 过渡更新,如UI从一个视图向另一个视图的更新。通常这种更新用户并不着急看到。
1.1 startTransition
startTransition中触发的更新会让更高优先级(如外面的click)的更新先进行
startTransition中的延迟更新,不会触发Suspens组件的fallback,便于用户在更新期间的交互
// 被startTransiton标记后为过渡更新
startTransition(()=> {
// 非紧急更新,会被降低优先级,延迟执行
setQueryValue(inputValue)
})
// 未被标记则马上执行
setInputValue(inputValue)
const [ isPending , startTransition ] = useTransition()
isPending:过度任务状态,true代表过渡中,false过渡结束
startTransition:执行的过渡任务
startTransition(()=>{
console.log(过度任务)
})
例子
export default function App(){
const [ value ,setInputValue ] = React.useState('')
const [ query ,setSearchQuery ] = React.useState('')
const [ isPending , startTransition ] = React.useTransition()
const handleChange = (e) => {
setInputValue(e.target.value)
startTransition(()=>{
setSearchQuery(e.target.value)
})
}
return <div>
{isPending && <span>isTransiton</span>}
<input onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<NewList query={query} />
</div>
}
5. useDeferredValue
本质上和内部实现与 useTransition 一样都是把任务标记成了过渡更新任务。
只是接受的是值
import { useState, useMemo, useDeferredValue } from "react";
const numbers = [...new Array(200000).keys()];
export default function App() {
const [query, setQuery] = useState("");
const handleChange = (e) => {
setQuery(e.target.value);
};
return (
<div>
<input type="number" onChange={handleChange} value={query} />
<List query={query} />
</div>
);
}
function List(props) {
const { query } = props;
const defQuery = useDeferredValue(query);
const list = useMemo(() => (
numbers.map((i, index) => (
defQuery
? i.toString().startsWith(defQuery)
&& <p key={index}>{i}</p>
: <p key={index}>{i}</p>
))
), [defQuery]);
return (
<div>
{list}
</div>
);
}
6. useSyncExternalStore
创建
createStore.js
export default function createStore (reducer) {
let currentState;
let listeners = [];
function getState() {
return currentState;
}
function dispatch(action) {
currentState = reducer(action, currentState);
listeners.forEach(listener => listener())
}
function subscribe(listener) {
listeners.push(listener);
}
return {
getState,
dispatch,
subscribe
}
}
index.js
import createStore from "./createStore";
const store = createStore(countReducer)
export default store;
function countReducer(action, state = 0){
switch (action.type) {
case "ADD":
return state + 1;
case "MINUS":
return state - 1;
default:
return state;
}
}
使用
import store from './index'
// 获取
const state = useSyncExternalStore(store.subScribe,store.getState)
// 修改
store.dispatch(type:'ADD')
7. useEffect
三、state 和 props
state是变量,一般情况下,state改变之后,组件会更新。
props是属性,用于父子通信,且props不可修改。
1、组件不更新的情况
状态改变,组件不会更新的情况:指更新被拦截,比如使用PureComponent组件、Component组件的shouldComponentUpdate、memo、useMemo。具体可参考下面的组件优化小节
四、Redux、MobX、Recoil、解决什么问题
这三者都是状态管理库,当组件内部状态无法满足需求的时候,比如需要实现组件间的状态共享,此时就可以定义一些外部状态,同时还要保证外部状态更新了,组件也随之更新
Redux
基于函数式编程思想实现,集中式管理状态仓库,即一个项目中通常只定义一个store。
MobX
是个响应式状态管理库,实现之初参考了Vue的设计思想。与Redux不同,MobX奉行分散式管理状态,即你可以定义多个store。其主要实现思路是拦截状态值的get与set函数,get时候的把状态值标记可观察变量,set的时候让组件更新。
Recoil
Recoil本身就是React的状态管理库,不再需要与React的绑定库,属于一体机。在Recoil中,状态的定义是渐进式和分布式的。
总结一下,Redux集中管理一个大状态,优点是比较专一,缺点是对于某些场景,比如不需要大量共享状态的时候,就不是特别灵活。而MobX和Recoil是可以分散式管理状态,因此相对Redux来说,状态比较单一来源。Recoil由于又多了一层selector,因此又可以渐进式定义状态。因此,就学习成本来说,一般是这样:Redux<MobX<Recoil。
五、fiber是什么
iber是VDOM的一种表现形式。
- 传统的VDOM中,如React15 VDOM 和 Vue3 VDOM,当父节点有多个子节点的时候,父节点标记子节点的属性children是个数组,在更新VDOM的过程中,我们会按照深度优先遍历的方式,自上而下,自左而右,遍历子节点
- 但是随着React的演进,传统VDOM被淘汰,Fiber取而代之,Fiber与传统VDOM的不同之处主要体现在它的结构上,如child、return、sibling属性的添加。关系如下图所示:
为什么要用fiber
在传统的stack reconciler中,一旦任务开始,就无法停下,不管这个任务有多庞大,而这个时候如果来了更高优先级的任务,那么高优先级的任务无法得到立即处理,从而会出现卡顿现象。
如何解决无法停止
做任务分解、给任务添加优先级,即实现增量渲染,把把渲染任务拆分成块,匀到多帧。这也就意味着一个任务执行完毕后,下个任务可能是它的下一个兄弟节点或者叔叔节点,这个时候Fiber的链表结构就派上用场了。
六、函数组件和类组件的内部状态不同点
类组件
export defalut class SetStatePage extends Component {
changeCount = () => {
const { count } = this.state
flushSync(()=>{
this.setState({
count:count + 1
})
})
console.log("改变count",this.state.count); // sy-log 1
}
}
函数组件
function FCSetStatePage(props) {
const [count, setCount] = useState(0)
const changeCount = () => {
flushSync(()=>{
setCount(count + 1);
});
console.log('count',count); // sy-log 0
}
}
相同点
定义组件状态,并且状态更新,组件也要更新
不同点
存储方式
- 类组件的state存储在类组件实例与fiber上
- 函数组件的state存储在fiber的hook上
更新不同
- setState时候,类组件的新的state与旧的state合并对象
- 而函数组件是新的state覆盖老的state。并且,在useState的setState中,新旧state相同,则函数组件拒绝更新。
取值不同
- 函组件中使用状态,直接使用this.state,它的直接来源是类组件实例。
- 函数组件中使用状态,直接使用useState或者useReducer函数返回值数组的第0个元素,这个值来着fiber上的hook对象。
换句话说,如果想要获取类组件的新的状态值,可以直接访问this.state。而如果想要获取函数组件中的一个新的状态值,必须重新执行useState或者useReducer函数,即必须执行函数组件。
常用组件
Fragment
function ULElment (){
return (<ul>
<LiElement />
</ul>)
}
function LiElement () {
return (<>
<li></li>
<li></li>
</>)
}
或者
function LiElement () {
return (<Fragment key={random}>
<li></li>
<li></li>
</Fragment>)
}
Suspense
请看上面的
Portal
相当于vue3的tTeleport
八、常见组件性能优化
组件复用
协调阶段,组件复用的前提是必须同时满足三个条件:同一层级下、同一类型、同一个key值。所以我们要尽量保证这三者的稳定性。
- 如果不复用会被销毁
减少组件的重新render
组件重新render会导致组件进入协调,协调的核心就是我们常说的vdom diff,所以协调本身就是比较耗时的算法,因此如果能够减少协调,复用旧的fiber节点,那么肯定会加快渲染完成的速度。组件如果没有进入协调阶段,我们称为进入bailout阶段,意思就是这层组件退出更新。
shouldComponentUpdate
Component类组件的一个生命周期,当用户定义的这个函数并且返回false,则进入bailout阶段。
shouldComponentUpdate(nextProps,nextState) {
if (nextProps.m1 === this.props.m1 && nextState.m2 === this.state.m2) {
return false;
} else {
return true;
}
}
PureComponent
更新前会自行浅比较新旧props与state是否改变,如果两者都没变,则进入bailout阶段
class ChildPureComponent extends PureComponent {
render() {
console.log("ChildPureComponent"); //sy-log
return (
<div className="border">
<p>{this.props.item}</p>
</div>
);
}
}
memo
用户可以自定义,如果没有定义,默认使用浅比较,比较组件更新前后的props是否相同,如果相同,则进入bailout阶段。
const ChildMemo = memo(
function Child({item}) {
console.log("ChildMemo"); //sy-log
return <div className="border">{item}</div>;
}
// (prev, next) => {
// return prev.item === next.item;
// }
);
useMemo
可以缓存参数,可以对比useCallback使用,useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
就是吧这些缓存到fiber节点上
function ChildUseMemo({item}) {
console.log("ChildUseMemo"); //sy-log
return useMemo(
() => (
<div className="border">
{item}
<Child2 />
</div>
),
[]
);
}
function Child2() {
console.log("Child2"); //sy-log
return (
<div className="border">
<h1>Child2</h1>
<Child3 />
</div>
);
}
function Child3() {
console.log("Child3"); //sy-log
return (
<div className="border">
<h1>Child3</h1>
</div>
);
}
const [count, setCount] = useState(0)
const expensive = useMemo(()=>{
console.log("compute")\
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sums
},[count])
九、组件通信的几种方式(juejin.cn/post/717504…)
父传子
父组件可以向子组件通过传 props 的方式,向子组件进行通讯
子传父
props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中
兄弟组件通信
找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
跨层级通信
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context
通信再适合不过
发布订阅模式
发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信
全局状态管理工具
借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态
十、useEffect/useLayoutEffect 的用法和区别
useEffect
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后延迟执行。
useLayoutEffect
它会在所有的 DOM 变更之后同步调用 effect
解释同步 延迟
这里所谓的延迟、同步,指的是React任务调度中的任务调度,所谓延迟就是useEffect的effect不与组件渲染使用同一个任务调度函数,而是再单独调用一次任务调度函数
十一、useState和useReducer 的原理
都是用于定义函数组件内部状态、状态更新、组件更新
状态值存储在函数组件的fiber的hook.memoizedState上
组件初次渲染
- 把state初始值存储hook.memoizedState
- 初始化更新队列,存储到hook.queue上
- 义dispatch事件,并存储到hook.queue上。
组件更新阶段(批量更新)
- 检查是否有上次未处理的更新,如果有,则添加到更新队列上(更新队列是个环形链表)
- 循环遍历更新队列,得到newState
- 把最终得到的newState赋值到hook.memoizedState上