Vueer 学 React 汇总27个知识点

2,156 阅读18分钟

笔者在17年接触前端以来,一直都是用Vue。可以说歪打正着吧,加入过的每个团队都是Vue,完美的避开了React。但是大环境下,React在大厂的地位不可动摇,只有一门Vue是不够的。如何高效学习React并掌握面试要领,这里我将把自己的学习历程做成笔记,分享给大🔥

目录大全

直接看看本文涉及的React知识点checklist👇

一、基本使用二、高级特性三、浅析原理四、Hooks
1.jsx基本使用1.非受控组件1.vNode+diff1.useState
2.条件渲染、列表渲染2.React Portals2.jsx本质2.useEffect
3.事件event3.React Context3.React合成事件3.useRef、useContext
4.受控组件4.异步组件4.batchUpdate机制4.useReducer
5.父子组件通信5.SCU5.组件更新和渲染(fiber)5.useMemo
6.setState6.高阶组件HOC6.性能优化6.useCallback
7.生命周期7.自定义Hook
8.Hooks注意事项

先从class组件入手,再了解原理,最后再到React Hooks。其中,小弟还会根据Vue的相关知识点跟React做对比,以加深对React的理解和掌握。只看不做可能印象不够深刻,大🔥可以跟着文章走自己敲一敲~ 👉另外,想学习Vue源码的朋友,点击传送门进入小弟的专栏看看哈~


一、基本使用

1. jsx基本使用

案例代码:

class MyComponent extends React.Component{
  constructor(props) {
    super(props)
    this.state = {
      text: '模板插值',
      imageUrl: 'xxx.png'
    }
  }

  render() {
    const myStyle = { color: 'blue' }
    return (
      <div>
        <!-- 模板插值 -->
        <p>{this.state.text}</p>
        <!-- 属性 -->
        <img src={this.state.imageUrl} alt=""/>
        <!-- 类名 -->
        <p className="text">{this.state.text}</p>
        <!-- 样式 -->
        <p style={myStyle}>{this.state.text}</p>
        <!-- 原生html -->
        <p dangerouslySetInnerHTML={{ __html: '<i>我是斜体字</i>' }} />
      </div>
    );
  }
}
  1. 插值{}。对应Vue的{{}}语法
    • 模板中使用。<p>{this.state.text}</p>
    • 属性中使用。<img src={this.state.imageUrl} alt=""/>
  2. className。Vue中直接写class,动态类名使用v-bind
    • class写成className(由于class是js的保留字)。<p className="text">{this.state.text}</p>
  3. style。对应Vue的v-bind
    • 使用对象。<p style={myStyle}>{this.state.text}</p>
    • 直接写内联。<p style={{ color: 'blue' }}>{this.state.text}</p>
  4. 原生html。对应Vue的v-html
    • <p dangerouslySetInnerHTML={{ __html: '<i>123</i>' }} />

2. 条件渲染、列表渲染

  1. 条件渲染。对应Vue的v-if
    // React
    render () {
      return this.state.flag ? <p>is flag</p> : <p>not flag</p>
    }
    // Vue
    <p v-if="flag">is flag</p>
    <p v-else>not flag</p>
    
  2. 列表渲染(map函数key)。对应Vue的v-for
    // React
    state = {
      list: ['list1', 'list2']
    }
    render () {
      // 在{函数体}中,通过map返回一个li的list
      return (
        <ul>
          { this.state.list.map(item => <li key={item}>{item}</li>) }
        </ul>
      )
    }
    // Vue
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
    

到这里可以看出其实二者真的很相似,只是:

  • React提供了简单的jsx语法,具体实现需要js的能力。内部没有再提供指令,理解为简洁上手吧。
  • Vue提供了很多指令,如v-if、v-for这种。用之前需要先了解指令的相关用法,日常使用会有一定便捷~

3. 事件——event

事件写法: onXxxx,如onClick。Vue中是@clickv-on:click

  1. 事件调用方法时的this

    • bind(this)
    • 箭头函数
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          text: '模板插值'
        }
        // bind(this)
        this.handleClick = this.handleClick.bind(this)
      }
      handleClick = function () {
        this.setState({
          text: '修改text'
        })
      }
      // 箭头函数
      handleArrowClick = () => {
        this.setState({
          text: '修改text from handleStaticClick'
        })
      }
      render() {
        return (
          <div>
            <p>{this.state.text}</p>
            <!-- bind(this)也可以在这里写,但是写在constructor性能更佳 -->
            <button onClick={this.handleClick}>change</button>
            <!-- 这里用的静态方法。静态方法this永远指向该实例 -->
            <button onClick={this.handleArrowClick}>change</button>
          </div>
        )
      }
    }
    
    • 浅析理解:React的事件不是真实绑定在dom上。并且事件调用并不是发生在组件层,而是发生在最外层的事件监听器(事件委托)。而js的this关键字是运行时确定的,在函数被调用时才绑定执行环境(谁调用就指向谁)。所以事件中需要绑定this;或者使用箭头函数写法(定义时就绑定this指向当前组件)
  2. event参数(Vue的event是原生事件)

    • event并不是原生事件,而是React封装的组合事件。如下代码和输出:
    handleClick = function (event) {
      // react的合成事件
      console.log('reactEvent', event)
      // 原生事件
      console.log('nativeEvent', event.nativeEvent)
      // 指向当前元素
      console.log('nativeEvent', event.nativeEvent.target)
      // 指向document
      console.log('nativeEvent', event.nativeEvent.currentTarget)
    }
    

    image.png

    • 模拟出了DOM的所有能力。如event.preventDefault()event.stopPropagation()
    • 所有事件都绑定到document上(React17之前)
    • 所有事件都绑定到root组件(React17)如下图:
  3. 传递自定义参数

    • 正常使用的方法传递即可,需要注意的是event参数会追加到最后一位
    handleClick = function (param, event) {
      console.log(param, event)
    }
    

4. 受控组件

  • 受控组件简单理解:表单的值,受到state的控制
  • state驱动视图渲染(可对应高级特性中的非受控组件进行理解)。如表单中实现双向绑定。对应Vue的v-model
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          inputVal: 'input'
        }
      }
      render() {
        return (
          <div>
            <p>{this.state.inputVal}</p>
            <input type="text" value={this.state.inputVal} onInput={this.handleInput} />
          </div>
        )
      }
      // 需要手动实现数据变更(比v-model多了一步)
      handleInput = e => {
        this.setState({
          inputVal: e.target.value
        })
      }
    }
    

5. 父子组件通信

  1. 传递数据和接收数据。对应Vue的是v-bind和接收props
  • 传递数据:<ChildComponent count={this.state.count} />
  • 接收数据:
    class ChildComponent extends React.Component {
      constructor(props) {
        super(props)
      }
      render() {
        return <div>
          {this.props.count}
        </div>
      }
    }
    
  1. 传递函数。对应Vue的@xxx=xxxthis.$emit('xxx')
    // 父组件
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          count: 99
        }
      }
      render() {
        return (
          <div>
            <ChildComponent count={this.state.count} changeCount={this.changeCount} />
          </div>
        )
      }
      // 接收子组件传递的count来修改自身的count
      changeCount = count => {
        this.setState({
          count
        })
      }
    }
    // 子组件
    class ChildComponent extends React.Component {
      constructor(props) {
        super(props)
      }
      render() {
        return <div>
          {this.props.count}
          <button onClick={this.addCount}>addCount</button>
        </div>
      }
      addCount = () => {
        // 获取父组件传递的props
        const { changeCount, count } = this.props
        // 调用父组件的changeCount方法
        changeCount(count + 1)
      }
    

6. setState

  1. 不可变值
  • class组件使用setState修改值,要注意不可变值的概念。不可变值也是从Vue转React时需要记住的一个概念~
    // 修改基础类型数据
    this.setState({
      // 保证原有值不变,不能直接操作state的值(不能写成this.state.count++)
      // this.state.count + 1 是一个新的结果 ✅
      // this.state.count++ 则是直接修改了this.state中的值 ❌
      count: this.state.count + 1
    })
    // 修改引用类型——数组
    this.setState({
      // 避免使用push、pop等直接修改原数组的方法
      list: [...this.state.list, 3], // 新增一个值
      list2: this.state.list.slice(1) // 截取
    })
    // 修改引用类型——对象
    this.setState({
      // 避免直接在原对象上做修改
      obj: {...this.state.obj, name: 'jingboran'}
    })
    
  • 实际开发中可能会遇到深层对象的情况需要setState,可以使用immerimmutable这种不可变值的库。这里推荐一篇相关的文章,讲得比较详细。
  1. setState是同步执行还是异步执行
  • 分两种情况(具体参考如代码演示):
    1. 正常用法时是异步执行;
    2. 在setTimeout和自定义DOM事件的cb中是同步执行
    // 直接用setState是异步的,这里console中拿不到修改后的值
    this.setState({   
      count: this.state.count + 1
    })
    // 同步console拿不到修改后的值(如果需要拿修改后的值,可以在setState中添加回调)
    console.log(this.state.count) 
    
    // 在setTimeout中用setState是同步的
    setTimeout(() => {
      this.setState({   
        count: this.state.count + 1
      })
      // 这里能拿到最新的结果
      console.log(this.state.count)
    })
    
    // 在自定义的DOM事件用setState也是同步
    componentDidMount() {
      // 绑定body的点击事件
      document.body.addEventListener('click', () => {
        this.setState({
          count: this.state.count + 1
        })
        // 能拿到最新结果
        console.log(this.state.count)
      })
    }
    
  1. setState是否会被合并执行
  • 两种情况:
    1. 合并执行——传入对象(注意:如果在setTimeout的回调中是同步执行,也就不存在合并执行的说法了)
      // 常规用法时,setState异步执行,结果只加1(被合并执行)
      this.setState({
        count: this.state.count + 1
      })
      this.setState({
        count: this.state.count + 1
      })   
      
    2. 不合并执行——传入函数
      // 结果加2,分别都执行
      this.setState((prevState) => {
        return {
          count: prevState.count + 1
        }
      })
      this.setState((prevState) => {
        return {
          count: prevState.count + 1
        }
      })
      

7. 生命周期(对比Vue)

  • React
    • constructor类似Vue的init阶段(initLifeCycleinitState......)
    • render类似Vue的$mount阶段(执行render -> 得到VNode -> patch到DOM上)
    • componentDidMount等价Vue的mounted
    • 在组件更新阶段,render之前还有一个shouldComponentUpdate的阶段(能控制本次组件是否需要render),可以在该阶段做一些性能优化 image.png
  • Vue(直接贴上官网的生命周期图回顾一下~) image.png

二、高级特性

1. 非受控组件

  • state不控制视图渲染。理解:数据不受组件state的控制,直接跟DOM相关
  • 使用场景:必须要操作DOM,setState不能满足。比如input的type=file时的一些交互
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          inputText: ''
        }
        // 获取DOM。对应Vue的ref
        this.textInput = React.createRef()
      }
    
      render() {
        return (
          <div>           
            <!-- 对应Vue中的ref,但是Vue传入的是个字符串 -->
            <input type="text" ref={this.textInput} defaultValue={this.state.inputText} />
          </div>
        )
      }
      handleClick = function () {
        // 直接获取DOM的值
        console.log(this.textInput.current.value)
      }
    }
    

2. React Portals

  • 让组件渲染到父组件以外。对应Vue3的teleport
    • 场景:比如日常开发中,父组件设置overflow:hidden的布局。导致组件内的一些弹窗、下拉菜单(绝对定位)等被切割的问题,这时候需要逃离父组件的嵌套
    return ReactDOM.createPortal(
      <div>
        <p>子组件挂在到body层,逃离父组件</p>
      </div>,
      document.body
    )
    

3. React Context

  • 两个核心:1.数据创建:provider;2.数据消费:consumer
  • 具有数据透传的能力,子孙组件能拿到最外层组件的数据。有点类似Vue的v-bind="$attrs"
    // MyContext文件
    import { createContext } from 'react'
    export default createContext()
    
    // 外层组件Provider
    import MyContext from './MyContext'
    
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          val: 'providerVal'    
        }
      }
      render() {
        return (
          // 设置value值
          <MyContext.Provider value={this.state.val}>
            <div>
              <!-- Second子组件 -->
              <Second />
            </div>
          </MyContext.Provider>
        )
      }
    }
    
    // 子组件Consumer——Class Component
    import MyContext from './MyContext'
    import Third from './Third'
    
    export default class Second extends React.Component {
      // 通过静态属性contextType接收MyContext
      static contextType = MyContext
      // 通过this.context访问
      render() {
        return (
          <div>
            { this.context }
            <!-- Second组件的子组件 -->
            <Third/>
          </div>
        )
      }
    }
    
    // 孙组件Consumer——function Component
    import MyContext from './MyContext'
    import {useContext} from 'react'
    
    export default function Third () {
      // 使用useContext获取
      const context = useContext(MyContext)
      return (
        <div>{ context }</div>
      )
    } 
    

4. 异步组件

  • 两个核心lazy+Suspense。跟Vue的异步组件很相似。可以一起学习理解

    // Vue中的异步组件——工厂函数
    Vue.component('async-component', function (resolve) {
        require(['./async-component'], resolve) 
    })
    // Vue中的异步组件——Promise
    Vue.component(
      'async-component',
      () => import('./async-component') 
    )
    
    // React的异步组件
    // lazy接收一个Promise,并返回一个新的React组件
    const Second = React.lazy(() => import('./Second'))
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          val: 'providerVal'
        }
      }
      render() {
        return (
          <MyContext.Provider value={this.state.val}>
            <div>
              <!-- fallback为必传属性(未加载完时显示),接收一个组件 -->
              <React.Suspense fallback={<div>loading...</div>}>
                <Second />
              </React.Suspense>
            </div>
          </MyContext.Provider>
        )
      }
    }
    

5. shouldComponentUpdate

  1. React的性能优化的重点SCU是组件update前的一个生命周期,可控制组件更新
  2. 默认返回true(组件每次都会执行render)。可通过自定义逻辑(如比对数据变化判断是否需要更新),返回false对组件进行性能优化
  3. 基本用法:
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)   
      }
      shouldComponentUpdate (nextProps, nextState, nextContext) {
        console.log(nextState.count, this.state.count)
        if (nextState.count !== this.state.count) {
          return true // 允许渲染,会往下执行render
        }
        return false // 不渲染,不执行render
      }
      render() {
        return ...
      }
    }
    
  4. 为什么需要SCU这个生命周期优化
  • React的组件更新逻辑:父组件更新,子组件无条件更新。对比Vue,这点有很大的区别。熟悉Vue响应式原理的都知道,Vue的数据是收集对应组件的渲染Watcher的,也就是形成了一个对应关系。只有当前组件依赖的值发生改变才会触发组件更新。
  • 如下代码。父组件点击按钮会修改count值触发父组件更新。如果不设置SCU,则父组件更新会导致子组件ShowText更新。观察可知,子组件接收的props并未被修改,其实是不需要重新render的。所以SCU的优化场景就出现了~
    // 父组件
    class MyComponent extends React.Component{
      constructor(props) {
        super(props)
        this.state = {
          count: 0,
          text: 'children Component'
        }
      }
      render() {
        return (
          <div>
              <p>{this.state.count}</p>
              <button onClick={this.handleClick}>add count</button>
              <!-- 注意子组件接收的text并未发生改变 -->
              <ShowCount count={this.state.text} />
            </div>
        )
      }
      handleClick = function () {
        // 父组件点击修改Count
        this.setState({
          count: this.state.count + 1
        })
      }
    }
    
    // 子组件
    class ShowText extends React.Component{
      constructor(props) {
        super(props)
      }
      componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('is update')
      }
      shouldComponentUpdate (nextProps, nextState, nextContext) {
        // 判断props的text是否改变
        if (nextProps.text !== this.props.text) {
          return true // 允许渲染,会往下执行render
        }
        return false // 不渲染,不执行render
      }
      render() {
        return (
          <div>
            <p>{this.props.text}</p>
          </div>
        );
      }
    }
    
  • 思考:为什么React不在内部实现SCU,而是提供了个生命周期出来给开发者自行实现?
    1. 不是所有场景都有必要做性能优化。出现卡顿的时候再进行优化往往是比较合理的开发模式。如果所有情况都直接在SCU做深度比对state,这样本来就会造成性能损耗,往往是不必要的~
    2. SCU一定要配合不可变值来用。如果内部实现SCU深度比对state再判断是否更新组件,且不遵循不可变值写法可能会造成组件更新失败问题。因为不排除有不遵循不可变值写法的开发者。比如this.state.list.push(3),再写this.setState({list: this.state.list})这种情况
  1. PureComponent——实现了浅比较的SCU
    • 其实就是在SCU中对state和props做了一层浅比较
    • 用上述案例来改造,能达到一样的效果。父组件改变count,子组件不会执行更新
    // 用法:extends React.PureComponent
    export default class ShowText extends React.PureComponent{
      constructor(props) {
        super(props)
      }
      componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('is update')
      }
    
      render() {
        return (
          <div>
            <p>{this.props.text}</p>
          </div>
        );
      }
    }
    

6. 高阶组件HOC

  • 组件的公共逻辑抽离。其实就是个高阶函数的概念。如果觉得不好理解,可以看看小弟另一篇文章,戳这里,有高阶函数的案例介绍
    1. 接收一个组件,返回一个新的组件。比如每个组件都需要拥有计数器的能力,则可这么写:
    // 高阶组件函数
    function HOCFactory (Component) {
      // 接收一个组件参数:Component
      class CountComponent extends React.Component {
        constructor(props) {
          super(props)
          // 公用逻辑计数器的count
          this.state = {
            count: 0
          }
        }
        render() {
          return (
            <div>
              <!-- 透传所有的props,并把count传递到组件以复用 -->
              <Component {...this.props} count={this.state.count}/>
              <button onClick={this.handleAdd}>add Count</button>
            </div>
          )
        }
        handleAdd = () => {
          this.setState({
            count: this.state.count + 1
          })
        }
      }
      // 返回一个新的组件
      return CountComponent
    }
    
    // 普通组件
    class NormalComponent extends React.PureComponent{
      constructor(props) {
        super(props)
      }
      render() {
        return (
          <div>
            <!-- 接收高阶组件传递的count并显示 -->
            <p>公用计数器:{this.props.count}</p>
            <p>组件自身的逻辑...</p>
          </div>
        );
      }
    }
    export default HOCFactory(NormalComponent)
    

    1. 使用方式跟普通组件一样,但是公用逻辑已经抽离
     import HOCComponent from './HOCComponent'
    
     class MyComponent extends React.Component{
       constructor(props) {
         super(props)       
       }
    
       render() {
         return (
           <div>
             <HOCComponent />
           </div>
         )
       }
     }
    
  • 效果如图所示
    计数器HOC事例动图.gif

  • Vue的大概HOC也来一个~其实原理都是一样的~
const HOCFactory = Component => {
  const instance = new Vue({
    // 实现公共组件逻辑
    created () {},
    mounted () {},
    data () {},
    // render中传入参数的组件Component
    render (h) => h(Component, {
      props: {
        // 传入props
      }
    })
  })
  // 返回这个组件实例
  return instance
}

三、浅析原理

1. vNode + diff算法

  • Vue2、Vue3、React具体vNode和diff实现不同,大致理解其中一个的实现即可,原理都相似
    1. 理解组件化流程:render -> vNode -> patch -> DOM
    2. 理解vNode是如何提升性能的。单纯vNode并不能提升性能,而是需要配合diff
    3. diff算法的核心三步走(vue2为例):
      • 首先,同层vNode对比(tagkeydata)。不同则整个vNode替换
      • 其次,旧vNode有子节点、新vNode无子节点。删除子节点
      • 其次,新vNode有子节点、旧vNode无子节点。新增子节点
      • 最后,都有子节点,进行子节点比对(首位指针法

2. jsx本质

  • 理解render函数的,可以自己手写Render函数。可以了解下编译原理(React的render函数可以再Babel官网编译查看变异结果)
  • 对应Vue中的template的本质(.vue文件最终也是编译成render函数)
  • render函数基本写法
    1. 第一个参数:
      • 字符串(如html的保留标签)
      • 组件对象(React规定组件首字母大写)
    2. 第二个参数:data属性(如原生的id、class;组件之间的props)
    3. 第三个参数:children。可以是个数组,也可以是个普通string(如果children是个标签或者组件,则嵌套的是另一个createElement函数)
    // Vue简易render函数
    createElement('tagName', { 属性... }, [children])
    

3. React合成事件SyntheticEvent

  • 事件怎么触发handle(以onClick为例),如图

react-event.png

  • 思考:为什么需要合成事件?
    1. 更好的兼容跨平台、跨端。将事件机制抽离,减少对DOM的依赖,以快速兼容跨平台
    2. 事件委托的优势。减少内存消耗,避免频繁的绑定、解绑事件(列表click场景)
    3. 方便事件统一管理。如事务机制
  • React17为什么把事件绑定从document移到root组件
    1. 微前端场景。有利于多个React版本并存,不会造成document中事件紊乱 image.png

4. setState和batchUpdate

  1. setState流程
    • dirtyComponent(state已经被更新的component) setState.png
  2. batchUpdate机制
    • isBatchingUpdates状态。为true时异步执行、为false时同步执行。函数开始执行时为true,函数执行结束时置为false
    /* 异步执行 */
    xxxfn = () => { 
      // 开始:处于batch update
      // isBatchingUpdates = true
      this.setState({ ... })
      // isBatchingUpdates = false
      // 函数结束
    }
    
    /* 同步执行 */
    xxxfn = () => {
      // 开始:处于batch update
      // isBatchingUpdates = true
      setTimeout(() => {
        // 异步执行,所以此时isBatchingUpdates为false
        this.setState({ ... })
      })
      // isBatchingUpdates = false
      // 函数结束
    }
    
    • 怎么命中batchUpdate机制
      1. 生命周期和其中调用的函数
      2. React中注册的事件和其调用的函数
  3. transaction(事务)机制
    • 通过perform去执行任何函数
      1. 创建函数前,定义函数开始前执行的initialize和结束时的close
      2. 再执行函数(如下图,从左到右)
        image.png

5. 组件更新和渲染(fiber)

  • setState(newState) -> dirtyComponents(可能有子组件)
  • render() 生成 newVNode
  • patch 到 DOM 拆分成两个阶段(fiber优化)
    1. reconciliation阶段 -> 执行diff( js执行计算 )
    2. commit阶段(渲染) -> 将diff渲染DOM
  • fiber性能优化
    1. 将reconciliation阶段的任务拆分
    2. DOM渲染时暂停执行js计算,空闲时恢复js计算
    3. 使用的api:window.requestIdleCallback

6.性能优化

  1. 列表渲染使用key(非index、random)
  2. 组件销毁时销毁定时器、解绑事件
  3. 异步组件使用
  4. 减少函数的bind(this)次数(class组件里是在render还是在constrctorbind?)
  5. SCU、pureComponent(浅比较SCU)、memo
  6. 不可变值优化——immerimmutable(state层级设计要浅)

四、React Hooks

大致了解class组件的问题

  • 不易拆分。大型class组件拆分困难
  • 逻辑混乱。相同的业务逻辑分散到不同的class的方法中调用
  • 逻辑复用复杂。Mixins、HOC、Render prop

一句话理解函数组件接收props -> 返回一个jsx

  • 没有组件实例
  • 没有生命周期
  • 没有state、setState 所以需要通过Hooks增强函数组件的能力

1. State Hook

useState作用等同于class组件中的state、setState

核心用法:

  1. 传入默认值,返回数组。const [count, setCount] = useState(0)
  2. 通过state,这里的count获取值
  3. 通过setState,这里setCount修改值
// 手敲一顿,加深印象
function ClickCount () {
  const [count, setCount] = useState(0)

  const handleClick = function () {
    setCount(count + 1)
  }
  return (
    <div>
      <p>点击了{count}次</p>
      <button onClick={handleClick}>点击</button>
    </div>
  )
}

2. Effect Hook

useEffect让函数组件 模拟 生命周期

  1. 模拟class组件 DidMountDidUpdate。传入一个回调函数
    useEffect(() => { console.log('DidMount & DidUpdate') })
    
  2. 模拟class组件 DidMount。第二个参数传入空数组
    useEffect(() => { console.log('DidMount') }, [])
    
  3. 模拟class组件 DidUpdate。第二个数组参数中,传入需要收集的依赖(有点类似Vue的watch
    // 上面useState案例中的count,只要count修改了就会再次执行这个cb
    useEffect(() => { console.log('DidUpdate') }, [count])
    
  4. 模拟class组件 WillUnMount。回调函数中return一个函数
    useEffect(() => {
      const timer = setInterval(() => {
        console.log('sb')
      }, 1000)
      // 注意回调会在每个下次执行useEffect前执行
      return () => {
        console.log('WillUnMount')
        // 组件销毁时清除timer
        clearInterval(timer)
      }
    }, [count])
    
    • 需要注意的是只是模拟WillUnMount,但是不全相等。只要第二个参数不是空数组,每个下次执行useEffect前都会先执行return中的函数。总结:只要update就会执行一次销毁,并不是只有组件销毁时才执行
  5. 完全模拟 WillUnMount(组件销毁才执行return fn)。第二个参数传入空[]
    useEffect(() => {
      return () => {}
    }, [])
    

3. useRef和useContext

  1. useRef。主要可用来获取DOM
    const btnRef = useRef()
    useEffect(() => {
      // mount时打印按钮的DOM
      console.log('btnRef', btnRef.current)
    }, [])
    
    return (
      <div>
        <p>点击了{count}次</p>
        <button ref={btnRef}>点击</button>
      </div>
    )
    
  2. useContext。接收Context
    • 使用可参考:二、高级特性 —— 3. React Context

4. useReducer

理解useReducer:复杂化的useState

  • 比如修改state要执行多个逻辑,可以使用useReducer。参考以下代码案例
    const initReducer = { count: 1 }
    
    const reducer = (state, action) => {
      switch (action.type) {
        case 'add':
          // 如果需要再add的时候,处理很多逻辑,就可以使用useReducer
          return { count: state.count + 1 }
        default:
          return { count: state.count - 1 }
      }
    }
    
    function ClickCount () {
      // 用法跟useState很像
      const [state, dispatch] = useReducer(reducer, initReducer)
    
      const handleAdd = function () {
        // 通过执行dispatch传入type完成setState
        dispatch({ type: 'add' }) 
      }
      const handleReduce = function () {
        dispatch({})
      }
      return (
        <div>
          <p>count = {state.count}</p>
          <button onClick={handleAdd}>add</button>
          <button onClick={handleReduce}>reduce</button>
        </div>
      )
    }
    
  • useReducer 和 redux 的区别
    1. 单组件的状态管理。只是复杂的state处理调用简单化,组件通信需要props等
    2. redux属于全局的状态管理。多组件共享数据

5. useMemo

使用 useMemo缓存数据 + memo子组件浅比较(类似 PureComponent)进行性能优化,可对比class组件中的SCU优化。注意引用类型值一定要配合 useMemo。可参考如下demo,分两种情况来写,加深大家的理解

  1. 如果props为常规类型数据,可以不使用useMemo
    // memo包裹,实现props的浅比较
    const Child =  memo((props) => {
      console.log('child render')
      return <div>{props.val}</div>
    })
    
    function ClickCount () {
      const [count, setCount] = useState(0)
      // 如果传入的props是常量,则可以不使用useMemo。但是引用类型一定要用
      const val = '测试子组件更新'
      const handleAdd = function () {
        setCount(count + 1)
      }
      return (
        <div>
          <p>count = {count}</p>
          <Child val={val} />
          <button onClick={handleAdd}>add</button>
        </div>
      )
    }
    
  2. 如果props是引用类型数据,则必须使用useMemo
    • 第二个参数跟 useEffect 一样,传入依赖,当依赖改变时不再缓存之前的数据
    const Child =  memo((props) => {
      console.log('child render')
      return <div>{props.val.text}</div>
    })
    
    function ClickCount () {
      const [count, setCount] = useState(0)
      const [text, setText] = useState('测试子组件更新')
      // 注意这里时引用类型,每次函数重新执行,都会生成新的引用类型。好比:{} !== {}
      const val = useMemo(() => ({ 
        text 
      }), [text])
    
      const handleAdd = function () {
        setCount(count + 1)
      }
      return (
        <div>
          <p>count = {count}</p>
          <Child val={val} />
          <button onClick={handleAdd}>add</button>
        </div>
      )
    }
    

6. useCallback

原理跟上述 useMemo 类似,useMemo缓存数据,useCallback缓存函数。主要是优化 props 传递函数时,子组件数据没变但是每次都更新的问题~

  • React Hooks 的优化策略主要就是 useMemouseCallback

使用方法跟useMemo很类似,只是第二个参数是个空数组即可,不需要传依赖。这里直接给用法~

const fn = useCallback(() => { ... }, [])

7. 自定义Hook

可以很好的进行组件公共逻辑抽离。相比HOC优势:

  • 减少组件嵌套
  • 避免在组件间处理props透传

注意自定义Hook命名规范:useXxx (use开头)

这里以一个Dialog为Demo,自己手写一个Hook~

  1. 实现打开弹窗 showDialog
  2. 实现关闭弹窗 hideDialog
  3. 实现弹窗状态切换 switchStatus
// 自定义hook 控制 Dialog
function useDialog () {
  const [visible, setVisible] = useState(false)
  const showDialog = () => setVisible(true)
  const hideDialog = () => setVisible(false)
  const switchStatus = () => setVisible(!visible)
  
  return {
    visible,
    showDialog,
    hideDialog,
    switchStatus
  }
}

// 组件中使用useDialog
function App() {
  // 使用自定义hook,接收状态、操作函数
  const {visible, showDialog, hideDialog, switchStatus} = useDialog()
  return (
    <div className="App">      
      { visible && <div>dialog</div> }
      <button onClick={showDialog}>打开弹窗</button>
      <button onClick={hideDialog}>关闭弹窗</button>
      <button onClick={switchStatus}>切换弹窗状态</button>
    </div>
  );
}

8. Hooks注意事项

  1. 只能用在 函数组件 或者 自定义Hook 中使用

  2. 只能在顶层。 不能在循环判断中使用Hook

    • 要保证Hooks每次执行顺序都是一样的(不能被打断)
    if (flag) return   
    useEffect(() => {}) ❌
    
    • 为什么要保证执行顺序一样?
      1. 函数组件执行完即销毁。class组件执行完,组件实例不会被销毁
      2. 组件初始化、组件更新,都需要重新执行函数。从而获取最新的组件
      3. 函数组件没有实例,如何对应上初始化的时候的值?👇(顺序对应
      // 组件初始化时 name=jingboran;age=26
      const [name, setName] = useState('jingboran')
      const [age, setAge] = useState(26)
      
      // 组件更新,重新执行函数,这时候name,age会恢复上次name、age的值
      const [name, setName] = useState('jingboran')
      const [age, setAge] = useState(26)
      
      // 如果存在if、for等逻辑,以if为例 ❌
      if (flag) const [name, setName] = useState('jingboran')
      // 如果这次执行flag为false,那age就会对应到name的值,就会导致错乱
      const [age, setAge] = useState(26)
      
  3. useState默认值只有在组件初始化时有效,组件更新无效(由上可知,组件更新时,state是恢复上次的state,所以不走默认值)

    function App() {
      const [name, setName] = useState('井柏然')
      const info = { name }
    
      return (
        <div className="App">
          <button onClick={() => setName('jingboran')}>change Name</button>
          <Children info={info} />
        </div>
      );
    }
    
    function Children ({ info }) {
      // name只有在首次render的时候才会取info.name作为默认值
      const [name, setName] = useState(info.name)
      return (
        <div>
          <!-- 组件更新随父组件改变 -->
          <p>接收的props {info.name} </p>
          <!-- 组件更新恢复上次的name值,如果要修改,只能内部用setName -->
          <p>组件内部state {name}</p>
        </div>
      )
    }
    
    • 效果图如下

    state演示.gif

  4. useEffect第二个参数是空数组 时直接使用setState的坑

    • 如下代码,useEffect只有在组件初始化的时候执行。在Effect中修改了count,触发组件重新渲染,不会重新执行Effect函数,所以每次的interval中,拿到的都是首次的count
    • 解决方案:
      1. 传入依赖值count(组件更新时会重新执行Effect)
      2. 第二个参数不传。Effect函数在DidMount和DidUpdate中都会重新执行
    const [count, setCount] = useState(0)
    
    useEffect(() => {
      const timer = setInterval(() => {
        console.log('count 一直都是1', count)
        // 这里的count一直都是初始时候的count,所以一直都是 0 + 1
        setCount(count + 1)
      }, 1000)
      return () => {
        clearInterval(timer)
      }
    }, []) // 这里没有传任何依赖
    
  5. useEffect 第二个参数注意接收数组对象的依赖!操作不当可能会造成死循环

  • 判断依赖是否改变而重新执行Effect的实现时Object.is()

    image.png

    // 避免直接在useEffect的依赖中写引用类型
    useEffect(() => {}, [{}, []]) ❌
    

写在最后,从自己学React走过的弯路来说,其实最高效率的还是有一份全面知识体系的checklist。如果没有实战项目,又只对着文档敲,其实学不到什么,即使当时会了,一段时间就会忘记。没有形成知识体系,印象也不深刻。学习一门新框架最好还是循序渐进,按照一定的章法学习,从基础用法、高级用法、原理层面逐步深入,这样就能牢牢掌握了!希望我们最后都在大厂相遇,都走在自己希望走上的道路😝