我们用react开发的时候,真正用到的React的api少之又少,基本停留在Component,React.memo等层面,实际react源码中,暴露出来的方法并不少,只是我们平时很少用。但是React暴露出这么多api并非没有用,想要玩转react,就要明白这些API究竟是干什么的,应用场景是什么。
相对于看天书一般的官方文档,本文将按自己的的学习顺序,将大多数的api,从类组件、生命周期、函数组件、hooks各个击破、工具组件到react-dom一一用小demo演示,方便大家理解。
类组件和函数组件
Component 的 Class组件及原理
创建一个类组件
使用
<B />react的思路
对于Component, react 处理逻辑还是很简单的,实例化我们类组件,然后赋值updater对象,负责组件的更新。然后在组件各个阶段,执行类组件的render函数,和对应的生命周期函数就可以了。
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function constructClassInstance(
workInProgress,
ctor,
props
){
const instance = new ctor(props, context);
instance.updater = {
isMounted,
enqueueSetState(){
/* setState 触发这里面的逻辑 */
},
enqueueReplaceState(){},
enqueueForceUpdate(){
/* forceUpdate 触发这里的逻辑 */
}
}
}
state内部数据
类组件不会的setState不立即改变this.state
解决办法,使用回调函数或函数式的写法
props外部数据 常用的组件间通信方式
父传子 子元素通过props或者Instance Methods拿到父元素的值
父组件 App.js:
import React,{ Component } from "react";
import Sub from "./SubComponent.js";
export default class App extends Component{
render(){
return(
<div>
<Sub title = "今年过节不收礼" />
</div>
)
}
}
子组件 SubComponent.js:
import React from "react";
const Sub = (props) => {
return(
<h1>
{ props.title }
</h1>
)
}
export default Sub;
子传父 子元素通过调用父元素的Callback Functions传值给父元素
SubComponent.js:
import React from "react";
const Sub = (props) => {
const cb = (msg) => {
return () => {
props.callback(msg)
}
}
return(
<div>
<button onClick = { cb("我们通信把") }>点击我</button>
</div>
)
}
export default Sub;
App.js:
import React,{ Component } from "react";
import Sub from "./SubComponent.js";
export default class App extends Component{
callback(msg){
console.log(msg);
}
render(){
return(
<div>
<Sub callback = { this.callback } />
</div>
)
}
}
兄弟间通信可以通过父元素为媒介
其他不相关组件通信常用方式有 Portals 、 Redux 以及下面会讲到的 useContext 等
生命周期
react的生命周期,绿色标的为常用组件
constructor
- 基本用途: 初始化props
初始化state,但此时不能调用setState
用来写bind this
shoudComponentUpdate
- 用途 返回true表示不阻止UI更新
返回false表示阻止UI更新
我们可以根据因引用场景设置返回值,手动判断是否要更新,避免不必要的更新
假如操作数据最终没有变化 如下
我们在render中打出'render'发现会出现两次
这个时候我们需要用到shoudComponentUpdate
屏蔽了不必要的更新
这个功能最终被内置成 React.PureComponent组件
PureComponent和 Component用法,差不多一样,唯一不同的是,纯组件PureComponent会浅比较。(在render 之前对比新state和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key。如果所有 key 的值全都一样,就不会 render;如果有任何一个 key 的值不同,就会 render)。所以一般用于性能调优,减少render次数。
render
- 用途
展示视图
return(<div>...</div>)
只有一个根元素
两个根元素的话用<React.Fragment>...</React.Fragment>或者<>...</>包裹
注意:和Fragment区别是,Fragment可以支持key属性。<></>不支持key属性。
componentDidMount
- 用途 在元素插入页面后执行代码,这些代码依赖DOM
比如我们想获取div的高度,最好在这个组件内写
此处可以发起加载数据的AJAX请求(官方推荐)
首次渲染会执行此钩子
componentDidUpdate
- 用途 在视图更新后执行代码
此处也可发起AJAX请求,用于更新数据
首次渲染不会执行此钩子
在此处setState可能会引起无线循环,可以放在if里进行判断跳出循环
若shouldComponentUpdate返回false,则不触发此钩子
componentWillUnmount
- 用途 组件将要被移出页面然后被销毁时执行代码
unmount过的组件不会再次mount
- 举例 1.在c..DidMount里面监听了window scroll,就要在componentWillUnmount中取消监听
2.在c..DidMount里面创建了Timer,就要在componentWillUnmount中取消Timer
3.在c..DidMount里面请求了AJAX,就要在componentWillUnmount中取消请求
原则:谁污染谁治理,不然会占内存
分阶段看钩子执行顺序
react的函数组件
实现+1操作和class组件对比
class组件
函数组件
函数组件代替class组件
useState代替setState
useEffect代替生命周期
hooks各个击破
实现useState
从上面的例子可以看出index的顺序非常重要,react不允许出现有if的情况:
这样做会打乱index的顺序报错:
总结
注意:setState不会立刻修改state
usesState的使用
需要使用浅拷贝才能部分更新:
useReducer
useReducer 接受的第一个参数是一个函数,我们可以认为它就是一个 reducer , reducer 的参数就是常规 reducer 里面的 state 和 action ,返回改变后的 state , useReducer 第二个参数为 state 的初始值 返回一个数组,数组的第一项就是更新之后 state 的值 ,第二个参数是派发更新的 dispatch 函数。
dispatch的内容会传给action
useContext
useEffect
封装useEffect(()=>{},[n])
当使用useEffect(()=>{},[n])时,与useState的初始值冲突会重置一次state的值带来未知风险,我们需要过滤第一次的初始值。
useLayoutEffect
useEffect在浏览器渲染完成后执行
useLayoutEffect在浏览器渲染前执行
useEffect可以用作事件监听,还有一些基于dom的操作。,别忘了在useEffect第一个参数回调函数,返一个函数用于清除事件监听等操作。
const DemoEffect = ({ a }) => {
/* 模拟事件监听处理函数 */
const handleResize =()=>{}
useEffect(()=>{
/* 定时器 延时器等 */
const timer = setInterval(()=>console.log(666),1000)
/* 事件监听 */
window.addEventListener('resize', handleResize)
/* 此函数用于清除副作用 */
return function(){
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
},[ a ])
return (<div >
</div>)
}
useMemo
memo
memo避免了child中数据没有变时的加载,不加时会打印出上面两行log
在child里引用app中的外部函数
这时我们发现当n变化时,child也会加载。这是由于两次执行的新旧函数虽然功能一样,但是地址不不一样,从而触发child的重新加载
解决办法:useMemo
useMemo接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值。
useCallback
useRef
创建useRef时候,会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,那么我们可以利用这个特性,来通过useRef保存一些数据(标签或表单组件)。
注意:当count.current变化时,dom里的值变了,但是不会render更新视图
useRef能做到自动render吗
只需要用useState在每次count.current变化时改变state的值即可
forwardRef
useRef也可以引用dom对象,使用时ref=你命名的对象。这样做的好处是可以不用id或者class去找这个标签
forwardRef让button3可以接受第二个参数ref,使用时在组件内ref={ref}就可以引用这个dom对象了
useImperativeHandle 自定义ref
useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是class类组件,我们可以通过ref获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref的,那么此时useImperativeHandle和 forwardRef配合就能达到效果。
useImperativeHandle接受三个参数:
第一个参数ref: 接受 forWardRef 传递过来的 ref。
第二个参数 createHandle :处理函数,返回值作为暴露给父组件的ref对象。
第三个参数 deps:依赖项 deps,依赖项更改形成新的ref对象。
删除button的示例
简单的自定义hooks
将读和删除接口暴露出去
使用接口
使用hooks避免stale closure的例子
工具类
React.lazy和Suspense实现懒加载
React.lazy和Suspense配合一起用,能够有动态加载组件的效果。React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise ,该 Promise 需要 resolve 一个 default export 的 React 组件。
父组件
import Index from './test'
const LazyComponent = React.lazy(()=> new Promise((resolve)=>{
setTimeout(()=>{
resolve({
default: ()=> <Index />
})
},2000)
}))
render(){
return (
<div className="App">
<div className="context_box" style={ { marginTop :'50px' } } >
<React.Suspense fallback={ <div className="icon" >懒加载前</div> } >
<LazyComponent />
</React.Suspense>
</div>
</div>
);
}
子组件
import React from "react";
class Index extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
--懒加载后--
</div>
)
}
}
export default Index
Profiler性能开销测试
Profiler这个api一般用于开发阶段,性能检测,检测一次react组件渲染用时,性能开销。
Profiler 需要两个参数:
第一个参数:是 id,用于表识唯一性的Profiler。
第二个参数:onRender回调函数,用于渲染完成,接受渲染参数。
const index = () => {
const callback = (...arg) => console.log(arg)
return <div >
<div >
<Profiler id="root" onRender={ callback } >
<Router >
<Meuns/>
<KeepaliveRouterSwitch withoutRoute >
{ renderRoutes(menusList) }
</KeepaliveRouterSwitch>
</Router>
</Profiler>
</div>
</div>
}
结果
- 0 -id: root -> Profiler 树的 id 。
- 1 -phase: mount -> mount 挂载 , update 渲染了。
- 2 -actualDuration: 6.685000262223184 -> 更新 committed 花费的渲染时间。
- 3 -baseDuration: 4.430000321008265 -> 渲染整颗子树需要的时间
- 4 -startTime : 689.7299999836832 -> 本次更新开始渲染的时间
- 5 -commitTime : 698.5799999674782 -> 本次更新committed 的时间
- 6 -interactions: set{} -> 本次更新的 interactions 的集合
StrictMode严格模式
StrictMode见名知意,严格模式,用于检测react项目中的潜在的问题。与 Fragment 一样, StrictMode 不会渲染任何可见的 UI 。它为其后代元素触发额外的检查和警告。
严格模式检查仅在开发模式下运行;它们不会影响生产构建,只会触发警告。
StrictMode目前有助于:
①识别不安全的生命周期。
②关于使用过时字符串 ref API 的警告
③关于使用废弃的 findDOMNode 方法的警告
④检测意外的副作用
⑤检测过时的 context API
开启严格模式:
<React.StrictMode>
...
</React.StrictMode>
createElement
提到createElement,就不由得和JSX联系一起。我们写的jsx,最终会被 babel,用createElement编译成react元素形式。
例如:
render(){
return <div className="box" >
<div className="item" >生命周期</div>
</div>
}
被解析成:
React.createElement("div", {
className: "box"
}, React.createElement("div", {
className: "item"
}, "\u751F\u547D\u5468\u671F"));
createElement的模型:
React.createElement(
type,
[props],
[...children]
)
第一个参数:如果是组件类型,会传入组件,如果是dom元素类型,传入div或者span之类的字符串。
第二个参数:第二个参数为一个对象,在dom类型中为属性,在组件类型中为props。
其他参数:为children
cloneElement
cloneElement的作用是以 element 元素为样板克隆并返回新的 React 元素。
我们可以在组件中,劫持children element,然后通过cloneElement克隆element,混入props。经典的案例就是 react-router中的Swtich组件,通过这种方式,来匹配唯一的 Route并加以渲染。
我们设置一个场景,在组件中,去劫持children,然后给children赋能一些额外的props:
function FatherComponent({ children }){
const newChildren = React.cloneElement(children, { age: 18})
return <div> { newChildren } </div>
}
function SonComponent(props){
console.log(props)
return <div>hello,world</div>
}
class Index extends React.Component{
render(){
return <div className="box" >
<FatherComponent>
<SonComponent name="alien" />
</FatherComponent>
</div>
}
}
createContext
createContext用于创建一个Context对象,createContext对象中,包括用于传递 Context 对象值 value的Provider,和接受value变化订阅的Consumer。
const MyContext = React.createContext(defaultValue)
createContext接受一个参数defaultValue,如果Consumer上一级一直没有Provider,则会应用defaultValue作为value。只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。
function ComponentB(){
/* 用 Consumer 订阅, 来自 Provider 中 value 的改变 */
return <MyContext.Consumer>
{ (value) => <ComponentA {...value} /> }
</MyContext.Consumer>
}
function ComponentA(props){
const { name , mes } = props
return <div>
<div> 姓名: { name } </div>
<div> 想对大家说: { mes } </div>
</div>
}
function index(){
const [ value , ] = React.useState({
name:'alien',
mes:'let us learn React '
})
return <div style={{ marginTop:'50px' }} >
<MyContext.Provider value={value} >
<ComponentB />
</MyContext.Provider>
</div>
}
打印结果:
createRef
createRef可以创建一个 ref 元素,附加在react元素上。
用法:
class Index extends React.Component{
constructor(props){
super(props)
this.node = React.createRef()
}
componentDidMount(){
console.log(this.node)
}
render(){
return <div ref={this.node} > my name is alien </div>
}
}
函数组件里
function Index(){
const node = React.useRef(null)
useEffect(()=>{
console.log(node.current)
},[])
return <div ref={node} > my name is alien </div>
}
isValidElement
这个方法可以用来检测是否为react element元素,接受待验证对象,返回true或者false。
这个api可能对于业务组件的开发,作用不大,因为对于组件内部状态,都是已知的,我们根本就不需要去验证,是否是react element 元素。 但是,对于一起公共组件或是开源库,isValidElement就很有作用了。
没有用isValidElement验证之前:
const Text = () => <div>hello,world</div>
class WarpComponent extends React.Component{
constructor(props){
super(props)
}
render(){
return this.props.children
}
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
<Text/>
<div> my name is alien </div>
Let's learn react together!
</WarpComponent>
</div>
}
我们用isValidElement进行react element验证:
class WarpComponent extends React.Component{
constructor(props){
super(props)
this.newChidren = this.props.children.filter(item => React.isValidElement(item) )
}
render(){
return this.newChidren
}
}
Children.map
React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。
有的同学会问遍历 children用数组方法,map ,forEach 不就可以了吗? 请我们注意一下不透明数据结构,什么叫做不透明结构?
我们先看一下透明的结构:
class Text extends React.Component{
render(){
return <div>hello,world</div>
}
}
function WarpComponent(props){
console.log(props.children)
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
<Text/>
<Text/>
<Text/>
<span>hello,world</span>
</WarpComponent>
</div>
}
但是我们把Index结构改变一下:
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map(()=><Text/>) }
<span>hello,world</span>
</WarpComponent>
</div>
}
这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历,每一个子元素。此时就需要 react.Chidren 来帮忙了。
我们把WarpComponent组件用react.Chidren处理children:
function WarpComponent(props){
const newChildren = React.Children.map(props.children,(item)=>item)
console.log(newChildren)
return newChildren
}
注意 如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。
Children.forEach
Children.forEach和Children.map 用法类似,Children.map可以返回新的数组,Children.forEach仅停留在遍历阶段。
Children.count
children 中的组件总数量,等同于通过 map 或 forEach 调用回调函数的次数。对于更复杂的结果,Children.count可以返回同一级别子组件的数量。
function WarpComponent(props){
const childrenCount = React.Children.count(props.children)
console.log(childrenCount,'childrenCount')
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map((item,index) => new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
<span>hello,world</span>
</WarpComponent>
</div>
}
Children.toArray
Children.toArray返回,props.children扁平化后结果。
function WarpComponent(props){
const newChidrenArray = React.Children.toArray(props.children)
console.log(newChidrenArray,'newChidrenArray')
return newChidrenArray
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map((item,index)=>new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
<span>hello,world</span>
</WarpComponent>
</div>
}
Children.only
验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。
function WarpComponent(props){
console.log(React.Children.only(props.children))
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map((item,index)=><Text key={index} />) }
<span>hello,world</span>
</WarpComponent>
</div>
}
工具组件参考了React进阶,作者总结的很全面
react-dom
render
render 是我们最常用的react-dom的 api,用于渲染一个react元素,一般react项目我们都用它,渲染根部容器app。
ReactDOM.render(element, container[, callback])
使用:
ReactDOM.render(
< App / >,
document.getElementById('app')
)
hydrate
服务端渲染用hydrate。用法与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。
ReactDOM.hydrate(element, container[, callback])
createPortal
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。createPortal 可以把当前组件或 element 元素的子节点,渲染到组件之外的其他地方。
那么具体应用到什么场景呢?
比如一些全局的弹窗组件model,组件一般都写在我们的组件内部,倒是真正挂载的dom,都是在外层容器,比如body上。此时就很适合createPortalAPI。
createPortal接受两个参数:
ReactDOM.createPortal(child, container)
第一个: child 是任何可渲染的 React 子元素 第二个: container是一个 DOM 元素。
function WrapComponent({ children }){
const domRef = useRef(null)
const [ PortalComponent, setPortalComponent ] = useState(null)
React.useEffect(()=>{
setPortalComponent( ReactDOM.createPortal(children,domRef.current) )
},[])
return <div>
<div className="container" ref={ domRef } ></div>
{ PortalComponent }
</div>
}
class Index extends React.Component{
render(){
return <div style={{ marginTop:'50px' }} >
<WrapComponent>
<div >hello,world</div>
</WrapComponent>
</div>
}
}
unstable_batchedUpdates
在react-legacy模式下,对于事件,react事件有批量更新来处理功能,但是这一些非常规的事件中,批量更新功能会被打破。所以我们可以用react-dom中提供的unstable_batchedUpdates 来进行批量更新。
一次点击实现的批量更新
class Index extends React.Component{
constructor(props){
super(props)
this.state={
numer:1,
}
}
handerClick=()=>{
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
}
render(){
return <div style={{ marginTop:'50px' }} >
<button onClick={ this.handerClick } >click me</button>
</div>
}
}
批量更新条件被打破
handerClick=()=>{
Promise.resolve().then(()=>{
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
})
}
unstable_batchedUpdate助力
handerClick=()=>{
Promise.resolve().then(()=>{
ReactDOM.unstable_batchedUpdates(()=>{
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
this.setState({ numer : this.state.numer + 1 })
console.log(this.state.numer)
})
})
}
渲染次数一次,完美解决批量更新问题。
flushSync
flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。我们知道react设定了很多不同优先级的更新任务。如果一次更新任务在flushSync回调函数内部,那么将获得一个较高优先级的更新。比如
import ReactDOM from 'react-dom'
class Index extends React.Component{
state={ number:0 }
handerClick=()=>{
setTimeout(()=>{
this.setState({ number: 1 })
})
this.setState({ number: 2 })
ReactDOM.flushSync(()=>{
this.setState({ number: 3 })
})
this.setState({ number: 4 })
}
render(){
const { number } = this.state
console.log(number) // 打印什么??
return <div>
<div>{ number }</div>
<button onClick={this.handerClick} >测试flushSync</button>
</div>
}
}
unmountComponentAtNode
从 DOM 中卸载组件,会将其事件处理器和 state 一并清除。 如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true ,如果没有组件可被移除将会返回 false 。
function Text(){
return <div>hello,world</div>
}
class Index extends React.Component{
node = null
constructor(props){
super(props)
this.state={
numer:1,
}
}
componentDidMount(){
/* 组件初始化的时候,创建一个 container 容器 */
ReactDOM.render(<Text/> , this.node )
}
handerClick=()=>{
/* 点击卸载容器 */
const state = ReactDOM.unmountComponentAtNode(this.node)
console.log(state)
}
render(){
return <div style={{ marginTop:'50px' }} >
<div ref={ ( node ) => this.node = node } ></div>
<button onClick={ this.handerClick } >click me</button>
</div>
}
}