React 进阶实践指南(笔记)

2022-05-27  本文已影响0人  Small_Song

1

问:老版本的 React 中,为什么写 jsx 的文件要默认引入 React?
如下

import React from 'react'
function Index(){
    return <div>hello,world</div>
}
答:因为 jsx 在被 babel 编译后,写的 jsx 会变成上述 React.createElement 形式,所以需要引入 React,防止找不到 React 引起报错。

2


问: React.createElement 和 React.cloneElement 到底有什么区别呢?

答: 可以完全理解为,一个是用来创建 element 。另一个是用来修改 element,并返回一个新的 React.element 对象。

3

注意:不要尝试给函数组件 prototype 绑定属性或方法,即使绑定了也没有任何作用
,因为通过上面源码中 React 对函数组件的调用,是采用直接执行函数的方式,而不是通过new的方式。

那么,函数组件和类组件本质的区别是什么呢?

对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。
对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在
函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面
的变量会重新声明。

4

export default class index extends React.Component{
    state = { number:0 }
    handleClick1= () => {
          this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback1', this.state.number)  })
          console.log(this.state.number)
          this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback2', this.state.number)  })
          console.log(this.state.number)
          this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback3', this.state.number)  })
          console.log(this.state.number)
    }
    handleClick2= () => {
      setTimeout(()=>{
        this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback1', this.state.number)  })
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1 },()=>{    console.log( 'callback2', this.state.number)  })
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback3', this.state.number)  })
        console.log(this.state.number)
      })
    }
    render(){
        return <div>
            { this.state.number }
            <button onClick={ this.handleClick1 }  >button1</button>
            <button onClick={ this.handleClick2 }  >button2</button>
        </div>
    }
} 

 点击button1打印:0, 0, 0, callback1 1 ,callback2 1 ,callback3 1
刷新,点击button2打印: callback1 1 , 1, callback2 2 , 2,callback3 3 , 3
那么,如何在如上异步环境下,继续开启批量更新模式呢?
React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新
import ReactDOM from 'react-dom'
const { unstable_batchedUpdates } = ReactDOM

setTimeout(()=>{
    unstable_batchedUpdates(()=>{
        this.setState({ number:this.state.number + 1 })
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1})
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1 })
        console.log(this.state.number) 
    })
})

打印: 0 , 0 , 0 , callback1 1 , callback2 1 ,callback3 1

5

更新优先级
handerClick=()=>{
    setTimeout(()=>{
        this.setState({ number: 1  })
    })
    this.setState({ number: 2  })
    ReactDOM.flushSync(()=>{
        this.setState({ number: 3  })
    })
    this.setState({ number: 4  })
}
render(){
   console.log(this.state.number)
   return ...
}
打印 3 4 1 ,相信不难理解为什么这么打印了。

首先 flushSync this.setState({ number: 3 })设定了一个高优先级的更新,所以 2 和 3 被批量更新到 3 ,所以 3 先被打印。
更新为 4。
最后更新 setTimeout 中的 number = 1。

flushSync补充说明:flushSync 在同步条件下,会合并之前的 setState | useState,
可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,
就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。

综上所述, React 同一级别更新优先级关系是:

flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

6

export default function Index(props){
    const [ number , setNumber ] = React.useState(0)
    /* 监听 number 变化 */
    React.useEffect(()=>{
        console.log('监听number变化,此时的number是:  ' + number )
    },[ number ])
    const handerClick = ()=>{
        /** 高优先级更新 **/
        ReactDOM.flushSync(()=>{
            setNumber(2) 
        })
        /* 批量更新 */
        setNumber(1) 
        /* 滞后更新 ,批量更新规则被打破 */
        setTimeout(()=>{
            setNumber(3) 
        })
       
    }
    console.log(number)
    return <div>
        <span> { number }</span>
        <button onClick={ handerClick }  >number++</button>
    </div>
}

7

上述讲的批量更新和 flushSync ,在函数组件中,dispatch 更新效果和类组件是一样的,
但是 useState 有一点值得注意,就是当调用改变 state 的函数dispatch,在本次函数执行
上下文中,是获取不到最新的 state 值的,把上述demo 如下这么改:

const [ number , setNumber ] = React.useState(0)
const handleClick = ()=>{
    ReactDOM.flushSync(()=>{
        setNumber(2) 
        console.log(number) 
    })
    setNumber(1) 
    console.log(number)
    setTimeout(()=>{
        setNumber(3) 
        console.log(number)
    })   
}
结果: 0 0 0

原因很简单,函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量
重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。所以在如上同
一个函数执行上下文中,number 一直为0,无论怎么打印,都拿不到最新的 state 。
|--------问与答---------|

类组件中的 setState 和函数组件中的 useState 有什么异同? 相同点:

首先从原理角度出发,setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,
而且事件驱动情况下都有批量更新规则。
不同点

在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。
但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。

setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;
但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。

setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。

|--------end---------|

8.ref

 场景三:高阶组件转发
如果通过高阶组件包裹一个原始类组件,就会产生一个问题,如果高阶组件 HOC 没有处理 ref ,那么由于高阶组件本身会返回一个新组件,
所以当使用 HOC 包装后组件的时候,标记的 ref 会指向 HOC 返回的组件,而并不是 HOC 包裹的原始类组件,为了解决这个问题,
forwardRef 可以对 HOC 做一层处理。

function HOC(Component){
  class Wrap extends React.Component{
     render(){
        const { forwardedRef ,...otherprops  } = this.props
        return <Component ref={forwardedRef}  {...otherprops}  />
     }
  }
  return  React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) 
}
class Index extends React.Component{
  render(){
    return <div>hello,world</div>
  }
}
const HocIndex =  HOC(Index)
export default ()=>{
  const node = useRef(null)
  useEffect(()=>{
    console.log(node.current)  /* Index 组件实例  */ 
  },[])
  return <div><HocIndex ref={node}  /></div>
}
经过 forwardRef 处理后的 HOC ,就可以正常访问到 Index 组件实例了。
2 ref实现组件通信
如果有种场景不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,
针对这种情况父组件可以通过 ref 模式标记子组件实例,从而操纵子组件方法,这种情况通常发生在一些数据层托管的组件上,
比如 <Form/> 表单,经典案例可以参考 antd 里面的 form 表单,暴露出对外的 resetFields , setFieldsValue 等接口,
可以通过表单实例调用这些 API 。

① 类组件 ref
对于类组件可以通过 ref 直接获取组件实例,实现组件通信。

/* 子组件 */
class Son extends React.PureComponent{
    state={
       fatherMes:'',
       sonMes:''
    }
    fatherSay=(fatherMes)=> this.setState({ fatherMes  }) /* 提供给父组件的API */
    render(){
        const { fatherMes, sonMes } = this.state
        return <div className="sonbox" >
            <div className="title" >子组件</div>
            <p>父组件对我说:{ fatherMes }</p>
            <div className="label" >对父组件说</div> <input  onChange={(e)=>this.setState({ sonMes:e.target.value })}/> 
            <button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) }  >to father</button>
        </div>
    }
}
/* 父组件 */
export default function Father(){
    const [ sonMes , setSonMes ] = React.useState('') 
    const sonInstance = React.useRef(null) /* 用来获取子组件实例 */
    const [ fatherMes , setFatherMes ] = React.useState('')
    const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 调用子组件实例方法,改变子组件state */
    return <div className="box" >
        <div className="title" >父组件</div>
        <p>子组件对我说:{ sonMes }</p>
        <div className="label" >对子组件说</div> <input onChange={ (e) => setFatherMes(e.target.value) }  className="input"  /> 
        <button className="searchbtn"  onClick={toSon}  >to son</button>
        <Son ref={sonInstance} toFather={setSonMes} />
    </div>
}
流程分析:

1 子组件暴露方法 fatherSay 供父组件使用,父组件通过调用方法可以设置子组件展示内容。
2 父组件提供给子组件 toFather,子组件调用,改变父组件展示内容,实现父 <-> 子 双向通信。
9 context
|--------问与答---------|
问:context 与 props 和 react-redux 的对比?

答: context解决了:

解决了 props 需要每一层都手动添加 props 的缺陷。

解决了改变 value ,组件全部重新渲染的缺陷。

react-redux 就是通过 Provider 模式把 redux 中的 store 注入到组件中的。

|--------end---------|
11 .优化篇-渲染控制

React 提供了几种控制 render 的方式。我这里会介绍原理和使用。说到对render 的控制,究其本质,主要有以下两种方式:
第一种就是从父组件直接隔断子组件的渲染,经典的就是 memo,缓存 element 对象。
第二种就是组件从自身来控制是否 render ,比如:PureComponent ,shouldComponentUpdate 。

memo 主要逻辑是

通过 memo 第二个参数,判断是否执行更新,如果没有那么第二个参数,那么以浅比较 props 为 diff 规则。如果相等,当前 fiber 完成工作,停止向下调和节点,所以被包裹的组件即将不更新。
memo 可以理解为包了一层的高阶组件,它的阻断更新机制,是通过控制下一级 children ,也就是 memo 包装的组件,是否继续调和渲染,来达到目的的。
接下来做一个小案例,利用 memo 做到自定义 props 渲染。 规则: 控制 props 中的 number 。

1 只有 number 更改,组件渲染。
2 只有 number 小于 5 ,组件渲染。
function TextMemo(props){ / /子组件
    console.log('子组件渲染')
    return <div>hello,world</div> 
}
const controlIsRender = (pre,next)=>{
   return ( pre.number === next.number ) ||  (pre.number !== next.number && next.number > 5) // number不改变或number 改变但值大于5->不渲染组件 | 否则渲染组件
}
const NewTexMemo = memo(TextMemo,controlIsRender)
class Index extends React.Component{
    constructor(props){
        super(props)
        this.state={
            number:1,
            num:1
        }
    }
    render(){
        const { num , number }  = this.state
        return <div>
            <div>
                改变num:当前值 { num }  
                <button onClick={ ()=>this.setState({ num:num + 1 }) } >num++</button>
                <button onClick={ ()=>this.setState({ num:num - 1 }) } >num--</button>  
            </div>
            <div>
                改变number: 当前值 { number } 
                <button onClick={ ()=>this.setState({ number:number + 1 }) } > number ++</button>
                <button onClick={ ()=>this.setState({ number:number - 1 }) } > number -- </button>  
            </div>
            <NewTexMemo num={ num } number={number}  />
        </div>
    }
}

12. 优化篇-渲染调优

通过本章节,你将学会 Suspense 用法和原理,React.lazy 用法和配合 Suspense 实现代码分割,渲染错误边界、渲染异常的处理手段, 以及 diff 流程以及 key 的合理使用。

// 子组件
function UserInfo() {
  // 获取用户数据信息,然后再渲染组件。
  const user = getUserInfo();
  return <h1>{user.name}</h1>;
}
// 父组件
export default function Index(){
    return <Suspense fallback={<h1>Loading...</h1>}>
        <UserInfo/>
    </Suspense>
}
const LazyComponent = React.lazy(() => import('./test.js'))

export default function Index(){
   return <Suspense fallback={<div>loading...</div>} >
       <LazyComponent />
   </Suspense>
}
 class Index extends React.Component{
   state={
       hasError:false
   }  
   componentDidCatch(...arg){
       uploadErrorLog(arg)  /* 上传错误日志 */
       this.setState({  /* 降级UI */
           hasError:true
       })
   }
   render(){  
      const { hasError } =this.state
      return <div>
          {  hasError ? <div>组件出现错误</div> : <ErrorTest />  }
          <div> hello, my name is alien! </div>
          <Test />
      </div>
   }
}
 class Index extends React.Component{
   state={
       hasError:false
   }  
   static getDerivedStateFromError(){
       return { hasError:true }
   }
   render(){  
      /* 如上 */
   }
}
/**
 * 
 * @param {*} Component  需要异步数据的component 
 * @param {*} api        请求数据接口,返回Promise,可以再then中获取与后端交互的数据
 * @returns 
 */
function AysncComponent(Component,api){
    const AysncComponentPromise = () => new Promise(async (resolve)=>{
          const data = await api()
          resolve({
              default: (props) => <Component rdata={data} { ...props}  />
          })
    })
    return React.lazy(AysncComponentPromise)
}

14. 优化篇-细节处理(持续)

1 React中防抖和节流
2 按需引入

.babelrc 增加对 antd 样式按需引入。

["import", {
    "libraryName":
    "antd",
    "libraryDirectory": "es",
    "style": true
}]

瘦身后体积:

3 React动画

① 首选:动态添加类名
② 其次:操纵原生 DOM
③ 再者:setState + css3

4 及时清除定时器/延时器/监听器
5 合理使用state
6 建议不要在 hooks 的参数中执行函数或者 new 实例
上一篇 下一篇

猜你喜欢

热点阅读