[React] 一文式React入门级教程

225 阅读22分钟

全文所使用的版本为React17版本。

全文为本人学习的随笔,以官网提供的内容为准,参考不可保证其正确性与权威性。


1.1.create-react-app脚手架

什么是cra:与vue-cl是同一类工具,为react脚手架。 先在开发环境中安装cra。

  yarn global add create-react-app

在安装完成后可以进入需要存放项目文件夹,创建react项目。

  create-react-app project-name

在创建完项目后,可以按照提示试运行。运行后可以看到初始化的页面,旋转的react图标。

  cd project-name
  yarn start

1.2.index.js

将src中除index.js外的文件全部删除。并在index.js中删减至如下内容。

  import React from 'react';
  import ReactDOM from 'react-dom/client';

  const root = document.querySelector('#root')
  const App = React.createElement('div',{className: 'red'}, n)
  ReactDOM.render(App,root)

App为一个由React.createElement()创建的节点。 经过ReactDOM.render()渲染到了id为root的div上。


1.3.React.createElement()

React.createElement()是用于创建React元素的函数。其接受的参数为: 1. 节点标签类型 2. 选项,className为其中一个选项 3. 节点内部的内容 React.createElement()的第三个参数为节点的内部内容,其如果有多个内容,则接受数组的形式 React.createElement()创建的是一个React元素,又称虚拟DOM对象,不能直接插入到页面中进行渲染,需要借助ReactDOM.render()进行渲染。


1.4.累加器

向App的createElement的第三个参数中,添加一个累加的按钮。

  React.createElement('button',
    {onClick: ()=>{
        n += 1
      }},
    '+1'
    )

将其放入App的createElement的第三个参数中。

  const App = React.createElement('div',{className: 'red'},[
    n,
    React.createElement('button',
    {onClick: ()=>{
        n += 1
      }},
    '+1'
    )
  ])

此时在页面中点击+1按钮,无法在页面上生效。


1.5.React手动渲染

App是一个const变量,其createElement中第三参数使用的n的值,在其定义声明时就已经确定,不再变更,要实现数据驱动页面,则需要实现App中n的值的变更,与页面的重新渲染。 ReactDOM.render()能够实现页面的重新渲染,此时需要再实现App中更新n的值。 将App写成一个返回react元素的函数,可以实现更新n的值

  const App = ()=> React.createElement()

此时当n的值变更时,调用App()函数,就能更新n的值。我们将App()的调用及其渲染写入到按钮的onClick事件函数中。

 const App = ()=> React.createElement('div',{className: 'red'},[
    n,
    React.createElement('button',
    {onClick: ()=>{
        n += 1
        ReactDOM.render(App(), root)
      }},
    '+1'
    )
  ])

1.6.DOM Diff算法更新渲染机制

ReactDOM.render()在更新渲染时,会使用DOM Diff算法。 DOM Diff算法会判断更新前后的两个React元素的差异,只重新渲染不同的地方。


2.1.JSX

Vue内的vue-loader使Vue能够在Vue-cli中的.vue文件内写template模板。 而React中有与Vue的template类似的JSX。babel-loader会将JSX语法转化为js

  import React from 'react'

  const App = () =>{
    return <>
      <div className="red">n</div>
    </>
  }

上述JSX会被babel-loader转译为:

  React.createElement('div',{className: 'red'}, 'n')

JSX的语法中没有实质写到React的引用,但依然需要引入React才能生效。 return后需要接<></>或(),并将html内容写入其中,<></>内可以写入多个标签,()内只能有一个根标签。 class需要写成className,以避免js中的关键字冲突,因为JSX本质依然是js。 JSX语法中标签内的所有JS代码需要使用{}包裹。 变量需要使用{}包裹。 对象需要使用{}包裹。


2.2.JSX中的v-if/if...else

JSX语法能够实现根据条件,返回不同的React元素。 实现与Vue-template中v-if一样的效果。

  const Component = ()=> {
    return n%2 - - - 0 
    ?
    <div>n是偶数</div>
    :
    <div>n是基数</div>
  }

注意上述强调的,标签内的js需要用{}包裹,如下述所写,div.wrapper内部会被当作标签的内容,而不会当作js,需要在外部加上{}包裹。

 const Component = ()=> {
  return (
    <div class="wrapper">
      n%2 - - - 0 
      ?
      <div>n是偶数</div>
      :
      <div>n是基数</div>
    </div>
  )
 }
  const Component = ()=> {
    return (
      <div class="wrapper">
      {
        n%2 - - - 0 
        ?
        <div>n是偶数</div>
        :
        <div>n是基数</div>
      }
      </div>
    )
  }

标签内的js内容用{}包裹 也可以在内部写入变量:

  const Component = ()=> {
    const content = (
      <div>
      {
        n%2- - -0
      ?
        <div>n是偶数</div>
      :
        <div>n是奇数</div>
      }
      </div>
    )
    return content
  }

或者直接往内部写入if...else...:

  const Component = ()=> {
    let inner
    if(n%2- - -0){
      inner = <div>n是偶数</div>
    }else{
      inner = <span>n是奇数</div>
    }
    const content = (
      <div>
        {inner}
      </div>
    )
    return content
  }

JSX为写法提供了很多自由性。


2.3.JSX中的遍历取值

  const Component = (props) => {
    return props.numbers.map((n,index)=>{
      return <div>下标{index}的值为{n}</div>
    })
  }

JSX能够使用js的map完成遍历,组件需要接受一个能够遍历的参数,返回其map用于遍历。 map接受一个函数,函数接收两个形参,第一个形参为值,第二个形参为下标。 获取其每次遍历的形参运用到标签中返回,实现了遍历。


2.4.JSX中的遍历存值

  const Component = (props) => {
    const array = []
    for(let i=0;i<props.numbers.length;i++){
      array.push(<div>下标{i}值为{props.numbers[i]}</div>)
    }
    return <div>{array}</div>
  }

通过for循环配合array.push,能实现将标签push入数组中。


3.1.元素与组件

再先前实现累加器中,在手动渲染数据时,将App写成了函数的形式。 App如果其写成变量形式,其值为React元素,其为元素。 如果写成函数形式,其返回值为React元素,其为组件。 组件在命名上通常以大写字母开头。

  const div = React.createElement('div',...)
  const Div = ()=> React.createElement('div',...)

3.2.两种组件

  <Welcome name="world">

函数组件与类组件都能够在引用的时候以如上方式引用。 函数组件会将Welcome标签内的属性与值,转化为键值对作为参数。props:{ name:'world' }

  function Welcome(props){
    return <h1>Hello, {props.name}</h1>
  }

类组件也会将Welcome标签内的属性与值,转化为键值对作为参数。当组件在取值时,需要继承React.Component对象,从其props属性中取值。

  class Welcome extends React.Component{
    render(){
      return <h1>hello, {this.props.name}</h1>
    }
  }

3.3.标签翻译

在JSX写入标签时,其会被翻译为js。 div标签会被翻译为React.createElement('div'),向createElement()中传入的为字符串,并创建标签。

welcome组件标签会被翻译为React.createElement(Welcome),向createElement()中传入的为Welcome函数。 React.createElement()根据接受的不同参数进行重载。 参数为字符串则创建对应标签。 参数为函数,则调用该函数获取其返回值。 参数为类,则会执行其构造器,并获取其组件对象,调用其render方法,再获取其返回值。

<div className="red", title="hi">Ogas</div>

//上述会被转译为:
React.createElement('div',{
  className: 'red',
  title: 'hi'
}, 'Ogas')
  const Welcome = ()=> {return <div>hj</div>}
  <Welcome>

  var Welcome = function Welcome(){
    return React.createElement('div',null,'hi')
  }
  React.createElement(Welcome,null)

3.4.数据父传子方式props

父组件向子组件传值时,需要使用props。 当父组件通过props传值给函数组件时,函数组件的第一个形参为props,props为一个对象,内部存储传入的值。

  const theFather = ()=>{
    const message = 'Are you winning son?'
    return <>
      <div>
        这里是父组件。
        <Son messageForSon={message}>
      </div>
    </>
  }
  const theSon = (props)=>{
    return <>
      <div>
        {props.messageForSon}
        Yes,my Dad.
      </div>
    </>
  }

当父组件通过props传值给类组件时,属性会被放入类组件所继承的React.compoent类的props属性中,通过this.props.获取。

  class Son extends React.component{
    render(){
      return <>
        <div>
          {this.props.messageForSon}
          Yes,my Dad.
        </div>
      </>
    }
  }

3.5.类组件内部数据state

类组件在构造函数中的state内声明数据。构造函数的super()的作用是用于初始化执行构造函数,必须写入在构造函数中。

  class Son extends React.component{
    constructor(){
      super()
      this.state = {
        n: 0
      }
    }
  }

类组件通过this.state.获取声明在state内的数据。

  class Son extends React.component{
    constructor(){
      super()
      this.state={
        n: 0
      }
    }
    
    render(){
      return <>
        <div>
          数值n:{this.state.n}
        </div>
      </>
    }
  }

类组件通过this.setState()函数修改state内的数据。 如果通过赋值方式修改state内的数据,不会被监听,而Vue能实现直接修改data的数据是因为其自动监听了开发者的赋值。

  class Son extends React.component{
    constructor(){
      super()
      this.state = {
        n: 0
      }
    }
    add(){
      this.setState({ n: this.state.n + 1})
    }
    render(){
      return <>
        <div>
          数值n:{this.state.n}
          <button onClick={()=> this.add()}> +1 </button>
        </div>
      </>
    }
  }

this.setState()接受一个新的对象用于代替state,因此有如下写法。 该写法虽然在编写上简便,但是会违反React数据不可变的原则,因此不会用这种写法。

  add(){
    this.state.n += 1
    this.setState(this.state)
  }

对于React开发的熟练者,不会向setState()中直接传入对象,而是将一个函数作为参数传入setState()中。该函数返回一个对象。

  add(){
    this.setState((state)=>{
      const n = state.n + 1
      return {n}
    })
  }

setState()是一个异步的函数,不会第一个时间变更值,而采取如上写法能够第一时间获取到变更的n的值。 如下写法方式输出的n为变更前的值,无法第一时间获取到变更后的值。

  add(){
    this.setState({ n: this.state.n + 1 })
    console.log(this.state.n)
  }

3.6.函数组件内部数据state

在声明定义数据的同时需要声明更改其值的函数。 React.useState()用于定义state数据,其接受的参数为数据值,并返回一个数组。 数组的第一项n用于读数据,为只读数据。 数组的第二项setN为函数,用于修改数据。 setN()其接受的参数为改变后的n值。 setN()与this.setState()都是异步改变数据,但setN()本质上不会真正改变n的值。

  const Son = () => {
    const [n,setN] = React.useState(0)
    return <>
      <div>
        数值n:{n}
        <button onClick={() => setN(n+1)}> +1 </button>
      </div>
    </>
  }

3.7.setState()特性

setState()中的对象仅有一部分属性修改,其他属性则会沿用旧值。

  this.state = {
    n: 0,
    m: 0
  }
  this.setState({n: this.state.n + 1})
  //m并不会因为传入的对象内没有属性m而为undefined,而是沿用m: 0。

3.8.函数组件使用useState()声明对象

React.useState()内部的参数传入一个对象,即声明一个对象

  const [state,setState] = React.useState({
    n: 0,
    m: 0
  })

当通过useState()声明对象时,setState()修改对象内其中一个属性时,其他属性不会沿用旧值,其不具备setState()的特性。

  <button onClick={() => setState({n:state.n + 1})}>

而解决此问题的方式时,在修改部分属性时先拷贝原有对象的属性。

  <button onClick={() => setState({...state,n:state.n+1 })}>

3.9.类组件的state内数据为对象的情况

类组件的state内的属性为对象时,修改user.name,user.age不会沿用旧值。

class Son extends React.Component {
  constructor(){
    super;
    this.state = {
      user:{
        name: 'Ogas',
        age: 18
      }
    }
    render(){
      return <>
        <div>姓名: {this.state.user.name}</div>
        <div>年龄: {this.state.user.age}</div>
        <button onClick={()=> this.setState({user:{
          name: 'Unclotho'
        }})}>更改姓名
        </button>
      </>
    }
  }
}

其解决方式依然是使用...运算符拷贝对象内的属性。

  <button onClick={
    ()=> this.setState({
      user:{
        ...this.state.user,
        name: 'jack'
      }
    })
  }>

4.1.类组件事件绑定方式演变过程 可不看

下面是事件绑定动作函数的四种写法,其中第一种写法是泛用的,后三种写法存在弊端。

addN(){
  this.setState({n: this.state.m + 1})
}

<button onClick={() => this.addN()}> n+1 </button>

<button onClick={this.addN}> n+1 </button>

<button onClick={this.addN.bind(this)}> n+1 </button>

this._addN = () => this.addN()
<button onClick={this._addN}> n+1 </button>

第二种写法中addN内的this是指向window,此时函数内部有this.setState()这样的语句,会引发this指向错误。 第三种写法中相当于对第二种写法的修正,重新修正了this指向,但太麻烦。 第四种写法相当于对第一种写法取别名,但依然是编写麻烦的。 下面是对于事件绑定的最优写法

  constructor(){
    this.addN = ()=> this.setState({n: this.state.n + 1})
  }
  render(){
    return <button onClick={this.addN}> n+1 </button>
  }

将事件绑定的动作函数写入到构造器中,此时在事件绑定时可以直接写入this.addN。 写入在构造器内的this.addN是一个箭头函数,其不会引发this指向的变更。 将动作函数写成箭头函数形式不会引发this变更,因此jsx给出了在类组件写入箭头函数的语法。

addN = ()=> this.setState({n: this.state.n + 1})
<button onClick={this.addN}> + 1 </button>

4.2.类组件事件绑定最优写法

  class Son extends React.component{
    constructor(){
      super()
      this.state = {
        n: 0
      }
    }
    add = () => {
      this.setState({ n: this.state.n + 1})
    }
    render(){
      return <>
        <div>
          数值n:{this.state.n}
          <button onClick={this.add}> +1 </button>
        </div>
      </>
    }
  }

将动作函数写为箭头函数,事件绑定动作函数时不需要加括号。


5.1.类组件详解


5.1.2.类组件创建方式

  class B extends React.Component{
    constructor(props){
      super(props)
    }

    render(){
      return <>
        <div> hi </div>
      </>
    }
  }
  export default B

5.1.3.类组件props

父组件可以将其变量与函数的引用传入子组件的props。 传入子组件的变量会被包装为一个对象,该对象为props。

  class Parent extends React.Component {
    constructor(props){
      super(props)
      this.state = {name:'frank'}
    }
    onClick = ()=>{}
    render(){
      <B 
        name={this.state.name}
        onClick={this.onClick}
      >hi</B>
    }
  }

子组件的constructor接受props。

  constructor(props){
    super(props)
  }

父组件只会将引用传给子组件,因此props是只读的,不允许更改,能够完成的更改不过是使引用不可用。 通过this.props.xxx = 'something'的方式去更改,不符合规范,组件的数据更改只允许由组件本身去修改,父组件传给子组件的数据应当由父组件更改。


5.1.3.componentWillReceiveProps钩子

componentWillReceiveProps函数(以下简称cwrp钩子函数)是一个写入在组件内的钩子函数。 其有两个形参nextProps与nextContext。 用于监视props的变化,当props变化后会触发该钩子函数。

  componentWillReceiveProps(nextProps, nextContext){
    console.log(this.props)
    //旧的props

    console.log(nextProps)
    //新的props
  }

在cwrp钩子函数内部,this.props是旧的props,因此props变动触发钩子在写值前。 第一形参nextProps是新的props。 cwrp钩子函数已经因为其存在问题被不推荐使用,因此更名为UNSAFE_componentWillReceiveProps。


5.2.函数组件详解


5.2.1.创建方式

使用箭头函数声明与函数组件声明的方式都能完成函数组件的声明。

  const Hello = (props) => {
    return <div>{props.message}</div>
  }

  function Hello(props){
    return <div>{props.message}</div>
  }

5.2.2.函数组件与类组件的差异

函数组件没有state。 函数组件没有生命周期。 函数组件的过简会使其可控性比类组件要弱。 没有生命周期意味着没有钩子函数用于调试。 同时也说明函数组件更加简便。 React v16.8推出了Hooks API用于加强函数组件。 例如useState、useEffect。


6.1.React生命周期

在推出Hooks前仅有类组件有生命周期,因此以类组件为例。

  let div = document.createElement('div')
  div.textContent = 'hi'
  document.body.appendChild(div)
  div.textContent = 'hi2'
  div.remove()

上述是一段DOM操作,可以反应出生命周期的过程。 声明变量创建节点。 向节点内写入内容,引入数据。 向节点内写入节点,挂载节点。 更改节点内容,修改数据。 删除节点。 React的生命周期符合这样一个大致流程。

  create / construct 
  state init
  mount
  update
  unmount

创建构造组件 初始化state数据 挂载视图节点 更新state数据渲染视图 组件卸载


6.2.React生命周期钩子函数

组件在创建后,调用下面函数。

  constructor()

组件在需要更新时,调用下面函数。 函数接收两个形参:nextProps为更新后的props,nextState为更新后的state。 当函数返回值为false阻止此次更新,返回值为true时允许此次更新,且一定要返回bool。

  shouldComponentUpdate(nextProps, nextState)

用于完成组件的渲染。 当视图的html只有一个根元素,使用()包裹。 当视图的html有多个根元素,不能使用()包裹,需要使用<React.Fragment>包裹,可以简写为<></>

  render()

组件在完成节点挂载后,调用下面函数。 该函数一般用于获取节点挂载之后的信息或进行操作。 例如在节点挂载后对节点进行DOM操作。 请求的发起一般写入在该函数中。

  componentDidMount()

组件在完成数据更新后,调用下面函数。 请求的发起一般写入在该函数中。 在该函数内写入setState会引发循环。 当函数返回值为false时,不会执行函数内容。 分别拥有两个形参,第一形参prevProps为旧的props,第二形参prevState为旧的state。

  componentDidUpdate()

组件在确定卸载,执行卸载前,调用下面函数。 一般在该函数中进行先前建立的监听的取消,计时器的消除,请求的取消。

  componentWillUnmount()

7.useState原理


7.1.setN()如何渲染视图

  const [n,setN] = React.useState(0)

函数组件中是使用useState去声明内部数据,通过useState()得到数据的读写。 React页面更新渲染是通过比对虚拟DOM,决定是否更新。 而setN()能够完成在视图上的更新渲染。 因此setN()执行render()。 而setN()的渲染是直接调用函数组件进行的。 例如在App组件中调用setN(),setN()则会通过调用App()的方式渲染。


7.2.setN()如何修改数据

从感观上看setN()对n进行了修改,但实际过程并非如此。 setN()对n的修改方式是,将自身的参数传给useState()。 useState()接受setN()的参数作为自己的参数后,执行而完成了对n的修改。 setN()会帮助useState()接受参数,而存放这些参数的变量为state。 即setN()异步调用render(),而调用render()会重新执行useState()。 在重新执行useState()时,其接受的参数来自setN()所接受的参数。 这个参数就是函数组件的state。


7.3.state的特点

上述提及修改数据会重新调用函数组件进行渲染。 这会引发一个问题,如果state作为函数内的数据,重新调用函数组件,是否会初始化state。 显然是不会的,如果每次调用都初始化state就没有办法完成对数据的修改。 因此state相当于是一个写入在函数组件外的变量。


7.4.useState与if的问题

组件用多个变量需要多次调用useState()时。 多个变量的n,setN会被存储入state中,意味着state是一个数组。 由此useState的调用顺序必须需要与前一次渲染一致。

  const [n,setN] = React.useState(0)
  const [m,setM] = React.useState(1)
  const [x,setX] = React.useState(2)

当上一次渲染的useState执行顺序为n,m,x时。 其在数据更新后的执行useState的顺序也必须是n,m,x保持一致。 因此React.useState()是不能够写入在条件中,会引发执行顺序不一致的问题。


8.useRef()


8.1.useRef()变量声明及读写方式

如下声明一个ref变量,变量名为refN,其值为useRef()的参数0。 声明方式与useState()相似,但不给出写值的setN函数。

  const refN = React.useRef(0)

当ref变量需要写值时,通过给变量的.current属性赋值的方式实现。

  refN.current += 1

useState()中提供的setN能够完成视图渲染。 但useRef提供的.current不会渲染视图


8.2.ref与state两者差异

useState在做变量修改时,实际上是用新的变量获取新的值,更改引用为新的变量。 因此state的变量修改并不是对原有变量进行修改。 而ref是对原有的变量进行更改。


8.3.通过.current写值并触发渲染的方式

在通过.current改值时,同时触发API去渲染,但React没有直接提供这样的API。 state的变量是通过setN来渲染的,ref变量可以利用setN完成更新。

  const nRef = React.useRef(0)

  const [n,setN] = React.useState(null)

  onClick={
    ()=>{
      nRef.current += 1
      setN(nRef.current)
    }
  }

向useState()内传入null作为参数,这样state变量n是不存在的。 我们可以将setN取出作为更新nRef的方式。 n是不需要的,因此在取出setN时采用另一种写法。

  const nRef = React.useRef(0)

  const update = React.useState(null)[1]

  onClick = {
    ()=>{
      nRef.current += 1
      update(nRef.current)
    }
  }

这样获得了一个名为update的函数用于更新ref变量。 当哪个ref变量通过.current写值后,调用update函数以其.current为参数。 就能够实现ref变量的渲染。


9.useContext

useContext提供了<.Provider>标签,用于标记一块区域提供变量。 标签名可以自定义。 该标签的属性value用于接受state变量,及其变量的set函数。 标签内部能够使用value提供的变量内容,并且内部引用的子组件也能够使用。

  function App(){
    const {theme, settheme } = React.useState('red')
    return <>
      <themeContext.Provider value={ {theme, settheme} }>
        <ChildA />
        <ChildB />
      </themeContext>
    </>
  }
  

被调用的子组件获取父组件<.provider>提供的变量需要使用useContext()结合析构语法。 useContext()接受的参数为.Provider标签名。

  fucntion ChildA() {
    const { setTheme } = React.useContext('themeContext')
  }

10.函数组件没有生命周期钩子的问题

类组件提供了多样的生命周期钩子用于开发的调试。 而函数组件并不存在这样的生命周期钩子。 自React v16.8.0后,推出了useEffect用于模拟生命周期钩子。

 craete       constructor(props)
 state init   shouldComponentUpdate(nextProps, nextState) render()
 mount        componentDidMount()
 update       componentDidUpdate()
 unmount      componentWillUnmount()

上述列出了类组件在其生命周期阶段对应的常用钩子函数。 可以用useEffect()模拟以下三个钩子函数。 componentDidMount()、componentDidUpdate()、componentWillUnmount()


10.1.componentDidMount()

  useEffect(()=>{
    console.log('挂载了组件')
  },[])

useEffect()内接受第一个形参为一个函数,在组件更新时则会调用。 接受的第二个形参为[]时,则useEffect()只会在组件初次挂载时调用。 由此通过uesEffect()的第二形参为[],在函数组件中模拟了componentDidMount()。


10.2.componentDidUpdate()

const [n,setN] = useState(0)

useEffect(()=>{
  console.log('n更新了')
},[n])

useEffect(()=>{
  console.log('n或者m更新了')
},[n,m])

useEffect()接受的第二形参为包含state变量的数组时。 当数组内的任意state变量更新,则会调用第一形参。 当第二形参不传入时,意味着组件更新时则会调用第一形参。 即任意变量更新都会调用第一形参。 使用该方式模拟componentDidUpdate(),会在变量初始化的时候触发一次。 如果需要变量初始化时不触发,则需要进行计数。

  const [n,setN] = useState(0)

  const [nUpdateCount, setNUpdateCount] = useState(0)

  useEffect(()=>{
    setNUpdateCount(nUpdateCount => nUpdateCount + 1)
  },[n])

  useEffect(()=>{
    if(nUpdateCoutn > 1){
      console.log('n更新了')
    }
  },[n])

通过uUpdateCount变量为n的更新计数,当第一次初始化时不触发模拟的钩子函数。


10.3.componentWillUnmount()

  useEffect(()=>{
    return ()=>{
      console.log('组件销毁了')
    }
  })

当useEffect()接受的第一形参的返回值为一个函数时。 组件销毁时会调用该函数。


10.4.模拟componentDidUpdate()的优化封装

  const [n,setN] = useState(0)

  const [nUpdateCount, setNUpdateCount] = useState(0)

  useEffect(()=>{
    setNUpdateCount(nUpdateCount => nUpdateCount + 1)
  },[n])

  useEffect(()=>{
    if(nUpdateCoutn > 1){
      console.log('n更新了')
    }
  },[n])

如果每个变量都需要准确的模拟该钩子,则需要为每个变量设置计数。 可以将其优化封装为一个所有变量通用的更新计数器。

  const useX = (n) => {
    const [nUpdateCount, setNUpdateCount] = useState(0)
    useEffect(()=>{
      setNUpdateCount( nUpdateCount => nUpdateCount+1 )
    }, [n])
    return {
      nUpdateCount, setNupdateCount
    }
  }
  const { nUpdateCount, setNupdateCount } = useX(n)

  useEffect(() => {
    if(nUpdateCount > 1){
      console.log('n更新了')
    }
  },[nUpdateCount])

我们将为n设置计数器的部分用useX()封装。 因为useX()内使用到了useEffect()这样的hook函数,所以其函数名需要以use开头。 因为外部需要获取计数,需要将nUpdateCount返回,外部用析构语法取出。 而useX()的参数为需要被计数的变量。


10.5.进一步优化封装模拟componentDidUpdate()

useX对需要监视的变量n进行监听,抛出计数。 而useEffect由对useX抛出的计数nUpdateCount进监听,完成动作函数。 可以将useEffect的动作函数作为参数传入useX,而免除对于计数的监听与计数的传递。 而外部只需要传入被监听的变量及动作函数即可。 将useX更名为useUpdate,由此封装了一个通用的模拟componentDidUpdate()接口。

  const useUpdate = (n, fn) => {
    const [nUpdateCount, setNUpdateCount] = useState(0)
    
    useEffect(()=>{
      setNUpdateCount( nUpdateCount => nUpdateCount+1 )
    }, [n])

    useEffect(() => {
    if(nUpdateCount > 1){
      fn()
    }
  },[nUpdateCount])

    return {
      nUpdateCount, setNupdateCount
    }
  }

  const [n,setN] = useState(0)

  const nUpdateFunction = ()=> {
    console.log('n更新了')
  }

  useUpdate(n, nUpdateFunction)

在使用hook提供的函数时,例如useEffect()。 其第一形参为动作函数,第二形参为存放在数组的变量。 因此如果需要符合这一规范,需要对useUpdate进行修改。

  const useUpdate = (fn, array) => {
    const [nUpdateCount, setNUpdateCount] = useState(0)
    
    useEffect(()=>{
      setNUpdateCount( nUpdateCount => nUpdateCount+1 )
    }, [...array])

    useEffect(() => {
    if(nUpdateCount > 1){
      fn()
    }
  },[nUpdateCount])

    return {
      nUpdateCount, setNupdateCount
    }
  }

  const [n,setN] = useState(0)

  const nUpdateFunction = ()=> {
    console.log('n更新了')
  }

  useUpdate(nUpdateFunction, [n])

由此封装了一个符合hook函数传参规律,准确模拟componentDidUpdate()的接口。 useUpdate()会报出一个警告,来源是其中array参数无法确保其为数组。 常规的处理方式是通过/eslint/去隐藏这个警告。 或者为了确保稳定性将第二形参的类型更改为单个参数,而不是参数数组。


11.Hooks列举

  状态:useState
  副作用:useEffect useLayoutEffect
  上下文:useContext
  Redux:useReducer
  记忆:useMemo useCallback
  引用:useRef useImperativeHandle
  自定义:Hook useDebugValue 

12.useState


12.1.state对象变量无法局部更新

  const [n,setN] = React.useState(0)
  const [user,setUser] = React.useState({name: 'Ogas'})

state可以是任何数据类型,但state变量为对象时存在一些特点。 当state变量为对象类型时,对象无法局部更新。

  const [user,setUser] = React.useState({
    name: 'Ogas',
    age: 18
  })

  setUser({
    name: 'Unclotho'
  })

目前知道上述写法setUser()传入的对象属性不全,useState不会自动补全属性。

  setUser({
    ...user
    name:'Unclotho'
  })

采用...语法,先拷贝原有的属性,在对拷贝的属性覆盖,实现了局部更新。


12.2.state对象变量引用变更

  setState(obj)

通过setState去更新对象时,对象的引用也会变更。 即用新的对象取代了旧的对象。 如果state对象变量的引用没有变更,则React认为其数据没有变化。


12.3.useState()传入函数

  const [user,setUser] = useState(
    () => {
      return {
        name: 'Ogas',
        age: 18
      }
    }
  )

当useState声明state变量,这个变量初始化过程复杂时。 可以传入一个函数,将该函数的返回值作为参数。

  const [age,setAge] = useState({age: 9+9})
  const [age.setAge] = useState(()=> ({age: 9+9}))

上述情况两种写法将会有细微的差别。 直接传入对象的写法,代码每次运行到state时,都会计算9+9、 而传入函数的写法,代码只会在第一次计算9+9,并将结果记录,下次沿用上次结果。


12.4.setState()传入函数

  const onClick = ()=>{
    setN(n + 1)
    setN(n + 2)
  }

上述模拟在一个动作函数中,需要连续使用两次setState作两次变量更新的情况。 目前我们知道,setN()对变量的更新并非是即刻生效的。 因此连续多次的setState()只有最后一次是生效的。

  const onClick = ()=>{
    setN(n => n+1)
    setN(n => n+2)
  }

当setN传入函数时,该函数会接受一个形参,即为n。 此时setN()传入的函数会被记录,在正式更新state变量时执行。 因此连续的setState(),传入的参数为函数时能够生效。 理论上setState()传入函数的更新state方式才是更泛用的写法。


13.useReducer

useReducer可以用Flux/Redux思想进行变量操作。 该思想将变量的操作分为了四步:

  • 创建初始值 - initial
  • 创建所有操作 - reducer
  • 获得读写API - useReducer
  • 调用读写操作类型 - state/dispatch
  const initial = {
    n: 0
  }

在initial中完成对Reducer变量的声明,并赋初始值。

  const reducer = (state, action) => {
    if(action.type === 'add'){
      return {n: state.n + 1}
    }else if(action.type === 'multi') {
      return {n: state.n*2}
    }else {
      throw new Error('未知操作')
    }
  }

在reducer中完成对操作的定义。 形参state为initial声明的变量。 形参action为操作类型。 通过对action操作类型进行条件判断重载reducer的返回值。

  const [state, dispatch] = useReducer(reducer, initial)

将reducer与initial传给useReducer。 通过析构语法得到state与dispatch。

  const {n} = state

从state中可以读reducer变量。

  const onClick = () =>{
    dispatch({type:'add'})
  }

在dispatch中传入action,完成对变量的操作。 Redux的变量思想将变量与操作分离, 并将操作集合聚集。 表单中的变量操作是固定或有限的, 并且一份表单数据可以对应一份reducer, 因此Redux变量思想很符合表单的特性。