react进阶(90%+的api)

675 阅读14分钟

我们用react开发的时候,真正用到的React的api少之又少,基本停留在Component,React.memo等层面,实际react源码中,暴露出来的方法并不少,只是我们平时很少用。但是React暴露出这么多api并非没有用,想要玩转react,就要明白这些API究竟是干什么的,应用场景是什么。

相对于看天书一般的官方文档,本文将按自己的的学习顺序,将大多数的api,从类组件、生命周期、函数组件、hooks各个击破、工具组件到react-dom一一用小demo演示,方便大家理解。

类组件和函数组件

Component 的 Class组件及原理

创建一个类组件 image.png 使用 <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内部数据

image.png

image.png

类组件不会的setState不立即改变this.state

image.png

解决办法,使用回调函数或函数式的写法

image.png

image.png

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 等

生命周期

image.png

react的生命周期,绿色标的为常用组件

image.png

constructor

  • 基本用途: 初始化props

初始化state,但此时不能调用setState

用来写bind this

image.png

shoudComponentUpdate

  • 用途 返回true表示不阻止UI更新

返回false表示阻止UI更新

我们可以根据因引用场景设置返回值,手动判断是否要更新,避免不必要的更新

假如操作数据最终没有变化 如下

image.png

我们在render中打出'render'发现会出现两次

image.png

image.png

这个时候我们需要用到shoudComponentUpdate

image.png

屏蔽了不必要的更新

image.png

这个功能最终被内置成 React.PureComponent组件

image.png

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属性。

image.png

componentDidMount

  • 用途 在元素插入页面后执行代码,这些代码依赖DOM

比如我们想获取div的高度,最好在这个组件内写

此处可以发起加载数据的AJAX请求(官方推荐)

首次渲染会执行此钩子

image.png image.png

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中取消请求

原则:谁污染谁治理,不然会占内存

分阶段看钩子执行顺序

image.png

react的函数组件

实现+1操作和class组件对比

class组件

image.png

函数组件

image.png

函数组件代替class组件

useState代替setState

image.png

useEffect代替生命周期

image.png

hooks各个击破

实现useState

image.png

image.png

从上面的例子可以看出index的顺序非常重要,react不允许出现有if的情况:

image.png

这样做会打乱index的顺序报错:

image.png

总结

image.png

注意:setState不会立刻修改state

usesState的使用

image.png

需要使用浅拷贝才能部分更新:

image.png image.png

useReducer

useReducer 接受的第一个参数是一个函数,我们可以认为它就是一个 reducer , reducer 的参数就是常规 reducer 里面的 state 和 action ,返回改变后的 state , useReducer 第二个参数为 state 的初始值 返回一个数组,数组的第一项就是更新之后 state 的值 ,第二个参数是派发更新的 dispatch 函数。

image.png

image.png

dispatch的内容会传给action

useContext

image.png

image.png

useEffect

image.png

封装useEffect(()=>{},[n])

当使用useEffect(()=>{},[n])时,与useState的初始值冲突会重置一次state的值带来未知风险,我们需要过滤第一次的初始值。

image.png image.png

useLayoutEffect

image.png

useEffect在浏览器渲染完成后执行

useLayoutEffect在浏览器渲染前执行

image.png

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

image.png

memo避免了child中数据没有变时的加载,不加时会打印出上面两行log

在child里引用app中的外部函数

image.png

这时我们发现当n变化时,child也会加载。这是由于两次执行的新旧函数虽然功能一样,但是地址不不一样,从而触发child的重新加载

解决办法:useMemo

useMemo接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值。

image.png

image.png

useCallback

image.png

useRef

创建useRef时候,会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,那么我们可以利用这个特性,来通过useRef保存一些数据(标签或表单组件)。 image.png

image.png

注意:当count.current变化时,dom里的值变了,但是不会render更新视图

useRef能做到自动render吗

image.png

只需要用useState在每次count.current变化时改变state的值即可

forwardRef

image.png

useRef也可以引用dom对象,使用时ref=你命名的对象。这样做的好处是可以不用id或者class去找这个标签

image.png

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的示例

image.png

简单的自定义hooks

将读和删除接口暴露出去

image.png

使用接口

image.png

使用hooks避免stale closure的例子

image.png

工具类

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>
}

结果

image.png

  • 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"));

babel在线解析

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>   
    }
}

image.png

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>
}

打印结果:

image.png

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>
}

image.png

我们用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
    }
}

image.png

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>
}

image.png

但是我们把Index结构改变一下:

function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WarpComponent>
            { new Array(3).fill(0).map(()=><Text/>) }
            <span>hello,world</span>
        </WarpComponent>
    </div>
}

image.png

这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历,每一个子元素。此时就需要 react.Chidren 来帮忙了。

我们把WarpComponent组件用react.Chidren处理children:

function WarpComponent(props){
    const newChildren = React.Children.map(props.children,(item)=>item)
    console.log(newChildren)
    return newChildren
}

image.png

注意 如果 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>
}

image.png

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>
}

image.png

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>
}

image.png

工具组件参考了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>
    }
}

image.png

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>
    }
}

image.png

批量更新条件被打破

 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)
    })
  }

image.png

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>
    }
}

image.png

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>
    }
}

image.png