前言
一、React18有哪些更新?
-
setState自动批处理
在react17中,只有react事件会进行批处理,原生js事件、promise,setTimeout、setInterval不会
react18,将所有事件都进行批处理,即多次setState会被合并为1次执行,提高了性能,在数据层,将多个状态更新合并成一次处理(在视图层,将多次渲染合并成一次渲染) -
引入了新的root API,支持new concurrent renderer(并发模式的渲染)
//React 17 import React from "react" import ReactDOM from "react-dom" import App from "./App" const root = document.getElementById("root") ReactDOM.render(<App/>,root) // 卸载组件 ReactDOM.unmountComponentAtNode(root) // React 18 import React from "react" import ReactDOM from "react-dom/client" import App from "./App" const root = document.getElementById("root") ReactDOM.createRoot(root).render(<App/>) // 卸载组件 root.unmount()
-
去掉了对IE浏览器的支持,react18引入的新特性全部基于现代浏览器,如需支持需要退回到react17版本
-
flushSync
批量更新是一个破坏性的更新,如果想退出批量更新,可以使用flushSyncimport React,{useState} from "react" import {flushSync} from "react-dom" const App=()=>{ const [count,setCount]=useState(0) const [count2,setCount2]=useState(0) return ( <div className="App"> <button onClick=(()=>{ // 第一次更新 flushSync(()=>{ setCount(count=>count+1) }) // 第二次更新 flushSync(()=>{ setCount2(count2=>count2+1) }) })>点击</button> <span>count:{count}</span> <span>count2:{count2}</span> </div> ) } export default App
-
react组件返回值更新
- 在react17中,返回空组件只能返回null,显式返回undefined会报错
- 在react18中,支持null和undefined返回
-
strict mode更新
当你使用严格模式时,React会对每个组件返回两次渲染,以便你观察一些意想不到的结果,在react17中去掉了一次渲染的控制台日志,以便让日志容易阅读。react18取消了这个限制,第二次渲染会以浅灰色出现在控制台日志 -
支持useId
在服务器和客户端生成相同的唯一一个id,避免hydrating的不兼容 -
useSyncExternalStore
用于解决外部数据撕裂问题 -
useInsertionEffect
这个hooks只建议在css in js库中使用,这个hooks执行时机在DOM生成之后,useLayoutEffect执行之前,它的工作原理大致与useLayoutEffect相同,此时无法访问DOM节点的引用,一般用于提前注入脚本 -
Concurrent Mode
并发模式不是一个功能,而是一个底层设计。它可以帮助应用保持响应,根据用户的设备性能和网速进行调整,它通过渲染可中断来修复阻塞渲染机制。在concurrent模式中,React可以同时更新多个状态
区别就是使同步不可中断更新变成了异步可中断更新
useDeferredValue和startTransition用来标记一次非紧急更新
二、React的设计思想
- 组件化
每个组件都符合开放-封闭原则,封闭是针对渲染工作流来说的,指的是组件内部的状态都由自身维护,只处理内部的渲染逻辑。开放是针对组件通信来说的,指的是不同组件可以通过props(单项数据流)进行数据交互
- 数据驱动视图
UI=f(data)
通过上面这个公式得出,如果要渲染界面,不应该直接操作DOM,而是通过修改数据(state或prop),数据驱动视图更新
- 虚拟DOM
由浏览器的渲染流水线可知,DOM操作是一个昂贵的操作,很耗性能,因此产生了虚拟DOM。虚拟DOM是对真实DOM的映射,React通过新旧虚拟DOM对比,得到需要更新的部分,实现数据的增量更新
三、JSX是什么,它和JS有什么区别
JSX是react的语法糖,它允许在html中写JS,它不能被浏览器直接识别,需要通过webpack、babel之类的编译工具转换为JS执行
JSX与JS的区别:
- JS可以被打包工具直接编译,不需要额外转换,jsx需要通过babel编译,它是React.createElement的语法糖,使用jsx等价于React.createElement
- jsx是js的语法扩展,允许在html中写JS;JS是原生写法,需要通过script标签引入
为什么在文件中没有使用react,也要在文件顶部import React from “react”
只要使用了jsx,就需要引用react,因为jsx本质就是React.createElement
为什么React自定义组件首字母要大写
jsx通过babel转义时,调用了React.createElement函数,它接收三个参数,分别是type元素类型,props元素属性,children子元素。
如下图所示,从jsx到真实DOM需要经历jsx->虚拟DOM->真实DOM。如果组件首字母为小写,它会被当成字符串进行传递,在创建虚拟DOM的时候,就会把它当成一个html标签,而html没有app这个标签,就会报错。
组件首字母为大写,它会当成一个变量进行传递,React知道它是个自定义组件就不会报错了
<app>lyllovelemon</app>
// 转义后
React.createElement("app",null,"lyllovelemon")
<App>lyllovelemon</App>
// 转义后
React.createElement(App,null,lyllovelemon)
React组件为什么不能返回多个元素
这个问题也可以理解为React组件为什么只能有一个根元素,原因:
-
React组件最后会编译为render函数,函数的返回值只能是1个,如果不用单独的根节点包裹,就会并列返回多个值,这在js中是不允许的
class App extends React.Component{ render(){ return( <div> <h1 className="title">lyllovelemon</h1> <span>内容</span> </div> ) } //编译后 class App extends React.Component{ render(){ return React.createElement('div',null,[ React.createElement('h1',{className:'title'},'lyllovelemon'), React.createElement('span'),null,'内容' ]) } }
-
react的虚拟DOM是一个树状结构,树的根节点只能是1个,如果有多个根节点,无法确认是在哪棵树上进行更新
vue的根节点为什么只有一个也是同样的原因
React组件怎样可以返回多个组件使用HOC(高阶函数)
使用React.Fragment,可以让你将元素列表加到一个分组中,而且不会创建额外的节点(类似vue的template)renderList(){ this.state.list.map((item,key)=>{ return (<React.Fragment> <tr key={item.id}> <td>{item.name}</td> <td>{item.age}</td> <td>{item.address}</td> </tr> </React.Fragment>) }) }
-
使用数组返回
renderList(){ this.state.list.map((item,key)=>{ return [ <tr key={item.id}> <td>{item.name}</td> <td>{item.age}</td> <td>{item.address}</td> </tr> ] }) }
React中元素和组件的区别
react组件有类组件、函数组件
react元素是通过jsx创建的
const element = <div className="element">我是元素</div>
四、简述React的生命周期
生命周期指的是组件实例从创建到销毁的流程,函数组件没有生命周期,只有类组件才有,因为只有class组件会创建组件实例
组件的生命周期可以分为挂载、更新、卸载阶段
-
挂载
constructor 可以进行state和props的初始化
static getDerivedStateFromProps
render
componentDidMount 第一次渲染后调用,可以访问DOM,进行异步请求和定时器、消息订阅
-
更新
当组件的props或state变化会触发更新
static getDerivedStateFromProps
shouldComponentUpdate 返回一个布尔值,默认返回true,可以通过这个生命周期钩子进行性能优化,确认不需要更新组件时调用
render
getSnapShotBeforeUpdate
componentDidUpdate 在组件完成更新后调用
-
卸载
componentWillUnmount 组件从DOM中被移除的时候调用
-
错误捕获
static getDerivedStateFromError 在errorBoundary中使用
componentDidCatch
render是class组件中唯一必须实现的方法
五、React事件机制
什么是合成事件
React基于浏览器的事件机制实现了一套自身的事件机制,它符合W3C规范,包括事件触发、事件冒泡、事件捕获、事件合成和事件派发等
React事件的设计动机(作用):
-
在底层磨平不同浏览器的差异,React实现了统一的事件机制,我们不再需要处理浏览器事件机制方面的兼容问题,在上层面向开发者暴露稳定、统一的、与原生事件相同的事件接口
-
React把握了事件机制的主动权,实现了对所有事件的中心化管控
-
React引入事件池避免垃圾回收,在事件池中获取或释放事件对象,避免频繁的创建和销毁
React事件机制和原生DOM事件流有什么区别
虽然合成事件不是原生DOM事件,但它包含了原生DOM事件的引用,可以通过e.nativeEvent访问
DOM事件流是怎么工作的,一个页面往往会绑定多个事件,页面接收事件的顺序叫事件流
W3C标准事件的传播过程:
- 事件捕获
- 处于目标
- 事件冒泡
常用的事件处理性能优化手段:事件委托
把多个子元素同一类型的监听函数合并到父元素上,通过一个函数监听的行为叫事件委托
我们写的React事件是绑定在DOM上吗,如果不是绑定在哪里
React16的事件绑定在document上, React17以后事件绑定在container上,ReactDOM.render(app,container)
React事件机制总结如下:
事件绑定 事件触发
-
React所有的事件绑定在container上(react17以后),而不是绑定在DOM元素上(作用:减少内存开销,所有的事件处理都在container上,其他节点没有绑定事件)
-
React自身实现了一套冒泡机制,不能通过return false阻止冒泡
-
React通过SytheticEvent实现了事件合成
React实现事件绑定的过程建立合成事件与原生事件的对应关系
registrationNameModule, 它建立了React事件到plugin的映射,它包含React支持的所有事件的类型,用于判断一个组件的prop是否是事件类型 ```typescript { onBlur:SimpleEventPlugin, onClick:SimpleEventPlugin, onClickCapture:SimpleEventPlugin, onChange:ChangeEventPlugin, onChangeCapture:ChangeEventPlugin, onMouseEnter:EnterLeaveEventPlugin, onMouseLeave:EnterLeaveEventPlugin, ... } ``` **registrationNameDependencies**, 这个对象记录了React事件到原生事件的映射 ```typescript { onBlur: ['blur'], onClick: ['click'], onClickCapture: ['click'], onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'], onMouseEnter: ['mouseout', 'mouseover'], onMouseLeave: ['mouseout', 'mouseover'], } ```
plugins对象, 记录了所有注册的插件列表
plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...]
为什么针对同一个事件,即使可能存在多次回调,document(container)也只需要注册一次监听
因为React注册到document(container)上的并不是一个某个DOM节点具体的回调逻辑,而是一个统一的事件分发函数dispatchEvent - > 事件委托思想
dispatchEvent是怎么实现事件分发的
事件触发的本质是对dispatchEvent函数的调用
React事件处理为什么要手动绑定this
react组件会被编译为React.createElement,在createElement中,它的this丢失了,并不是由组件实例调用的,因此需要手动绑定this
为什么不能通过return false阻止事件的默认行为
因为React基于浏览器的事件机制实现了一套自己的事件机制,和原生DOM事件不同,它采用了事件委托的思想,通过dispatch统一分发事件处理函数
React怎么阻止事件冒泡
- 阻止合成事件的冒泡用e.stopPropagation()
- 阻止合成事件和最外层document事件冒泡,使用e.nativeEvent.stopImmediatePropogation()
- 阻止合成事件和除了最外层document事件冒泡,通过判断e.target避免
document.body.addEventListener('click',e=>{
if(e.target && e.target.matches('div.stop')){
return
}
this.setState({active:false})
})
HOC和hooks的区别
useEffect和useLayoutEffect区别
React性能优化手段
- shouldComponentUpdate
- memo
- getDerviedStateFromProps
- 使用Fragment
- v-for使用正确的key
- 拆分尽可能小的可复用组件,ErrorBoundary
- 使用React.lazy和React.Suspense延迟加载不需要立马使用的组件
六、常用组件
错误边界
React部分组件的错误不应该导致整个应用崩溃,为了解决这个问题,React16引入了错误边界
使用方法:
React组件在内部定义了getDerivedStateFromError或者componentDidCatch,它就是一个错误边界。getDerviedStateFromError和componentDidCatch的区别是前者展示降级UI,后者记录具体的错误信息,它只能用于class组件
import React from "react"
class ErrorBoundary extends React.Component{
constructor(props){
super(props)
this.state={
hasError:false
}
}
staic getDerivedStateFromError(){
return { hasError:true}
}
componentDidCatch(err,info){
console.error(err,info)
}
render(){
if(this.state.hasError){
return <div>Oops,err</div>
}
return this.props.children
}
}
// App.jsx
import React from "react"
import ErrorBoundary from "./components/ErrorBoundary"
import ComponentA from "./components/ComponentA"
export class App extends React.Component{
render(){
return (
<ErrorBoundary>
<ComponentA></ComponentA>
</ErrorBoundary>
)
}
}