React高级指引与Hooks笔记

2,437 阅读13分钟
本文主要撰写关于React 16.8的进阶概念的理解 

我的文笔不太好,大部分内容会通过代码演示来叙述。 

本文章仅代表个人观点。 如果内容有误,欢迎指正

React高级指引

1.代码分割

React文档的高级指引中除开无障碍后的第一个篇幅就是说明代码分割,通常我们敲代码都是通过export导出,import导入。通过打包后的生产环境包会一次性加载所有模块。代码分割实际上是一种优化手段

React.lazy是React为代码分割而提供的API,通过React.lazy来实现懒加载

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

React.lazy(()=>import('./one'))
React.lazy(()=>Promise.resolve(import('./one')))   
//这两句是等价的

这两段代码都会懒加载./one文件中export default的默认模块,文档中还示例了一种方法

...
return ( 
<div>
<Suspense fallback={<div>loading...</div>}>
<Two/>
<One/>
</Suspense>
</div>
)

Suspense是React对懒加载的一个补充,当加载未完成时fallback属性中的JSX暂时替代Suspense的children,懒加载完成后就会显示Two和One组件

2.Context

为了理解Context,我们来想象一棵树,每个模块都是分支节点,React的props数据流是由父节点向子节点传递的信息,子节点不能修改父节点传递来的信息,而Context就像公共信息站,在父节点中设置信息站,子节点获取Context是去寻找最近的由这个Context设置的信息站,创建一个Context很简单,使用React提供的API来新建Context

//Context.js
const Bird=React.createContext('bird')
export Bird

bird字符串是这个Context的默认信息,当向上寻找没有找到公共信息站时就会返回这个默认信息

//App.js
import {Bird} from './Context'
import Case from './Case'
function App(){
return (
    <Bird.Provider value={{a:0}}>
    <Case />
</Bird.Provider>
)}

导入这个Context 在父节点,设置它的公共信息站并携带信息{a:0},多个信息站会获取最近的那一个所携带的信息,例如:

return (
    <Bird.Provider value={{a:0}}>
    <Bird.Provider value={{a:10}}>
    <Case />        //在Case中获取Bird的信息站会获取{a:10}
</Bird.Provider>
</Bird.Provider>
)

接下来是演示如何获取到Bird信息站的信息

//Case.js
import {Bird} from './Context'
function Case(){
return (
    <Bird.Consumer>
    {value=>{
    console.log(value.a++)
    return(
    <div>value.a</div>    
)
}}
</Bird.Consumer>
)
}
export default Case

通过Bird.Consumer请求最近的信息站Bird.Provider

这里要注意一点,Bird.Consumer的成员只能是一个函数,如果携带JSX则会报错。函数的参数value是信息站中传入的value信息

Context不像Props不能修改传来的数据,也不像Redux需要通过向Reducer发送事件函数进行修改,Context.Consumer中可以直接修改信息站的信息

//React文档中说明的另外一种写法,Class组件绑定一个Context
import React from 'react'
import {Bird} from './Context'
class CaseClass extends React.Component{
    render(){
    console.log(this.context.a)        //10    this.context返回了最近的信号站中的信息
    return (<></>)
}
}
CaseClass.contextType=Bird    //绑定到Bird

3.Refs转发

Refs转发的概念是将组件内的ref转发到其他组件供其使用

正常情况下我们在父组件中声明Ref对象,传入子组件,在子组件中绑定Ref会报错

class App extends React.Component{
constoructor(props){
super(props)
this.ref=React.createRef()
}
render(){
    return (
    <Case v={this.ref}/>
)}}
function Case(props){
    return(<button ref={props.v}></button>)    //报错
}
//

React对这种情况提供了一种解决方法:React.forwarRef 。这个方法接受一个函数,函数的参数分别是props和ref,现在让我们重新写一下这个示例

class App extends React.Component{
constoructor(props){
super(props)
this.ref=React.createRef()
}
render(){
return (<Refs ref={this.ref}/>)
}
}
function Case(props,ref){
return(<button ref={ref}>Hello World</button>)
}
const Refs=React.forwarRef(Case)
//现在App中的this.ref拿到Case中的button元素了

Context也可以实现Refs转发

4.Fragments

JSX的最外层必须是只有一个父元素

let b=<span></span><span></span>;    //报错
let b=<div><span></span><span></span></div>    //OK
//往往这个父元素并不是项目所必须的,React.Fragment是用来解决这种情况的组件,而且它不附带元素,只是分组
let b=<React.Fragment><span></span><span></span></React.Fragment>    //OK
let b=<><span></span><span></span></>    //OK,这是React.Fragment的简写

5.高阶组件

引用文档中的一句话:高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

React并不提倡使用继承来复用组件逻辑,而是一种叫"组合"的概念,比如A需要向服务器请求一段数据,B同样需要向服务器请求同一段数据,那么就可以将他们组合起来。高阶函数的概念和工厂函数的概念有些类似,但不一样。直接上代码

//我们需要设置三个组件,他们渲染的JSX不同但他们都拥有同一个逻辑(比如向控制台输出0)
class A extends React.Component{
    ...独特的逻辑
    render(){
    <div>{props.v}</div>
    }
}
...B    参考A
...C    参考A
function HOC(Component){    //HOC高阶组件应该是一个纯函数
    return class D extends React.Component{    //这个类名不重要
        constructor(props){
        super(props)    
        }
        componentDidMount(){
        console.log(0)
        }
        render(){
        return(<Component {...this.props}/>)
        }
    }
}
const As=HOC(A),Bs=HOC(B),Cs=HOC(C)
class App extends React.Component{
    render(){
    return (<><As v={'a'}/><Bs v={'b'}/><Cs v={'c'}/></>)
    }
}
ReactDOM.render(<App/>,document.querySelector('#root'))
//页面渲染了三个DIV 内容分别是a,b,c    并且输出了3次0

这是一个很简单的示例,高阶组件应该是一个纯函数,返回一个组件来处理重复逻辑再渲染传入的组件。

6.Portals

Portals是一种将子节点渲染到父节点之外的DOM节点的优秀方案,让子节点渲染到其他DOM元素上,它的API是ReactDOM中的createPortal方法,这个方法接受两个参数,第一个参数是任何可渲染的React子元素,例如JSX或字符串,第二个参数是目标DOM元素

现在我们来实现一个简单的点击按钮+1的demo,并用createPortal方法挂到根元素#root上

class App extends React.Component{
    constructor(props){
    super(props)
    this.state={number:0}
    }
    handleClick(){
    let i=this.state.number
    this.setState({number:i+1})
    }
    render(
    <div style={{display:'none'}}>
    <div>这是APP组件</div>
    <A><button onClick={this.hanldeClick.bind(this)}>点击这里{this.state.number}</button></A>
    </div>
    )
}
function A(props){
    return ReactDOM.createPortal(props.children,document.querySelector('#root'))
}
ReactDOM.render(<App/>,document.querySelector('#root'))

再让我们康康DOM结构      PS:我点了几下


React Hooks

React的组件分为函数组件和class组件,两者的用法截然不同。我们先从三个基础Hook API开始

1.useState

useState是函数组件的状态管理方案

console.log(useState(1))    //返回一个拥有两个成员的数组 第一个成员是1 第二个成员是修改state的函数
let [case,useCase]=useState('change')    //使用ES6的解构赋值可以快速取得state和修改函数

在使用Hooks的过程中我发现函数组件的状态更新会使整个组件函数重新执行一遍。让我们来看一个例子

let i=0
function App(props){
    console.log(i++)
    let [case,useCase]=useState(0)
    setInterval(()=>{useCase(new Date())},1000)
    return (<>{i}</>)
}
//这串代码的控制台输出0 1 2 3 4 5 6 ...这在预期之中

但不同的是渲染到页面中的数字是以1 2 4 8 16 ...这种形式的更新。这和class组件中的constructor函数不同,constructor函数会在挂载前执行一次计时器。试验证明,调用状态更新的useCase会使这个函数重新执行一遍,函数中的计时器并没有被销毁,每次计时器触发又会再执行useCase触发更新。

纯函数与副作用

在继续写之前,先讲一下函数式编程中的两个概念。

首先,什么是一个纯函数呢?设想一下:一家医院,我丢进去一只羊,它送出来羊的体检单。丢进去的那只羊还是和之前一样。

function hospital(sheep){
    ...检查羊的身体
    return {羊的数据}
}
hosipital(羊)
//羊完好无恙,显然这是一个纯函数

那么,什么是副作用呢?就是这家医院附加了修剪羊毛这个行为,这是个副作用

function hospital(sheep){
    ...检查羊的身体
    修剪羊毛
    return {羊的数据}
}
hospital(羊)
//羊变成了秃羊,显然这是产生的副作用

当然纯函数和副作用只是对函数的定义,没有好坏之分。

2.useEffect

我写的例子中,在useEffect中调用状态变更函数并不提倡大家这样写,这会使useEffect形成死循环。但我想不到什么代码量少还清晰的例子了,略略略~

接下来的useEffect就是React为了处理副作用而提供的API,useEffect接收两个参数:一个副作用函数和可选的数组参数。要理解这个useEffect的机制,我们需要很多试验!

首先我们来改写之前写到的App组件

let i=0
function App(props){
    console.log(i++)
    let [case,useCase]=useState(0),time
    useEffect(()=>{
    time=setInterval(()=>{useCase(new Date())},1000)
})
    return (<>i</>)
}

现在我们来看看效果,和没加useEffect是一样的,这是因为我们并没有销毁计时器,useEffect的第一个函数参数返回的函数就是在状态被修改前调用的,可以理解为当前状态被弃用并且将要以新状态更新前。知道原因后我们再修改一下

let i=0
function App(props){
    console.log(i++)
    let [case,useCase]=useState(0),time
    useEffect(()=>{
    time=setInterval(()=>{useCase(new Date())},1000)
    return ()=>{console.log(time);clearInterval(time)}    //返回了销毁副作用的函数
})
    return (<>{i}</>)
}

好像成功了,这时画面上渲染i是1 2 3 4 5 ...这符合每次更新状态函数组件调用,但我们看向控制台,time的输出也一直在变,这意味着time计时器一直在循环清除与建立,这是因为我们没有向useEffect的第二个参数添加固定依赖,导致任何状态更新都会让useEffect调用副作用函数和销毁副作用函数,我现在需要它像componentDidMount和componentWillUnmount一样,不要依赖任何状态。我们再修改一下这个例子

let i=0
function App(props){
    console.log(i++)
    let [case,useCase]=useState(0),time
    useEffect(()=>{
    time=setInterval(()=>{useCase(new Date())},1000)
    return ()=>{console.log(time);clearInterval(time)}
},[])        //不依赖任何状态
    return (<>{i}</>)
}

现在它完全符合了我的需求,即使useEffect副作用函数中调用了状态变更函数useCase,useEffect也没有理会它,他只会在第一次状态设置(参考组件挂载)调用副作用函数,在所有状态被清除时(参考组件卸载)调用清除副作用函数

useEffect篇,我任性的用我的理解去写,我看到别人会照着生命周期去解读useEffect,我认为函数组件是没有生命周期这个概念的,它的概念更像是面向状态,照着生命周期去解读会让人混乱

3.useContext

这个比较好理解,就是函数组件对MyContext.Consumer的补充,写段代码就能理解

let MyContext=createContext('bird'),YourContext=createContext('hi')
function App(props){
    let i=useContext(MyContext)
    let y=useContext(YourContext)
    return (<>{i} {y}</>)
}
//渲染App会渲染出bird hi

这很方便但是所有的HooksAPI都不能在class组件中使用,当然你也可以继续在函数组件中使用MyContext.Consumer

4.useReducer

熟悉Redux的人看到reducer一定会熟悉,这是React对useState的替代方案,它接受一个形如(state,action)=>new state的reducer函数,并返回当前的state以及与其配套的dispatch。

要理解reducer,我们得先写一个标准的reducer模板

const initialState={count:0}         //初始的state
function reducer(state,action){      //事件接收函数 
switch(action.type){        
        case 'addition':
            return {count:state.count++}
        case 'reduce':
            return {count:state.count--}
        default:
            return new Error('不存在')    }}
function App(props){
    let [state,dispatch]=useReducer(reducer,initialState)
    return (<>{state.count}</>)     //渲染0
}

initialState是state的初始对象,reducer是一个事件接收器函数,现在我们调用dispatch函数

dispatch({type:'addition'})

这时reducer事件接收器函数就会接收到这个事件对象,这时reducer函数的第一个参数是state对象,也就是initialState对象,action就是dispatch传入的这个对象,函数开始执行,返回的对象就是新的state对象。这时从useReducer获取的state就会更新。

不久后我会开个坑写一个简易Redux,也可以通过那个去理解reducer函数机制

更有趣的是你还可以结合Context和高阶组件写一个全局reducer

//reducer.js
const initialState={count:0},Bird=createContext('bird')
function reducer(state,action){
    switch(action.type){
        case 'addition':
            return {count:state.count+1}
        case 'reduce':
            return {count:state.count-1}
        default:
            return new Error('不存在')
    }}
function Context(Component){
    return function(){
    const [state,dispatch]=useReducer(reducer,initialState)
    let store={state,dispatch}
    return (<Bird.Provider value={{store}}>
        <Component />
    </Bird.Provider>)
    }}
export {Context,Bird}

// App.js
import {Context,Bird} from 'reducer.js'
function App(props){
    let {store}=useContext(Bird)
    return (<>{store.state.count}</>)
}
let Apx=Context(App)
ReactDOM.render(<Apx />,document.querySelector('#root'))//这时页面渲染一个0

5.useCallback

useCallback是对useEffect相同逻辑的封装,来看一个例子

function App(props){
    let [a,useA]=useState(0),[b,useB]=useState(0)
    useEffect(()=>{
    console.log(a)
},[a,b])
    let handleClick=function(){useB(b+1)}
    return (<>{b}<button onClick={handleClick}>点击</button></>)
}

如果已经理解前面的概念,看懂这个例子并不困难,现在用useCallback改写

function App(props){
    let [a,useA]=useState(0),[b,useB]=useState(0)
    let ABCallbakc=useCallback(()=>{
    console.log(a)
},[a,b])    
    useEffect(()=>{
    ABCallback()
},[ABCallbakc])    let handleClick=function(){useB(b+1)}
return (<>{b}<button onClick={handleClick}></button></>)
}

这两个例子的效果是一样的

6.useMemo

useMemo主要用于性能优化,它接收一个函数参数和一个依赖数组参数

let memoizedValue=useMemo(()=>{return a*10},[a])

memoizedValue会在a依赖变化时调用那个函数参数,为了更好的理解它我们再来写个例子

function App() {
    const [count1, changeCount1] = useState(0);
    const [count2, changeCount2] = useState(10);
  
    const Count = useMemo(() => {
      console.log(0)
      return count2 * 10;
    }, [count2]);
    function as(){
        console.log(1)
        return count1*10
    }
    return (
      <div>
        {Count} and {as()}
        <button onClick={() => { changeCount1(count1 + 1); }}>改变count1</button>
        <button onClick={() => { changeCount2(count2 + 1); }}>改变count2</button>
      </div>
    );
  }

当我们点击第一个按钮时控制台会输出1,因为Count的依赖中没有count1所以Count没有输出0。当我们点击第二个按钮时,控制台会输出0,也会输出1,as函数每次更新都会重新调用。使用useMemo可以优化它

React官方文档中还提到:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

7.useRef

这是属于Hooks的Ref API,用法与createRef区别不大

function App(){
    let a=useRef(null)
    let b=useRef(null)
    useEffect(()=>{
      console.log(a.current,b.current)
      },[])
    return (
      <div>
        <button ref={a}>改变count1</button>
        <button ref={b}>改变count2</button>
      </div>
    );
  }

自定义Hook

我们可以通过现有的API自定义自己想要的Hook函数

function useCount(init){
    let [count,changeCount]=useState(init)
    const increase=()=>{
        changeCount(count+1)
    }
    const decrease=()=>{
        changeCount(count-1)
    }
    const reset=()=>{
        changeCount(0)
    }
    return [count,{increase,decrease,reset}]
}
function App() {
    let [a,changeA]=useCount(10)
    return (
      <div>
        <button onClick={changeA.increase}>增加:{a}</button>
        <button onClick={changeA.decrease}>减少:{a}</button>
        <button onClick={changeA.reset}>重置:{a}</button>
      </div>
    );
  }


欢迎讨论,完结撒花~