likes
comments
collection
share

从 0 开始学习「React」

作者站长头像
站长
· 阅读数 31

简约,没有太多废话,直接干!

参考 —— React 官方中文文档

React 合集 (持续更新):


一、什么是 React

React 是用于构建用户界面的 JavaScript 库

  • React 用于「构建界面」,专注于视图,主要提供 UI 层的解决方案
  • 是一个「JavaScript 库」,并不是一个 框架,不能解决项目中的所有问题,需要结合其它的库来协助提供完整的解决方案,如:Redux/React-router 等,在这周边生态的配合下,才能组合成一个 框架

三大特性

声明式

命令式编程 v.s. 声明式编程

  • 命令式编程:用代码告诉「计算机」去做什么
    • 老板:“小王啊,去我办公室,桌上有份文件,打开后拿过来给我”
  • 声明式编程:通过代码告诉「计算机」我想要的是什么,让「计算机」想出如何去做
    • PM:“TA 同学,这需要你实现个五彩斑斓的黑”
    • 具体 TA 同学怎么去实现这个五彩斑斓的黑,PM 不关心,PM 只关心实现这个五彩斑斓的黑

组件化

函数就是组件,组件就是函数(因为我基本不用 class 组件)

  • React 提供了一种全新的语法扩展:JSX
  • 创造性地将 渲染逻辑UI 逻辑 结合在一起
  • 这个结合体在 React 中,就成为组件
  • 组件化的出现,大幅度提升了代码的复用率

一次学习,随处编写

学会后,可以在很多地方使用 React 语法来写代码

  • 配合 React 写 web 页面
  • 配合 React Native 写 手机客户端 APP
  • 配合 React 360 写 VR界面

二、「起手式」开始使用

从零开始,不多废话

预备知识:

  1. 已经安装 node,并且会使用 node,包括 npm 或 yarn;
  2. 了解 ES6 语法
  3. 会使用 vs code

create-react-app

React 官方能提供的脚手架工具 —— 中文文档

打开终端,敲以下代码:

我使用 yarn

1. 全局安装 create-react-app

yarn global add create-react-app

2. 创建一个 React 项目

create-react-app react-demo

react-demo 是你的项目名,你爱咋命名就咋命名(不要有大写,会给你个报错)

3. 进入项目

cd react-demo/

4. 使用 vs code 打开项目

code .

5. 本地运行项目

yarn start

6. 删除

把无关的东西都删掉,便于入门学习

选中的这些文件,看到没?都给它删咯:

从 0 开始学习「React」

删完之后你看到的应该需要和我一样:

从 0 开始学习「React」

7. 写代码

进入 public\index.html 「复制」以下代码,「粘贴替换」

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>我的第一个 React 项目</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

进入 src\index.js 清空现有代码,「手写」以下代码!

从 0 开始学习「React」

现在页面应该是这样子的:

从 0 开始学习「React」

三、关于 JSX

JSX 是 React 定义的一个 JS 语法扩展,可以在 JS 里写类似 HTML 的代码,但它「并不是 HTML」

在刚刚手写代码的时候,你就已经写了 JSX 代码!

注意事项

  • 注意 className

  • 只有一个语法:想写 JS 代码,就用 {} 包起来

    • 标签里面 的所有 JS 代码都要用 {} 包起来
    • 如果需要变量 n,那么就用 {} 把 n 包起来
    • 如果需要对象,那么久用 {} 把对象包起来,如 {{name: 'heycn'}}
  • return 后面习惯写上一个括号,括号包住代码

四、组件

1. 组件 V.S. 元素

  • 这是一个 React 元素:const div = React.createElement('div', ...)(注意 d 小写)
  • 这是一个 React 组件:const Div = () => React.createElement('div', ...)(注意 D 大写)
  • 注意大小写,这是一个「不成文的规定」

2. 什么是 React 组件

  • 能够跟其他物件 组合 起来的 物件,就是组件
  • 组件并没有明确的定义,全靠 感觉理解
  • 目前而言,一个返回 React 元素的 函数,就是组件
  • 在 Vue 里,一个 构造选项 就可以表示一个组件

3. React 的两种组件

「函数组件」

我最喜欢的

创建一个函数组件:

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

// 或

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

使用方法:

<Welcome name="chennan" />

「类组件」

我比较少用

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

使用方法:

<Welcome name="chennan" />

4. <Welcome /> 会被翻译成什么

「会被翻译成什么」

  • <div /> 会被翻译为 React.createElement('div'),如果是 原生的HTML标签,那么 div 就会自动变成字符串
  • <Welcome /> 会被翻译为 React.createElement(Welcome),如果是大写的 Welcome,那就会当做 “函数” 放在这里
  • 可以去 babeljs.io 看一下怎么翻译的

「React.createElement 的逻辑」

  • 如果传入一个 字符串 'div',这会创建一个 div,注意这个 div 是 虚拟 DOM
  • 如果传入一个 函数,就会调用该函数,并且获取其返回值
  • 如果传入一个 ,则在类前面加一个 new(这会导致执行 constructor),获取一个组件对象,然后调用对象的 render 方法,获取其返回值

5. 类组件和函数组件使用 props (外部数据)

  • 类组件直接读取属性 this.props.xxx
  • 函数组件直接读取参数 props.xxx
// 代码示例
// other codes...

function App() {
  return (
    <div>
      爸爸
      <Son messageForSon='儿子你好' />
    </div>
  )
}

class Son extends React.Component {
  render() {
    return (
      <div>
        我是儿子,我爸对我说「{this.props.messageForSon}」
        <Grandson messageForGrandson='孙子你好' />
      </div>
    )
  }
}

const Grandson = props => {
  return (
    <div>
      我是孙子,我爸对我说「{props.messageForGrandson}」
    </div>
  )
}

// other codes...

6. 类组件和函数组件使用 state (内部数据)

  • 类组件用 this.state 读,this.setState
  • 函数组件用 useState 返回数组,第一项读,第二项写

「类组件使用 state

普通写法

// other codes...

class Son extends React.Component {
  constructor() {
    super()
    // 初始化数据
    this.state = { n: 0 }
  }
  add() {
    // 写数据用 this.setState 去写
    this.setState({ n: this.state.n + 1 })
  }
  render() {
    return (
      // 读数据 this.state.n
      <div>
        儿子 n: {this.state.n}
        <button onClick={() => this.add()}>+1</button>
      </div>
    )
  }
}

// other codes...

注意:写数据的时候,最好使用一个新的对象,而不是在原有的对象上进行修改

牛 x 写法

「React」牛x的前端这么写 setState()

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

注意:setState 不会马上去改 state 的值,所以最好使用函数去读新的值是最好的

注意事项

  • this.state.n += 1 无效?

    • 其实 n 已经改变了,只不过 UI 不会自动更新而已
    • 调用 setState 才会触发 UI 更新(异步更新)
    • 因为 React 没有像 Vue 监听 data 一样监听 state
  • setState 会异步更新 UI

    • setState 之后,state 不会马上改变,马上读 state 会失败
    • 所以更推荐的方式是 setState( 这里写函数 )
  • 不推荐 this.setState(this.state)

    • React 希望我们不要修改旧 state(数据不可变)
  • 这是一种函数式的理念

「函数组件使用 state

const App = () => {
  const [n, setN] = React.useState(0) // 第一项读,第二项写;0 是初始值
  return (
    <div>
      n: {n}
      <button onClick={() => setN(n + 1)}>+1</button>
    </div>
  )
}

函数组件 setN 永远不会改变 n

注意事项

  • 跟类组件类似的地方

    • 也要通过 setX (新值) 来更新 UI
  • 跟类组件不同的地方

    • 没有 this,一律用参数和变量
    • 爽吧?

7. class 组件 深入浅出

跟上面会有些重复

「ES6 创建 class 组件的方式」

import React from 'react'

class App extends React.Component {
  constructor() {
    super()
    this.state = { n: 0 }
  }
  render() {
    return (
      <div>n: {this.state.n}</div>
    )
  }
}

export default App

「props」

我将它翻译为「外部数据」,更便于理解

props 的作用

  • 接受外部数据

    • 只能读不能写
    • 外部数据由父组件传递
  • 接受外部函数

    • 在恰当的时候,调用该函数
    • 该函数一般是父组件的函数

初始化

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

这样做之后,this.props 就是外部数据 对象的地址

怎么读

// 通过 this.props.xxx 来读
this.props.xxx

语法

class A extends React.Component {
  constructor(props) {
    super(props)
  }
  render(){
    return <div onClick={this.props.onClick}>
      {this.props.name}
      <div>
        {this.props.children}
      </div>
    </div>
  }
}

示例

class A extends React.Component {
  constructor(props) {
    super(props)
    this.state = { x: 1 }
  }

  onClick = () => {
    this.setState({
      x: this.state.x + 1
    })
  }

  render() {
    return (
      <div>
        这是 A
        <button onClick={this.onClick}>+1</button>
        <B name={this.state.x} />
      </div>
    )
  }
}

class B extends React.Component {
  render() {
    return (
      // 使用 this.props.name 来读
      <div>n: {this.props.name}</div> 
    )
  }
}

怎么写

  • 永远不要去改 props
  • 外部数据,就应该由外部更新
  • 这是 React 的理念!

「state & setState」

我将 state 翻译为「内部数据」

初始化

class A extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      user: {
        name: 'heycn',
        age: 18
      }
    }
  }

  render() {
    return ( ... )
  }
}

怎么读

// 通过 this.state.xxx 来读
this.state.xxx

怎么写

// 通过 this.setState(???, fn) 来写
this.setState(???, fn)

参考这篇博客:「React」牛x的前端这么写 setState()

推荐第二种方法

方法一

重点看 onClick1 的代码

class A extends React.Component {
  constructor(props) {
    super(props)
    this.state = { x: 1 }
  }

  onClick1 = () => {
    this.setState({ x: this.state.x + 1 })
  }

  render() {
    return (
      <div>
        x: {this.state.x}
        <button onClick={this.onClick1}>+1</button>
      </div>
    )
  }
}
方法二

重点看 onClick2 的代码

class A extends React.Component {
  constructor(props) {
    super(props)
    this.state = { x: 1 }
  }

  onClick2 = () => {
    this.setState(state => ({ x: state.x + 1 }))
  }

  render() {
    return (
      <div>
        x: {this.state.x}
        <button onClick={this.onClick2}>+1</button>
      </div>
    )
  }
}

总结

  • 读用 this.state

    • this.state.xxx.yyy.zzz
  • 写用 this.setState(???, fn)

    • this.setState(newState, fn)
    • 注意:setState 不会立刻改变 this.state,会在当前代码运行之后,再去更新 this.state,从而触发 UI 更新
    • this.setState((state, props) => newState, fn)
    • 这种方式的 state 反而更容易理解
    • fn 会在写入成功后执行
  • 写时会 shallow merge

    • setState 会自动更新 state 与旧 state 进行 第一层的合并

8. 函数组件 深入浅出

「创建方式」

实现 +1 功能

import React, { useState } from 'react'

const App = props => {
  const [n, setN] = useState(0)
  const onClick = () => setN(n + 1)
  return (
    <div>
      {n}
      <button onClick={onClick}>+1</button>
    </div>
  )
}

export default App

class 组件 简约得多

「没有 state 怎么办」

React v16.8.0 推出 Hooks API,其中 useState 可以解决问题

「没有生命周期钩子怎么办」

React v16.8.0 推出 Hooks API,其中 useEffect 可以解决问题

「模拟 c...DidMount

useEffect(() => { console.log('第一次渲染') }, []) 返回的第二个参数为空数组

import React, { useState, useEffect } from 'react'

const App = props => {
  const [n, setN] = useState(0)
  const onClick = () => setN(n + 1)
  useEffect(() => {
    console.log('第一次渲染')
  }, [])
  return (
    <div>
      {n}
      <button onClick={onClick}>+1</button>
    </div>
  )
}

export default App

「模拟 c...Didupdate」

useEffect(() => { console.log('n 更新了') }, [n]) 返回的第二个参数里面写:什么东西更新的时候执行

useEffect(() => { console.log('任意属性更新') }) 如果想要任何一个 state 变化都要执行的话,就去掉中括号 []

import React, { useState, useEffect } from 'react'

const App = props => {
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)
  const onClickN = () => setN(n + 1)
  const onClickM = () => setM(m + 1)
  useEffect(() => {
    console.log('n 更新了')
  }, [n])
  return (
    <div>
      {n} <button onClick={onClickN}>n+1</button>
      <hr />
      {m} <button onClick={onClickM}>m+1</button>
    </div>
  )
}

export default App

「模拟 c...WillUnmount」

useEffect(() => { return () => {console.log('Child 销毁了') } })useEffect 里 return 另外一个函数

import React, { useState, useEffect } from 'react'

const App = props => {
  const [childVisible, setChildVisible] = useState(true)
  const hide = () => setChildVisible(false)
  const show = () => setChildVisible(true)

  return (
    <div>
      {childVisible ? <button onClick={hide}>hide</button> : <button onClick={show}>show</button>}
      {childVisible ? <Child /> : null}
    </div>
  )
}

const Child = props => {
  useEffect(() => {
    return () => {
      console.log('Child 销毁了')
    }
  })
  return (
    <div>Child</div>
  )
}

export default App

「其他生命周期钩子什么模拟」

  • constructor 函数组件执行的时候,就相当于 constructor

  • shouldComponentUpdate 后面的 React.memo 和 useMemo 可以解决

  • render 函数组件 return 的东西就是 render 的东西

总结: 能用函数组件就用函数组件吧,因为它更简单

五、生命周期钩子

类比以下代码

  • let div = document.createElement('div') 这是 create/construct 过程
  • div.textContent = 'hi 这是初始化 state
  • document.body.appendChild(div) 这是 div 的 mount 过程
  • div.textContent = 'hi2 这是 div 的 update 过程
  • div.remove() 这是 div 的 unmount 过程

同理

  • React 组件也有这些过程,我们称为生命周期
  • 不过只需记住这几个常用的

1.constructor

在这里初始化 state

2.shouldComponentUpdate

是否需要更新组件

用途

  • 它允许我们手动判断是否要进行组件更新,我们可以根据应用场景灵活地设置返回值,以避免不必要的更新(让我们自己确定是否更新)
  • 返回 true 表示不阻止 UI 更新
  • 返回 false 表示阻止 UI 更新

示例

 class App extends React.Component {
   constructor(props) {
     super(props)
     this.state = { n: 1 }
   }
   onClick = () => {
     // onClick 函数里对 n 进行了 +1 -1 操作
     this.setState(state => ({ n: state.n + 1 }))
     this.setState(state => ({ n: state.n - 1 }))
   }
   shouldComponentUpdate(newProps, newState) {
     if (newState.n === this.state.n) {
       return false  // 返回 false,表示阻止 render
     } else {
       return true  // 返回 true,表示不阻止 render
     }
   }
   render() {
     console.log('render了') // 不加 shouldComponentUpdate 的时候 log 一下看看?
     return (
       <div>
         n: {this.state.n}
         <button onClick={this.onClick}>+1</button>
       </div>
     )
   }
 }

使用内置 React.PureComponent

使用 React.PureComponent 代替 React.Component

PureComponent 会在 render 之前对新旧 state 进行对比(之对比第一层,浅对比),如果对比结果一样,那就不更新

 class App extends React.PureComponent {
   constructor(props) {
     super(props)
     this.state = { n: 1 }
   }
   onClick = () => {
     // onClick 函数里对 n 进行了 +1 -1 操作
     this.setState(state => ({ n: state.n + 1 }))
     this.setState(state => ({ n: state.n - 1 }))
   }
   render() {
     console.log('render了')
     return (
       <div>
         n: {this.state.n}
         <button onClick={this.onClick}>+1</button>
       </div>
     )
   }
 }

3. render

创建虚拟 DOM

用途

  • 展示视图

    return (<div>...</div>) 这是虚拟 DOM

  • 只能有一个根元素

  • 如果有两个根元素,就要用 <React.Fragment> 包起来

  • <React.Fragment> 可以缩写成<></>

示例

 class App extends React.PureComponent {
   constructor(props) {
     super(props)
     this.state = { n: 1 }
   }
   onClick = () => {
     this.setState(state => ({ n: state.n + 1 }))
   }
   render() {
     return (
       <>
         n: {this.state.n}
         <button onClick={this.onClick}>+1</button>
       </>
     )
   }
 }

可以写 if...else...

&& 也可以,|| 也可以

 class App extends React.PureComponent {
   constructor(props) {
     super(props)
     this.state = { n: 1 }
   }
   onClick = () => {
     this.setState(state => ({ n: state.n + 1 }))
   }
   render() {
     return (
       <>
         {this.state.n % 2 === 0 ? <div>偶数</div> : <div>奇数</div>}
         <button onClick={this.onClick}>+1</button>
       </>
     )
   }
 }

可以写 map

不可以直接写 for 循环,需要用数组,可以用 map 来代替

 class App extends React.PureComponent {
   constructor(props) {
     super(props)
     this.state = { array: [1, 2, 3] }
   }
   render() {
     return this.state.array.map(n => <div key={n}>{n}</div>)
   }
 }

记得要写 key

4. componentDidMount

组件已出现在页面

用途

  • 在元素插入页面后执行代码,这些代码一般来说应该依赖 DOM
  • 比如:获取 div 的高度,就最好在这里写
  • 此处可以发起 加载数据 的 AJAX 请求(官方推荐)
  • 首次渲染会执行此钩子

5. componentDidUpdate

组件已更新

用途

  • 在视图更新后执行代码
  • 此处也可以发起 AJAX 请求,一般用于 更新数据
  • 首次渲染不会执行此钩子
  • 在此处 setState 可能会引起无限循环,除非放在 if 里
  • shouldComponentUpdate 返回 false,则不触发此钩子

6. componentWillUnmount

组件将要被卸载

用途

  • 组件将要 被移出页面然后被销毁 时执行代码
  • unmount 过的组件不会再次 mount

举例

  • 如果在 componentDidMount 里监听了 window scroll,那么就要在 componentWillUnmount 里取消监听
  • 如果在 componentDidMount 里创建了 Timer,那么就要在 componentWillUnmount 里取消 Timer
  • 如果在 componentDidMount 里创建了 AJAX 请求,那么就要在 componentWillUnmount 里取消请求
  • 否则前端页面的内存会很多
  • 原则:谁污染谁整治

六、Hooks

有哪些

  • useState
  • useEffect & useLayoutEffect
  • useContext
  • useReducer
  • useMemo
  • useRef
  • 自定义 Hook

1. useContext

我把它翻译为 上下文

  • 全局变量 是全局的 上下文
  • 上下文 是局部的 全局变量

使用方法

  1. 使用 C = createContext(initial) 创建上下文
  2. 使用 <C.provider> 圈定作用域
  3. 在作用域内使用 useContext(C) 来使用上下文

注意事项

  • 你在一个模块将 C 里面的值改变
  • 另一个模块不会感知到这个变化

2. useEffect & useLayoutEffect

useEffect

副作用(这个 API 名字起得非常不好!)

  • 对环境的改变就叫副作用,如修改 document.title = 'hi'
  • 但我们不一定非要把副作用放在 useEffect 里
  • 实际上叫做 afterRender 更好,因为实在每次 render 后执行

用途

  • 作为 componentDidMount 使用,[] 作第二个参数,只会在第一次渲染执行
  • 作为 componentDidUpdate 使用,可指定依赖,只会在指定的依赖更新的时候执行
const App = () => {
  const [n, setN] = useState(0)
  const onClick = () => {
    setN(i => i + 1)
  }
  useEffect(() => {
    console.log('第 1 次渲染执行')
  }, [])
  useEffect(() => {
    console.log('任何一个 state 变化都会执行,包括第 1 次')
  })
  useEffect(() => {
    console.log('每次 n 变化都会执行,包括第 1 次')
  }, [n])
  useEffect(() => {
    if (n !== 0) {
      console.log(`n 变化时执行,现在 n 是: ${n}`)
    }
  }, [n])
  console.log('------ 分割线 ------')
  return (
    <div>
      n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  )
}
  • 作为 componentWillUnmount 使用,通过 return 函数,这个函数会在组件快死掉的时候执行,用来清理现场(清理垃圾)
useEffect(() => {
  const id = setInterval(() => {
    console.log('hi')
  }, 1000)
  return () => {
    window.clearInterval(id)
  }
}, [])

但是目前无法消除,以后项目中会用到

  • 以上三种用途可同时存在

特点

  • 如果同时存在多个 useEffect,会按照顺序执行

useLayoutEffect

布局副作用

  • useEffect 在浏览器渲染完成后执行(在屏幕像素改变之后执行)
  • useLayoutEffect 在浏览器渲染前执行(在屏幕像素改变之前执行,在 DOM 操作之后紧接着执行)
const App = () => {
  const [value, setValue] = useState(0)

  // 这时网速慢的话,页面会从 value: 0 -> value: 1000
  // useEffect(() => {
  //   document.querySelector('#x').innerText = `value: 1000`
  // }, [value])

  // 这时候使用 useLayoutEffect 就可以解决 value 从 0 -> 1000 时抖动的情况
  useLayoutEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value])

  return (
    <div id='x' onClick={() => setValue(0)}>
      value: {value}
    </div>
  )
}

特点

  • useLayoutEffect 总是比 useEffect 先执行
  • useLayoutEffect 里的任务最好是影响了 Layout(最好是影响外观),如果任务是 '上报数据' 这种,就不需要放在这里占用时间了

经验

  • 为了用户体验,优先使用 useEffect,优先 render

3. useMemo

多余的 render

在学习 useMemo 前,首先需要知道 React.memo

const App = () => {
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)

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

  return (
    <div>
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m} />
    </div>
  )
}

const Child = props => {
  console.log('执行了')
  console.log('如果这里有大量代码')
  return <div>Child: {props.data}</div>
}

上面代码只改变了 n,m 没有改变,但是 Child 还是执行了,但我不想执行 Child

解决方法,使用 React.memo

const App = () => {
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)

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

  return (
    <div>
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m} />
    </div>
  )
}

// 使用 memo 能解决不必要的 render
const Child = memo(props => {
  console.log('执行了')
  console.log('如果这里有大量代码')
  return <div>Child: {props.data}</div>
})

小结

  • 我的 n 变了,m 没变
  • 但是依赖于 m 的组件却自动刷新了
  • 那么就可以使用 React.memo
  • 如果 props 不变,就没有必要再次执行一个函数组件

但是,这里有个 BUG:

const App = () => {
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)

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

  const onClickChild = () => {}

  return (
    <div>
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m} onClick={onClickChild} />
    </div>
  )
}

// 使用 memo 能解决不必要的 render
const Child = memo(props => {
  console.log('执行了')
  console.log('如果这里有大量代码')
  return <div onClick={props.onClick}>Child: {props.data}</div>
})

我在 Child 添加了 onClick,但是函数没有任务,Child 竟然进行了 不必要的 render

这是什么呢?

  • 因为,当点击 n + 1 的时候
  • App() 会重新执行
  • 那么第 9 行代码 const onClickChild = () => {} 会重新执行,之前执行的是空函数,现在执行的是新的空函数
  • 虽然都是空函数,但是每次执行的都是不一样的空函数
  • 那么执行的是不同的空函数,那么就代表 Child 的 onClick 变了
  • 那为什么 n 可以?因为 n 是一个数值,数值是相等的,而函数是一个对象
  • 第一次 空函数的地址,和第二次 空函数的地址不相等的,即使功能是一样
  • 这就是 引用 的区别

如何解决?那我们就需要用到 useMemo

useMemo

可以实现函数的重(chong)用

useMemo 接受一个函数和一个参数,函数的返回值是 你要缓存的东西,第二个参数是依赖(代码中表示:当 m 变化了,我再执行)

作用

  • 用来缓存一些在两次新旧组件迭代的时候
  • 希望用上一次的值
  • 比如我上面的代码:
    • 我希望 m 改变的时候,再执行
    • 如果 m 没改变,那我就不执行
const App = () => {
  console.log('App 执行了')
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)

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

  const onClickChild = useMemo(() => {
    return () => {
      console.log('我不会执行的')
    }
  }, [m])

  return (
    <div>
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m} onClick={onClickChild} />
    </div>
  )
}

const Child = memo(props => {
  console.log('执行了')
  console.log('如果这里有大量代码')
  return <div onClick={props.onClick}>Child: {props.data}</div>
})

特点

  • 第一个参数是 () => value
  • 第二个参数是依赖 [m]
  • 只有当依赖变化时,才会计算出新的 value
  • 如果以来不变,那么就重用之前的 value
  • 这很像 Vue2 的 computed

注意

  • 如果 value 是个函数,那么就要写成一个这么奇怪的东西:useMemo(() => (x) => console.log('我执行了'))
  • 这是一个返回函数的函数
  • 于是就有了 useCallback,它的作用和 useMemo 一模一样!
const onClickChild = useCallback(() => {
  console.log('我不会执行的')
}, [m])
  • useMemo((x) => console.log('我执行了'), [m]) 等价于useMemo(() => (x) => console.log('我执行了'), [m])

4. useRef

目的

  • 如果你需要一个值,在组件不断 render 时 保持不变,就需要使用到 useRef
  • 初始化:const count = useRef(0)
  • 读取:count.current
const App = () => {
  const [n, setN] = useState(0)
  const count = useRef(0)
  const onClick = () => setN(n => n + 8)

  useEffect(() => {
    count.current += 1
    console.log(`执行了 ${count.current} 次`)
  })

  return (
    <div>
      n: {n}
      <button onClick={onClick}>n+1</button>
    </div>
  )
}

为什么需要 current

为什么需要 current 来 读/写 它的值?

为了保证两次 useRef 是同一个值(只要引用能做到)

能做到变化时自动 render 吗?

  • 不能!
  • 因为这不符合 React 的理念(函数式编程)
  • React 的理念是:UI = f(data)
  • 你如果想要这个功能,完全可以自己加
  • 监听 ref,当 ref.current 变化时,调用 setX 即可

5. forwardRef

  • 如果你的函数组件,想要接受别人传来的 ref 参数
  • 必须把这个函数用 forwardRef 包起来
  • 所以:想用 ref,就必须使用 forwardRef
const App = () => {
  const buttonRef = useRef(null)
  return (
    <div>
      <Button ref={buttonRef}>按钮</Button>
    </div>
  )
}

const Button = forwardRef((props, ref) => {
  return <button ref={ref} {...props} />
})

useRef

  • 可以用来引用 DOM 对象
  • 也可以用来引用普通对象

forwardRef

  • 由于 props 不包含 ref,所以需要 forwardRef
  • 为什么 props 不包含 ref 呢?因为大部分时候不需要

6. useImperativeHandle

  • 对 ref 进行设置
  • 大概率不会用到这个 Hook

自定义 Hook

这个功能可能是 React Hooks 里面最牛 x 的功能

自定义 Hook 之 useUpdate

import { useState, useEffect } from 'react'

const useUpdate = (fn, dep) => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    setCount(x => x + 1)
  }, [dep])
  useEffect(() => {
    if (count > 1) {
      fn()
    }
  }, [count, fn])
}

export default useUpdate

最后

  • 好了,差不多这样子,就可以找一些小项目练练手了
  • 多实践才能提高

完。

转载自:https://juejin.cn/post/7080439852323307556
评论
请登录