React主要版本的重大API更新
react/CHANGELOG.md at main · facebook/react
React 版本 | 发布日期 | 重要API更新 |
---|---|---|
16.0 | 2017年9月 | - Fiber架构引入:提升性能,支持异步渲染 - Error Boundaries:新增 componentDidCatch ,用于捕获子组件树的运行时错误 |
16.3 | 2018年3月 | - 生命周期方法更新:新增getDerivedStateFromProps 和getSnapshotBeforeUpdate ,对老方法逐步废弃- 现代化的Context API |
16.6 | 2018年10月 | - React.memo:用于高效的React函数组件性能优化 - React.lazy:实现组件的动态懒加载和代码分割 Suspense |
16.8 | 2019年2月 | - React Hooks 引入:新增useState , useEffect , useContext , useReducer , useRef , useCallback , useMemo 等Hooks API |
17.0 | 2020年10月 | - 改善事件委托机制 - 新增的事件处理改进,支持 onFocusCapture 和onBlurCapture 等事件捕获 |
18.0 | 2022年3月 | - 引入 Concurrent Rendering:通过createRoot 取代render ,利用并发模式提高 UI 响应性- 新增 useTransition 和useDeferredValue |
18.2 | 2022年6月 | - 自动批处理更新(Automatic Batching):在多个状态更新时默认启用批量更新机制 |
React 16.3:生命周期和 Context API
- 新增生命周期方法:
getDerivedStateFromProps(props, state)
:代替componentWillReceiveProps
,从props中衍生并更新state。getSnapshotBeforeUpdate(prevProps, prevState)
:用于在DOM更新前获取快照(如滚动位置)。
- 新的Context API:
- 提供更加简洁的跨组件传递数据方法,核心API如:
React.createContext
,<Provider>
,<Consumer>
。
- 提供更加简洁的跨组件传递数据方法,核心API如:
React 16.6:优化工具
React.memo
:高阶组件,用于优化函数式组件的性能,相当于PureComponent
的函数版。React.lazy
和Suspense
:实现动态加载组件配合代码分割,简化Bundle文件的体积。
React 16.8:Hooks的诞生
- React Hooks:
- 打破了类组件的限制,Hooks能在函数组件中使用状态和生命周期逻辑:
useState
:管理组件状态值。useEffect
:处理副作用,如事件监听、异步调用等。useContext
:直接访问Context上下文。useReducer
:实现状态复杂时的Redux风格处理方式。useRef
:用于访问/存储DOM元素或变量。useCallback
和useMemo
:优化性能,避免函数或值的重新创建。
- 打破了类组件的限制,Hooks能在函数组件中使用状态和生命周期逻辑:
React 18.0:并发模式和性能提升
- Major API Changes:
createRoot(container)
:为新的并发渲染模式提供支持,替代旧的ReactDOM.render
。- 并发功能:
useTransition
:管理用户界面中优先级较低的更新(如切换页面加载状态)。useDeferredValue
:降低输入更新的优先级,用于大量数据渲染的优化。
- 自动批处理更新:以往手动调用
ReactDOM.unstable_batchedUpdates
的逻辑,现在在React的所有事件环境中都会自动启用批量更新。
React 18.2:自动批处理更新扩展
- React事件之外也支持自动批处理更新,包含原生事件和异步任务,减少了非必要的重新渲染情况。
javascript
umi //企业级react应用框架,类似Next.js
//dva作为umi插件, 底层引入了redux-sagas做异步流程控制,内置了 react-router('dva/router')
启动过程
umi dev -> bin/umi.js -> lib/cli -> forkedDev.ts -> Service继承自CoreService,是对CoreService二次封装。它的核心代码在ServiceWithBuiltIn.ts文件中
-> 进程启动需要两步:1、实例化Service,2、调用Service的run方法 -> runCommand -> api.registerCommand({ name: 'dev'...}) ->
// 调用实例化后的 bundler 的 setupDevServerOpts 方法,这个方法做了如下几件事:
// 1. 调用webpack方法,获取webpack的编译器实例 compiler
// 2. 编译器实例 compiler 通过 webpack-dev-middleware 封装器,将webpack处理过的文件封装成 server 能接收的格式
// 3. 通过调用 sockjs 的 sockWrite 方法,实现热更新
// 4. 处理服务类 Server 实例化时需要的 onListening 和 onConnection 函数
//umi内置的核心插件都通过插件集@umijs/preset-built-in注入
模板html//\@umijs\core\lib\Html\Html.js
//\@umijs\server\lib\Server\Server.js -> proxyMiddleware
///@umijs/preset-built-in/lib/plugins/commands/dev/createRouteMiddleware.js-> sendHtml
//getGetContent-> let html = render(tpl, context, { ...filename: 'document.ejs' });
enquire.js //是一个轻量级的纯JavaScript库,用于响应CSS媒体查询。
SSR获取window: if (typeof window !== 'undefined') {window}
SLOT插槽:
this.props.children//默认slot是组件包含的内容
合成事件//统一委托到根元素
onClick={ (e) => this.deleteRow(id, e) }
受不受react控制
/受控组件/, // state是组件的唯一数据源, 即表单值是由 React 组件监听onChange来存到state的
非受控组件 //用ref控制,可以减少代码量时, 直接ref.current.value获取表单值
Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。
Fragments 组件:用来包裹多个组件,封装成一个,方便引用
组件 hook
js
组件
类组件
函数组件//颗粒度更小,逻辑复用
useEffect(() => {
//相当于componentDidMount、componentDidUpdate
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(timer);//相当于componentWillUnmount
}, []/*依赖项们, 空的话useEffect只执行一次*/);
//每个async函数都会默认返回一个隐式的promise。但是,useEffect不应该返回任何内容,所以useEffect(async () => …) 是不允许的
setState({k:v}) ,/在 setTimeout 和 native事件 中使用是同步的, 其他地方使用如合成事件中 是异步的,更新会合并/
setState(preState=>{return obj},回调) //非合并非批量更新
纯组件//无shouldComponentUpdate,内部自动shouldComponentUpdate, "浅比较"只比较了对象第一层
/性能优化/
hook //Hook 不能在 class 组件中使用
useState
//定义一个叫count的state变量,初始化为0
const [count, setCount] = useState(0);
useEffect//相当于componentDidMount、componentDidUpdate
/在组件渲染到屏幕后延迟执行,用来完成副作用操作/
//渲染时的数据获取、订阅或者手动修改过 DOM。这些操作称为“副作用”/“作用”
useLayoutEffect,/跟useEffect的区别是它不延迟执行,在DOM变更后同步调用/
缓存参数:value = useMemo(回调,[依赖项])//只有依赖项改变时才执行,防止其他无关变量变动时, 它也跟着傻傻重算
React.memo(组件,可选比较函数)//跟useMemo一样可以用来对函数式组件进行性能优化, 即使没有比较参数那也是"浅比较"
缓存函数:func = useCallback(比如包一个onChange监听器函数,[依赖项])
//配合纯组件(例如shouldComponentUpdate使用引用相等性去避免非必要渲染)
//隔壁组件或组件内部的某个属性改变造成重新渲染时,函数也会重新初始化,那函数引用就变了,又造成重新渲染,如此形成死循环, 如果把方法包起来作为prop再传给子组件, 就能避免子组件动不动就重新渲染
use自定义//只能在最外层使用,只有函数式组件可用? 对比普通函数可以引起重新渲染
useContext
声明
const ThemeContext = React.createContext({ foreground: "#000000", background: "#eeeeee" })
用法1
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
用法2
const theme = useContext(ThemeContext);
跨组件通信
js
高阶组件HOC//传入组件, 返回组件的函数
装饰器写法
/不要在render里使用HOC,性能差/
- React Context 更适合于管理简单的数据,例如应用程序主题、用户认证信息等。expand_more
- Redux 更适合于管理复杂的数据,例如购物车中的商品、表单数据等。
跨组件通信传值context//穿透进去
父创建:
//用const context = React.createContext();
//用<context.Provider>包起来
子获取值的方式:
1.Class.contextType 会被重新赋值为由React.createContext()创建的对象去赋值给this.context
2.Context.Consumer包裹
<UserConsumer>
{userContext => <Child {...userContext} />}
</UserConsumer>
3.const store = useContext(context)
ref
// formRef = React.createRef();
// ref={this.formRef}
const formRef = useRef(null);
React.clone(老元素,新属性对象,新儿子)
React.useImperativeHandle(ref, () => formInstance);//在使用 ref 时自定义暴露给父组件的实例值
const Form = React.forwardRef(_Form);//转发ref到函数组件(本来不支持的),而非实例值,实例值要用useImperativeHandle
createPortal(jsx,要附加的节点)//插入dom到指定节点下
性能
js
fiber//小任务们
大组件树解析阻塞->拆分任务->requestIdleCallback里根据优先级执行->更流畅
fiber
child 第一个子fiber
sibling 第一个子fiber的其他兄弟
return 父fiber
性能 //减少计算,减少渲染
shouldComponentUpdate
PureComponent//对 props 和 state 进行浅比较
useMemo
不要使用内联属性/函数
无状态组件
export default (props)=><div>{props.name}</div>
样板
javascript
import * as React from 'react';
import classNames from 'classnames';
className=classNames(
{
class2: false,
class1: true,
},
)
function 组件({children}) {}
生命周期(重点)
生命周期函数的最佳实践
- 尽量避免在 Render Phase 生命周期函数中进行副作用操作:
- 不要在
render
,constructor
,getDerivedStateFromProps
,shouldComponentUpdate
中执行副作用。这些函数可能会在调和过程(包括调和的多次尝试)中被多次调用。
- 不要在
- 把副作用操作放到 Commit Phase 生命周期函数中:
- 使用
componentDidMount
和componentDidUpdate
来执行副作用,例如数据获取、DOM 操作等。这样可以确保这些副作用只在 DOM 确定更新后运行。
- 使用
- 特别注意
shouldComponentUpdate
:shouldComponentUpdate
用于性能优化,返回false
可以阻止不必要的渲染。它不会阻止子组件的生命周期调用,所以要谨慎使用。
- 使用
getSnapshotBeforeUpdate
处理 DOM 读取(仅用于类组件):- 如果需要在 DOM 更新前进行读取操作,可以使用
getSnapshotBeforeUpdate
,它是在 DOM 更新前的最后一次机会进行读取操作。
- 如果需要在 DOM 更新前进行读取操作,可以使用
javascript
UNSAFE_开头表示v17可能会废弃它,以为fiber可以中断,造成willXXX可能被执行多次
挂载 //当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
static defaultProps={msg:'1'};
//这一般用于 props 未赋值,但又不能为 null的情况
static contextTypes = ThemeContext;//注入this.context
//声明有const ThemeContext = React.createContext({ foreground: "#000000", background: "#eeeeee" })
//之后即可通过 const {foreground.background} = this.context;获取
static propTypes={msg:PropTypes.string /*或PropTypes.string.isRequired*/};
//先import PropTypes from 'prop-types';
constructor()//初始化state | 方法绑定
/因为Reconciliation(diff)阶段是可以被打断的,所以Reconciliation(diff)阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起Bug。/
/所以对于Reconciliation(diff)阶段调用的几个函数,除了shouldComponentUpdate以外,其他都应该避免去使用/
(V17)static getDerivedStateFromProps(props,state) 在新版本用来替代UNSAFE_componentWillReceiveProps,让组件在 props 变化时更新 state
UNSAFE_componentWillMount() //UNSAFE_开头表示v17可能会废弃它,可以用命令自动加
render() ,/唯一必须实现!!/
componentDidMount() //在这使用setState //所以在最好在这Ajax,因为获得数据后可以setState
//错误处理
//static getDerivedStateFromError()
//componentDidCatch()
更新 //当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
UNSAFE_componentWillReceiveProps(nextProps)//在这使用setState //UNSAFE_开头表示v17可能会废弃它
(V17)static getDerivedStateFromProps(props, state) 在新版本用来替代UNSAFE_componentWillReceiveProps,让组件在 props 变化时更新 state
return stateObj || null//它应返回一个对象来更新 state,如果返回 null 则不更新任何内容
shouldComponentUpdate(nextProps,nextState) return bool
/用来性能优化/
UNSAFE_componentWillUpdate() //UNSAFE_开头表示v17可能会废弃它
render() ,/唯一必须实现!!/
(V17)getSnapshotBeforeUpdate(preProps,preState) 在新版本用来替代UNSAFE_componentWillUpdate
return stateObj || null //返回作为参数给componentDidUpdate
componentDidUpdate(preProps,preState,/snapshot/)
卸载 //当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount()
在 React v16 之后,props 改变后会触发以下两个生命周期:
- getDerivedStateFromProps
- shouldComponentUpdate 一般来说,可以在 getDerivedStateFromProps 中根据新的 props 更新 state,并在 shouldComponentUpdate 中根据 state 的变化决定是否要更新组件。 以下是一个示例:
js
class MyComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 根据新的 props 更新 state
if (nextProps.count !== prevState.count) {
return {
count: nextProps.count,
};
}
return null;
}
shouldComponentUpdate(nextProps, nextState) {
// 根据 state 的变化决定是否要更新组件
return nextState.count !== this.state.count;
}
render() {
// 渲染组件
return (
<div>
<h1>{this.state.count}</h1>
</div>
);
}
}
原生redux
javascript
原生redux://Redux是JavaScript应用程序的可预测状态容器
import { createStore } from "redux";
//Reducer: 定义state初始化和修改规则,reducer是一个纯函数
function counterReducer(state = 0, action) {//dispatch(action) 跳到这里来
const { type, payload } = action;
2.switch (action.type) {
case "ADD": return state + 1;
case "MINUS": return state - 1;
default: return 3.state;
}
//不可变的set方法仅设置立即属性,即对象的直接子代。 setIn让您设置数据中任何深度节点的值。
//set仅采用属性名称。 setIn使用键/索引数组来深入嵌套的元素
}
const store = createStore(counterReducer,可选applyMiddleware(中间件thunk,中间件logger));
export default store;
//类的使用:
import store from "../store/";
export default class ReduxPage extends Component {
componentDidMount() {
4.store.subscribe(() => this.forceUpdate() });
}
render() {
return (
<div>
<p>{store.getState()}</p>
<button onClick={() => 1.store.dispatch({ type: "ADD",payload:'内容' })}>add</button>
</div>
);
}
}
//函数式组件的使用:
const [state, dispatch] = useReducer(counterReducer, "0", 这里可以处理一下初始值"0");
react-redux
js
react-redux: //使用 React Redux,你的组件永远不会直接访问store
//把Provider放在根组件外层,使子组件能获得store
import { Provider } from "react-redux";
<Provider store={store}>
<App />
</Provider>
//用法1:高阶组件
import { connect } from "react-redux";
class ReactReduxPage extends Component { ////还有写法2, 装饰器写法@connect
render() {
const { num, dispatch, add } = this.props;
console.log("props", this.props);
return (
<p>{num}</p>
<button onClick={add}>add</button>
);
}
}
export default connect( //connect返回一个高阶组件函数, 加强了组件
state => ({ ...state }), //mapStateToProps 把state映射到props
(dispatch) => { return { increment: () => dispatch({ type: 'INCREMENT' }), dispatch }} //mapDispatchToProps
//mergeProps?: (stateProps, dispatchProps, ownProps) => Object //return value as this.props
)(ReactReduxPage)
//用法2: hook
const count = useSelector(({count}) => count);
const dispatch = useDispatch();
中间件saga //中间件thunk更容易形成嵌套地狱
管理副作用//让有顺序要求的异步操作按顺序执行
call和fork
// call 是阻塞型调用 generator在调用结束之前不执行其他事情
// fork是非阻塞型调用 任务在后台启动 调用者可以继续自己的流程,不用等待fork任务结束
put/*dispatch派发*/
take和takeEvery一次和持续监听
路由守卫
中间件redux-observable//类似saga,但它是链式操作
react-router
javascript
import { BrowserRouter as Router, HashRouter , Link, NavLink ,MemoryRouter, Prompt, Redirect, Route, Router, StaticRouter, Switch, generatePath, matchPath,
useHistory, useLocation, useParams, useRouteMatch, withRouter } from 'react-router'; //v5
withRouter(组件)// withRouter注入 history, location, match等到props
提供了两种不同的路由:
BrowserRouter
HashRouter
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户中心</Link>
<Switch>//独占路由,匹配到了后面就都不管了
<Route path="/a/:b?"/> //可匹配 /a /a/xxx
<Route path="/"
// component={HomePage} //会注入 history, location, match到组件的props
// children={() => <div>children</div>}
render={() => <div>render</div>}
/>
<Route exact path="/user" component={UserPage} />//exact精确匹配它一个, 不加的话, /user/a也会匹配到它
<Route path="/product/:id" render={() => <Product />} />//动态路由,match.params.id
<Route path="/(about|who)/" component={Dashboard} />//匹配多条路径
<Route component={EmptyPage} /> //无论怎么样都会匹配
</Switch>
</Router>
Route渲染优先级: /children > component > render/
//children,是path匹不匹配都会渲染的
//内联函数不要用component, component会调用React.createElement,如果用匿名函数的话每次生成的组件type不一样,会重复卸载挂载,性能不好
Next
javascript
Next.js:
执行 next 时,读取next.config.js的堆栈:
'<root>\\next.config.js',
'<root>\\node_modules\\next\\dist\\next-server\\server\\config.js',
'<root>\\node_modules\\next\\dist\\next-server\\server\\next-server.js',
'<root>\\node_modules\\next\\dist\\server\\next.js',
'<root>\\node_modules\\next\\dist\\server\\lib\\start-server.js',
'<root>\\node_modules\\next\\dist\\cli\\next-dev.js',
'<root>\\node_modules\\next\\dist\\bin\\next'
服务端堆栈:
Home//被在打包在0.js 里 //\.next/server/0.js 该文件包含了Nav0.jsx,Content3.jsx ,antMotionStyle.less,utils.js等等bundle
processChild
resolve
render
read
renderToString //node_modules/react-dom/cjs/react-dom-server\.node\.development.js
render
renderPage //node_modules/next/dist/next-server/server/render.js
getInitialProps //\.next/server/static/development/pages/_document.js
loadGetInitialProps //node_modules/next/dist/next-server/lib/utils.js
renderToHTML //node_modules/next/dist/next-server/server/render.js
前端堆栈:
performSyncWorkOnRoot(循环)
//第1次是进construct, 过程:
//workLoopSync->performUnitOfWork->beginWork$1->updateClassComponent->constructClassInstance->Home.jsx的constructor
//第2次是进:render, 反过程:
//render->finishClassComponent->updateClassComponent->beginWork->beginWork$1->performUnitOfWork->workLoopSync
//第3次是进componentDidMount, 反过程:
//componentDidMount->commitLifeCycles->commitLayoutEffects->callCallback->invokeGuardedCallbackDev
//->invokeGuardedCallback->commitRootImpl->unstable_runWithPriority->runWithPriority$1->commitRoot->finishSyncRender
scheduleUpdateOnFiber
updateContainer
unbatchedUpdates
legacyRenderSubtreeIntoContainer
hydrate //react-dom.development.js:24823
renderReactElement
doRender
render //webpack:///./node_modules/next/dist/client/index.js
requestAnimationFrame (async)
displayContent //webpack:///./node_modules/next/dist/client/dev/fouc.js
Promise.then (async) //webpack:///./node_modules/next/dist/client/next-dev.js
__webpack_require__
checkDeferredModules
webpackJsonpCallback //webpack:///webpack/bootstrap:32
http://localhost:3000/_next/static/runtime/main.js?ts=1585882815943
'pages/XXX.js'下的3个获取数据的方法的区别:
服务端 客户端 执行时间
1.getInitialProps true true 在渲染页面之前就会运行(服务器端)执行, 而当使用Next/Link或Next/Router切换页面时,在(客户端)执行
/9.3版本后被以下2个替代了且只能选1个来用!!/
1.1getStaticProps true false 在build时(客户端一请求,服务端就build)就搞数据来渲染页面
1.2getServerSideProps true false 在每次请求时,都用getServerSideProps返回的数据来渲染页面
2.getStaticPaths true false 仅在建造时(fallback = true)//用于在使用动态路由时生成静态文件
React源码
- https://p1.music.126.net/VU37zHp-6hAUfNaZbu3HRw==/109951165071751567.jpg类图
- https://juejin.cn/post/7202085514400038969#heading-23【动图+大白话🍓解析React源码】Render阶段中Fiber树的初始化与对比更新~
javascript
jsx → React.createElement() → fiber → DOM
Fiber//即虚拟dom, 用于描述ReactElement对象在内存中的状态
fiber1是当前的旧的
fiber2是正在构造的新的
wip //work in progress fiber
nextUnitOfWork //将要更新的下一个fiber
reconciliation协调(也就是diff)
//算法复杂度O(n) //每个节点都只走一遍
render 阶段:这个阶段是可中断的,会找出所有节点的变更, 调用组件的 render 方法,`生成新的虚拟 DOM`
中断: 在 React 18 中引入的并发模式下,React 可能会暂停和恢复这段工作
- `constructor`
- `static getDerivedStateFromProps`
- `render`
- `shouldComponentUpdate`
commit 阶段:这个阶段是不可中断的,会执行所有的变更, `更新真实DOM `
- `componentDidMount`
- `componentDidUpdate`
- `componentWillUnmount`
render时
createRootFiber
scheduleUpdateOnFiber(rootFilber)
requestIdleCallback(workLoop)//React自己实现了替代requestIdleCallback的scheduler
//workLoop 一直取performUnitOfWork 返回的 next.sibling 或 next.return.sibling, 直到拿不到就跳出循环
performUnitOfWork(render阶段) || commitRoot(commit阶段);
updateHostComponent || updateFunctionComponent
reconcileChildren //diff child
React 的并发模式
React 的 Concurrent Mode 是 React 在 16 版本之后引入的一项重要特性,旨在提升大型应用的性能和用户体验。Concurrent Mode 并不是一个单独的 API,而是一套新的渲染机制,使 React 能够更智能地调度和管理渲染任务,从而在用户交互时保持界面的响应性。以下将详细分析其实现原理和核心概念。 在传统的 React 渲染机制中,一旦渲染开始,它会持续执行直到所有组件更新完毕。在这种模式下,如果一个组件树很大,渲染时间可能会很长,导致浏览器在此期间无法响应用户输入。
1. 时间切片 (Time Slicing)
时间切片是 Concurrent Mode 的一个关键技术,它通过将渲染任务分解为多个小的时间片段,使得 React 可以在浏览器空闲时间内进行渲染,从而避免阻塞用户交互。 实现机制:
- requestIdleCallback: React 早期尝试使用
requestIdleCallback
来实现时间切片,但它的缺点是调用频率不可控,且在某些高优先级任务上不适用。 - Scheduler: React 引入了一个独立的调度器 (
scheduler
),它基于requestAnimationFrame
和MessageChannel
进行任务调度。这使得 React 能够更精确地控制渲染任务的切片,从而在高优先级任务插入时(如用户输入),能够及时打断并处理。 时间切片允许 React 在每个时间片结束时检查是否有更高优先级的任务(例如用户点击或键盘输入)。如果有,React 可以中断当前的渲染任务,并优先处理用户交互。这使得大型应用在渲染时保持流畅和高响应性。
2. 优先级调度 (Priority Scheduling)
优先级分类:
- Immediate: 例如用户输入或点击事件,需要立即响应的任务。
- User-blocking: 影响用户操作的任务,比如动画或界面变化,这些任务应尽快完成。
- Normal: 常规的渲染任务,这类任务可以被打断。
- Low: 低优先级的后台任务,例如预加载数据或非关键组件的渲染。
- Idle: 非关键任务,可以等到浏览器空闲时再执行。
3. 可中断更新 (Interruptible Updates)
工作原理:
- 当 React 开始一个渲染任务时,它会在每个时间片结束时检查是否有更高优先级的任务需要处理。
- 如果有高优先级任务(如用户点击按钮),React 会立即中断当前的渲染并切换到高优先级任务。当高优先级任务完成后,React 可以恢复之前的渲染任务。
- React 的 Fiber 架构使得这种中断和恢复机制变得可能。Fiber 可以理解为一种可中断的虚拟 DOM,它允许 React 记录和恢复渲染任务的状态,从而使渲染过程具备流动性。