20分钟带你vue转react

2,318 阅读23分钟

前言

vuereact 都是 js 框架,均是 mvvm 框架和 SPA 单页应用,通过数据更新驱动试图的更新

二者在应用场景方面是有一定区别的,vue 源码设计之初其目的就是让用户更好的上手,封装的很彻底。因此 vue 非常适合移动端、中小型 pc 端项目的开发,目前很多大公司都是 react 开发

目前 react 最新版本已经达到 18 了,react 更新迭代很快,基本上一年一更,react 是由 Facebook 公司开源的,生态非常完善,react 还有一套源码 react-native 用于移动端开发

其实 vue 的诞生就是借鉴了 react 的设计理念,本文将从有一定 vue 基础的视角去学习 react

正文

先用脚手架创建一个 react 项目,此前用 vue 项目时用得脚手架有 vite,其实 vite 也可以创建 react,甚至可以创建 angular,vue 的脚手架是 Vue-cli

官方文档介绍中如何创建一个 react 框架,它用的是 next

1.png

next 是 react 的一个框架, next 将 react 封装成为一个全栈式框架,nuxt 是 vue 的框架,nest 是 node 框架

记得用 vue-cli 还需要在电脑上安装东西,而 react 不用

npm 是 node 中的包管理工具,而 npx 也是 node 管理工具,一般临时用 npx

这里我们不用 next 来创建 react 项目,那个给你弄成全栈式了,这里没必要,还有个 Create React App 脚手架

2.png

react 脚手架无需我们安装,react 官方直接将这个脚手架上传到了 node 库中,npx 安装是临时安装的,这个项目一旦创建完成,脚手架的源码是会自动删除的

安装很慢可以考虑 node 源不行,换源方法此前提过初识node | 模块化 | yrm换源 | nvm切换node - 掘金 (juejin.cn)

还是不行可以直接把 creat-react-app 脚手架全局下载下来

npm i -g creat-react-app

还是不行就用 vite

npm create vite@latest react-basic -- --template react

vite 需要自行安装依赖,这里我就用 vite 来创建

项目目录

大致解释下生成的目录文件

3.png

package.json

4.png

用 vue 的都知道,vue 这里的 dependencies 只有一个,但是 react 有两个,react 官方将 react 源码拆成了两部分,一个是 react 源码,还有个是 react-dom,其实 vue 也有个 dom 是 compiler-dom , react 将其拆分了出来, react-dom 源码就是用于将虚拟 dom 转换成真实 dom

index.html

5.png

跟 vue 区别不大,vue 中这个 div 的 id 为 app,这里是 root

src/assets/react.svg

svg 图片是用代码描述的,性能更好

main.jsx

6.png

类似于 vue 的 main.js,create-react-app 创建的都是 js 后缀,而 vite 都是 jsx,其实二者没有区别

react 中所有的文件都是 js 文件,不像 vue 都是 vue 后缀

看这个 main.jsx 的最后代码,引入两份 react 代码,通过 reactDom 来渲染来自 App.jsx 中的 root,并用的 react 的严格模式,render 就相当于 vue 中的 mount 挂载

App.jsx

7.png

App.jsx 其实就是 vue 中的 App.vue,根组件。我们看这个代码,跟原生 js 简直一模一样,没有 vue 中的 template 模板语法, react 就是原生 js 开发,创建一个函数,抛出一个函数。 react17 或者更早的版本是类组件写法

package.json

8.png

同样,这个是项目说明书,我们可以看到项目如何启动:npm run dev

为什么 vite 创建的 js 是 jsx

其实 react 脚手架用的 create react app 更多,不过 vite 创建的 react 项目默认是 jsx 后缀,本质上和 js 后缀没有区别。

但是非要论区别,jsx 有点像是 vue 的 template,jsx 等同于 js 和 html 或者 xml, jsx 语法就是在 js 中写 html,其优势在于降低我们的学习成本, jsx 是 js 的拓展语法,浏览器同样读不懂这个语法, react 源码可以帮你读懂,其实 vue3 也可以读懂这个语法

其实这个语法和 jsp 有一点相似, jsp 是在 js 中写 java,而 jsx 是在 js 中写 html

当然 jsx 和 js 的区别还有一个,就是 jsx 可以快捷方式写 html

语法

react 学起来会比 vue 快,因为 react 贴近原生 js 开发

组件式开发

既然也是组件开发,那么就存在组件之间的通讯,待会儿再看

比如我把 App.jsx 改成如下代码,这就是 jsx 语法

function App () {
  return (
    <div className="app">
      <h2>hello React</h2>
    </div>
  )
}

export default App

react 中的类名是 className 而非 class

js需要写在{ }中

这里简单写一个 ul,如果是 vue 就是 v-for,我们看 react

const songs = [
  { id: 1, name: '爷爷泡的茶' },
  { id: 2, name: '发如雪' },
  { id: 3, name: 'Things you said' }
]

function App () {
  return (
    <div className="app">
      <h2>hello React</h2>
      <ul>
        {
          songs.map(item => {
            return <li>{item.name}</li>
          })
        }
      </ul>
    </div>
  )
}

export default App

react 还是用的 { },目前这样写是会报一个错的

9.png

报了一个没有唯一 key 的错误的,熟悉吧,跟 vue 很像,不过 vue 不会报错,均是涉及到 diff 算法的内容

li 加上唯一 key

return <li key={item.id}>{item.name}</li>

这里遍历用的 map,如果用 forEach 是没有效果,我们需要有一个返回值给到外层的 { }

jsx 中不能存在 if 语句

假设写一个开关变量,为 true 展示某个内容,为 false 展示别的内容

10.png

这样写是不行的,报错

jsx 语法中是不能出现 if 语句,一般我们会去写三元运算

<h3>{flag ? 'react' : 'vue'}</h3>

逻辑与运算符

我们可以写一个开关变量控制一个标签的出现,就是用的与 && 运算符,很优雅

{ flag && <a href="#">Dolphin</a> }

用三元就是下面这样

{ flag ? <a href="#">Dolphin</a> : null }

行内样式

先看下行内样式如何写的

<h2 style={{color: 'red'}}>红色字体</h2>

这里虽然也是 {{ }} ,很像是 vue 挖坑写法,但是其实不是,外层 { } 是用来写 js 的,里面的 { } 是样式, red 一定要打引号,否则当成变量处理

当然,你也可以把样式写到一个对象中,这个对象拿出去写

const styleObj = {
  color: 'blue'
}

function App () {
  return (
    <div className="app">
      <h2 style={styleObj}>蓝色字体</h2>
    </div>
  )
}

export default App

样式写到css中

App.jsx

import './App.css'

function App () {
  return (
    <div className="app">
      <h2 className="green">绿色字体</h2>
    </div>
  )
}

export default App

App.css

.green{
    color: green;
}

这种写法样式就不需要打引号了,一个组件同级目录下对应一个 css 文件,记得引入

动态绑定类名

import './App.css'

const showGreen = true

function App () {
  return (
    <div className="app">
      <h2 className={showGreen ? 'green' : ''}>绿色字体</h2>
    </div>
  )
}

export default App

依旧是 { } 写 js

jsx 表达式只能一个父元素

像下面这种写法是不允许的

function App () {
  return (
    <div className="app"></div>
    <div className="box"></div>
  )
}

export default App

return 里面不能有两个节点,若非要两个就用一个空的 div 包裹一下,其实还有个更优雅的写法,直接写个的容器,如下

function App () {
  return (
	<>
	    <div className="app"></div>
    	<div className="box"></div>
	</>
  )
}

export default App

组件

组件名一定要大写,另外,在 react 中,一个函数就是一个组件

function HellowReact () {
    return <div>这是一个函数组件</div>
}

function App () {
    return (
        <div className="app">
            <HellowReact />
        </div>
    )
}

export default App

当你写的 html 只有一个节点时,就可以 return 时不加括号

另外,这里还可以换个写法,函数放到 { } 中调用,返回给 { }

function HellowReact () {
    return <div>这是一个函数组件</div>
}

function App () {
    return (
        <div className="app">
            {
                HellowReact()
            }
        </div>
    )
}

export default App

18 版本之前的是类编程,而不是现在这样写函数,类也是函数,这种写法复杂点,如下

import React from 'react'

class HelloVue extends React.Component {
    render() {
        return <h3>这是一个类组件</h3>
    }
}

function App () {
    return (
        <div className="app">
            <HellowReact />

            <HelloVue />
        </div>
    )
}

export default App

类中写 return 之前需要用上 render,而这个 render 又需要 import 导入 ,并且从 Component 中继承过来

绑定事件

在 vue 中想要绑定一个事件是 v-on ,而 react 则是用原生 js 写法

原生 js 写法有两种,一种是获取 dom 然后 addEventListener 监听;另一种是比如 button 上写一个 onclick ,通过 on 来绑定

react 是采用后者写法,不过与原生略有不同,on 后面的单词采用驼峰写法

function HellowReact () {
    const handler = (e) => {
        console.log(e, '按钮被点击');
    }

    return <div>
        <p>这是一个函数组件</p>
        <button onClick={handler}>click me</button>
    </div>
}

外部组件

方才讲的组件引入都是内部,这里我们看看外部如何使用

快捷方式

对了,react 可以安装插件 ES7 React/Redux/GraphQL/React-Native snippets

11.png

这样就可以用快捷方式 rafc 或者 rsc 来迅速创建一个函数组件,类组件的快捷方式是 rcc

也可以用 rfcp 迅速创建一个函数组件,这个有多个类型限制,待会儿再聊

需要用下面这个插件 Reactjs code snippets

12.png

我们再创建一个组件 src/components/ExitComponent.jsx

const ExitComponent = () => {
  return (
    <div>
      <p>外部组件</p>
    </div>
  )
}

export default ExitComponent

我创建一个数组放进去遍历出来

const ExitComponent = () => {
  const list = [
    { id: 1, name: 'react' },
    { id: 2, name: 'vue' }
  ]

  const onDel = (id) => {

  }

  return (
    <div>
      <p>外部组件</p>
      <ul>
        {
            list.map(item => (
                <li key={item.id}>
                    <span>{item.name}</span>
                    <button onClick={() => onDel(item.id)}>x</button>
                </li>
            ))
        }
      </ul>
    </div>
  )
}

export default ExitComponent

组件中的变量要是希望是全局的就拿到全局去写

其实 ( ) 就是 { }return

另外,onClick 中的函数若是直接写函数进去,默认是会给你调用的,你需要写在一个函数体中,这是因为 react 的 onClick 不是原生 js 的 onclick,会被读取一遍,因此会被一上来执行一下

同样的效果我们看下类组件会怎样写

import React, { Component } from 'react'

export default class ClassComponent extends Component {
  constructor () {
    super()
    this.list = [
        { id: 1, name: 'react' },
        { id: 2, name: 'vue' }
    ]
  }

  onDel (id) { // 相当于写到了原型上
    console.log(id);
  }

  render() {
    return (
      <div>
        <p>外部类组件</p>
        <ul>
            {
                this.list.map(item => (
                    <li key={item.id}>
                        <span>{item.name}</span>
                        <button onClick={() => this.onDel(item.id)}>x</button>
                    </li>
                ))
            }
        </ul>
      </div>
    )
  }
}

类组件中写参数需要写到 constructor 中,继承还需要在其中写 super 关键字

定义函数就在类中定义即可,无需 static 关键字,这样写就是写在类的原型上,拿到这个函数需要用 this

虽然目前 react 最新版本是函数式编程,但是实际开发中很多企业还是会写类编程,因此还是有学习的必要

我们拿到根组件 App.jsx 中使用,需要导入

import ExitComponent from './components/ExitComponent.jsx';

function App () {
    return (
        <div className="app">
            <ExitComponent />
        </div>
    )
}

export default App

组件的状态

状态就涉及到数据更新带动视图的更新,在 vue 中我们会使用 ref 或者 reactive,在 react 我们则是使用 setState

比如下面的这个例子,我希望点击按钮实现 ++ 的效果,这里用类展示

import React, { Component } from 'react'

export default class Counter extends Component {
  state = { // 这样写相当于在constructor中写this.state
    count: 0
  }

  // react中类编程,函数体中this默认会丢失,是undefined,因此改成箭头函数
  setCount = () => {
    console.log(this);
    this.setState({ // setState从Component类中继承过来的
        count: this.state.count + 1
    }) 
  }

  render() {
    return (
      <div>
        <button onClick={this.setCount}>计数器 -- {this.state.count}</button>
      </div>
    )
  }
}

在 class 写法中, constructor 定义的值可以直接拿到最前面去写,并且不需要使用 this,但是用的时候需要用 this,另外 react 中的 class 的 this 在函数中是会丢失的,因此里面的函数需要写成箭头函数,让 this 指向 class,这里的 setState 我们并没有写过,这说明这个 setState 是来自 Component 中继承而来的。

另外组件中的状态一定要命名为 state

demo:受控表单组件

下面简单写一个双向绑定的 input 框,不同于 vue,这里实现需要先将 state 值赋给 input 中的 value 值,然后利用 onChange 事件函数去调用 setState 改变 state 值,这就是 react 中的受控组件

import React, { Component } from 'react'

export default class InputComponent extends Component {
  state = {
    msg: ''
  }

  changeHandler = (e) => {
    console.log(e.target.value);
    this.setState({
      msg: e.target.value
    })
  }

  render() {
    return (
      <div>
        <input type="text" value={this.state.msg} onChange={this.changeHandler} />
      </div>
    )
  }
}

demo:非受控组件

import React, { Component, createRef } from 'react'

export default class InputComponent2 extends Component {
  msgRef = createRef() // 创建一个用于存放dom的容器

  changeHandler = () => {
    console.log(this.msgRef.current.value);
  }

  render() {
    return (
      <div>
        <input type="text" ref={this.msgRef} />
        <button onClick={this.changeHandler}>提交</button>
      </div>
    )
  }
}

非受控组件写法用的是 Ref,这点很像是 vue,都是用 ref 去拿到 dom 结构,然后 this.msgRef.current.value 就是输入框的值

区别

非受控组件直接拿到 dom 结构,受控组件用的是 react 中的 state,一句话解释就是

受控组件 input 框自己的状态被 react 组件状态所控制,非受控组件反之

组件通信

父子传参

类组件写法

父组件

import React, { Component } from 'react'
import CChild from './components/CChild'

export default class CApp extends Component {
  state = {
    msg: '父组件数据'
  }

  render() {
    return (
      <div>
        <h2>父组件</h2>
        <CChild msg={this.state.msg} />
      </div>
    )
  }
}

子组件

import React, { Component } from 'react'

export default class CChild extends Component {
  

  render() {
    return (
      <div>
        <h4>子组件</h4>
        <p>{this.props.msg}</p>
      </div>
    )
  }
}

父组件直接把参数给到子组件,= 左边的数据可以随意取,子组件通过 component 中的 props 来拿到父组件传递过来的参数

函数组件写法

函数式就用不了 this 了,react 将函数中的 this 都给默认改成 undefined

子组件

function CChild (props) {
  return (
    <div>
      <h4>子组件</h4>
      <p>{props.msg}</p>
    </div>
  )
}

export default CChild

父组件传递过来的参数就在函数形参 props

在 vue 中,官方不建议我们在子组件中修改父组件传递过来的值,不建议,但是也不会报错

react 更狠,父组件传递过来的值是只读的,若修改会报错

传递的数据类型除了 undefinednull 不能传递外,其余都可以传,甚至可以传递 jsx 语法

既然 react 中,不能修改来自父组件的值,我们就用父组件的函数来进行修改,这又涉及到子父传参

子父传参

子父传参是逆向的行为,需要一个事件驱动,这里我写一个按钮,点击事件来触发传参

父组件写一个函数传递给子组件,子组件拿到父组件传递过来的函数,给这个函数传递自己的参数

父组件

import React, { Component } from 'react'
import PChild from './components/PChild'

export default class PApp extends Component {
  
  callback = (newMsg) => {
    console.log('拿到子组件的数据' + newMsg);
  }

  render() {
    return (
      <div>
        <h2>父组件</h2>
        <PChild cb={this.callback} />
      </div>
    )
  }
}

子组件

import React, { Component } from 'react'

export default class PChild extends Component {
  state = {
    msg: '子组件的数据'
  }

  handler = () => {
    this.props.cb(this.state.msg)
  }

  render() {
    return (
      <div>
        <h4>子组件</h4>
        <button onClick={this.handler}>传递参数给父组件</button>
      </div>
    )
  }
}

函数式组件是一样的,区别就在于 props 前面加不加 this,若是类组件需要 this,函数式无需 this,props 就是形参

兄弟组件通讯

父作中间人

这里我用函数式编程来写,父组件,子组件A,子组件B,父组件传函数给到 A组件,A组件 传参给 父组件,父组件 把 A组件的参数给到 B组件

子组件A

const BrotherA = (props) => {
  const msg = 'A组件数据'

  const handler = () => {
    props.cb(msg)
  }

  return (
    <div>
      <h4 onClick={handler}>子组件A</h4>
    </div>
  )
}

export default BrotherA

子组件B

const BrotherB = (props) => {
  return (
    <div>
      <h4>子组件B--{props.msg}</h4>
    </div>
  )
}

export default BrotherB

父组件

import BrotherA from "./components/BrotherA"
import BrotherB from "./components/BrotherB"

const BApp = () => {

  let msg = ''

  const fn = (newMsg) => {
    console.log(newMsg);
    msg = newMsg
  }

  return (
    <div>
      <h2>父组件</h2>
      <BrotherA cb={fn} />
      <BrotherB msg={msg} />
    </div>
  )
}

export default BApp

这里如果你运行,发现实现不了,因为父组件想要改动 msg 需要响应式处理,但是函数式编程没有 this.setState 可供你访问

其实有方法,后面我们再讲,这里先改成 class 写法

在类编程中的任何方式在函数式编程中都有更优雅的方式去写

import React from 'react';
import BrotherA from "./components/BrotherA"
import BrotherB from "./components/BrotherB"

class BApp extends React.Component {
    state = {
        msg: ''
    }

    fn = (newMsg) => {
        console.log(newMsg);
        this.setState({msg: newMsg})
    }

    render () {
        return(
            <div>
              <h2>父组件</h2>
              <BrotherA cb={this.fn} />
              <BrotherB msg={this.state.msg} />
            </div >
          )
    }
}

export default BApp

其实 react 的兄弟组件通信只能这样写了,react 中没有 eventBuseventBus 在 vue3 中已经被移除了,为了这么点功能没必要引入第三方库。

但是涉及到嵌套比较深层的关系时,比如孙组件传值,就需要用到第三方仓库了

其实还可以用 provideinject 来实现组件通信,vue 中可以用这个方法实现祖先组件向后代组件传值,父传子,父传孙……,react 也是这样的

Provider ,Consumer 实现祖先向后代组件传值

react 中的叫法稍微变了一点,父组件提供 Provider ,子组件接受 Consumer 。这里仅展示父向子组件

其实 ProviderConsumer 都是一个组件

这两个组件需要从 React.createContext() 中解构出来,祖先组件和后代组件都需要这么干,祖先组件用的 Provider,后代组件用的 Consumer,双方都自行解构就不是同一个对象,因此需要另起一个文件 provider 专门写这两个方法

src/provider.js

import React from 'react';

const { Provider, Consumer } = React.createContext()

export {
    Provider, 
    Consumer
}

祖先组件

import React from 'react';
import BrotherB from "./components/BrotherB"

import { Provider } from './provider.js';

class BApp extends React.Component {
    state = {
        msg: 'hello'
    }

    render() {
        return (
            <Provider value={this.state.msg}>
                <div>
                    <h2>父组件</h2>
                    <BrotherB />
                </div >
            </Provider>
        )
    }
}

export default BApp

祖先组件写法上需要用 Providerhtml 包裹起来,然后把 value 传递给后代组件

后代组件

import React from "react"

import { Consumer } from "../provider"

const BrotherB = (props) => {
  return (
    <Consumer>
      {
        value => (
          <div>
            <h4>子组件B--{value}</h4>
          </div>
        )
      }
    </Consumer>
  )
}

export default BrotherB

后代组件写法上需要用 Consumer 包裹 html 代码,用上祖先组件传递过来的参数 value

这种方法只能实现从上往下传值,无法实现后代向祖先传值,同 vue 一样

props 参数类型限定

我们看一个情景,父组件给子组件一个数组,让子组件去展示

父组件

import List from "./components/List"

const App = () => {

  const colors = [
    { id: 1, name: '红色' },
    { id: 1, name: '蓝色' },
    { id: 1, name: '黄色' }
  ]

  return (
    <div>
      <h2>hello react</h2>
      <List colors={colors} />
    </div>
  )
}

export default App

子组件

const List = (props) => {

  const arr = props.colors
  const lists = arr.map((item, index) => <li key={index}>{item.name}</li>)

  return (
    <div>
      <ul>
            {lists}
      </ul>
    </div>
  )
}

export default List

假设这个组件是别人写好的,但是人家传递过来的参数可能写错了类型

比如这里父组件里面的这样写colors={100}

报如下错误

13.png

看错误说的是子组件的问题

这样子不知情的人很难找到问题所在,因此我们会给 props 做一个错误校验

需要安装依赖prop-types

这个时候你肯定就会觉得,react 官方怎么这种东西自己不考虑好,让第三方来做, vue 的 props 都做了类型校验,确实是这样的,vue 源码是比较多的

安装完依赖后,对子组件 List 进行使用

import PropTypes from "prop-types";

const List = (props) => {

  const arr = props.colors
  const lists = arr.map((item, index) => <li key={index}>{item.name}</li>)

  return (
    <div>
      <ul>
            {lists}
      </ul>
    </div>
  )
}

List.propTypes = { // 为组件添加校验规则
    colors: PropTypes.array
}

export default List

这个时候的报错就可以看出问题所在了

14.png

“期待的是 array,却给了我 number

因此我们可以用这个去帮我们限定父组件给我们传的类型

若是限定函数,则是如下写法

List.propTypes = { // 为组件添加校验规则
    fn: PropTypes.func
}

用的 func 这个关键字

我们还可以对一个参数限定为必传项,直接在后面写 isRequired

List.propTypes = { // 为组件添加校验规则
    fn: PropTypes.func.isRequired
}

假设我们希望父组件给子组件传的值是个对象,并且一定要包含 nameage 字段,类型也加上限定,则是如下写法

List.propTypes = { // 为组件添加校验规则
    obj: PropTypes.shape({
        name: PropTypes.string,
        age: PropTypes.number
    }).isRequired
}

这里 obj 必传,里面的 key 可以不传,若是 key 也想要必传,自行加上 isRequired

还有个默认值,跟 js 语法一致,自行解构出默认值在形参中

对了,方才提到的快捷方式 rfcp 创建组件,这个时候你就理解了

import React from 'react';
import PropTypes from 'prop-types';

const Page = ({page = 10}) => {
    return (
        <div>
            Props的默认值:{page}
        </div>
    );
};

Page.propTypes = {
    
};

export default Page;

这样写,props 的默认值就是 10

我们再看下类的写法

import React, { Component } from 'react'

export default class Page extends Component {
    static defaultProps = {
        page: 10
    }

    render() {
    return (
      <div>
        Props的默认值:{this.props.page}
      </div>
    )
  }
}

这种写法就没得函数式编程来得舒服,还需要额外定义 static defaultProps

生命周期

所谓生命周期就是代码被读取到的那一刻到页面渲染完成的历程

react 的生命周期相比较 vue 会显得复杂点

钩子链接:React lifecycle methods diagram (wojtekmaj.pl)

先看下常用的几个钩子,记住这张图

15.png

整个过程被分成了三部分:挂载时更新时卸载时

挂载时

挂载时有个 constructor,这不是类中的构造器吗,然后 render 不是类编程中的函数吗,下面简单写一个类组件

import React, { Component } from 'react'

export default class Life extends Component {

  constructor () {
    super()
    console.log('组件开始加载');
    this.name = 'Dolphin'
  }

  componentDidMount() {
    console.log('组件挂载完成');
  }

  render() {
    console.log('组件开始被编译');
    return (
      <div>
        {this.name}
      </div>
    )
  }
}

看下打印顺序

16.png

挂载时:先执行 constructor ,此时就是获得数据源,再执行 render 渲染,编译得到虚拟 dom,中间穿插了个更新 DOMrefs,最后执行 componentDidMount,这个 did 就是过去时,因此就是 vue 中的 onMounted,挂载完成。

更新 dom 不是生命周期,只是这个过程在这里发生

render 的目的是生成虚拟 dom,这个时候可以拿到 dom,但是值是空的,下面展示下,先引入 createRef 函数拿到 h4 的 dom,如下

import React, { Component, createRef } from 'react'

export default class Life extends Component {

  constructor () {
    super()
    console.log('组件开始加载', this.ref);
    this.name = 'Dolphin'
    this.ref = createRef() // 存放dom
  }

  componentDidMount() {
    console.log('组件挂载完成', this.ref);
  }

  render() {
    console.log('组件开始被编译', this.ref);
    return (
      <div>
        <h4 ref={this.ref}>{this.name}</h4>
      </div>
    )
  }
}

运行下看看

17.png

这就有点像是 vue 中的 beforeMount,均拿不到 dom

因此接口请求我们一般写在挂载完成 componentDidMount 中,因为有时候接口请求速度很快,会出现数据拿到了,但是 dom 还没渲染完成,若接口请求还想要操作 dom,就会失败,为了更好的操作 dom,请求我们写在挂载完成后

更新时

更新阶段的 New props 就是拿到父组件传递过来的值,setState 是自身数据源的改变,forceUpdate 也是数据更新的方法,这是强制更新,这三个都会再一次触发 render,渲染完后又是更新 dom,最后又是更新 componentDidUpdate

因此更新阶段的钩子就只有两个,一个 render,一个 componentDidUpdate

刚刚上面的🌰我改下,添加一个 setState,就是响应式更改数据源,那就一定会重新触发 rendercomponentDidUpdate

import React, { Component, createRef } from 'react'

export default class Life extends Component {

  constructor () {
    super()
    this.ref = createRef() // 存放dom
    this.state = {
        count: 1
    }
  }

  handleClick = () => {
    this.setState({
        count: this.state.count + 1
    })
  }

  componentDidUpdate() {
    console.log('组件更新完成');
  }

  render() {
    console.log('组件开始被编译', this.ref);
    return (
      <div>
        <h4 ref={this.ref} onClick={() => this.handleClick()}>{this.state.count}</h4>
      </div>
    )
  }
}

其实 react 中并没有说响应式这个概念,他就是原生 js,setState 是在数据更新时劫持了这个数据,setState 可以帮我们触发 render,更新视图,若不用 setState,数据是能照常更改,只是视图没有更新罢了

触发 render 操作的还有个 forceUpdate,这是强制更新,我们可以不用 setState 来改,如下

import React, { Component, createRef } from 'react'

export default class Life extends Component {

  constructor () {
    super()
    this.ref = createRef() 
    this.count = 1
  }

  handleClick = () => {
    this.count++
    this.forceUpdate() // 强制render重新调用
  }

  componentDidUpdate() {
    console.log('组件更新完成');
  }

  render() {
    console.log('组件开始渲染', this.ref);
    return (
      <div>
        <h4 ref={this.ref} onClick={() => this.handleClick()}>{this.count}</h4>
      </div>
    )
  }
}

在某些场景下,我们需要用 forceUpdate 来进行强制更新的

组件更新完成在 react 中只有一个 componentDidUpdate,而 vue 中就是 beforeUpdateupdated,其实 react 中的 render 就是充当了 beforeUpdate

卸载时

就一个钩子 componentWillUnmount,组件卸载时执行

componentWillUnmount () {
	console.log('组件即将卸载');
}

总结

挂载时

  1. constructor:类中的构造器充当了生命周期,此时拿到数据源
  2. render:相当于vue中的 beforeMount,还拿不到 dom
  3. componentDidMount:挂载完成,可以拿到 dom

更新时

  1. render:父组件传参 New props,修改数据源 setState,强制更新 forceUpdated 都会重新渲染 render
  2. componentDidUpdate:更新完成

卸载时

  1. componentWillUnmount:组件即将卸载时执行

不常用的生命周期函数

常用的就是上面五个生命周期函数 constructorrendercomponentDidMountcomponentDidUpdatecomponentWillUnmount,不常用的还有,如图,我把不常用的也展开来

18.png

static getDerivedStateFromProps()

这是个静态方法,因此这个生命周期函数只能在类组件中使用,这个方法比较冷门,它在render前调用,自行查看罕见用例

shouldComponentUpdate()

这个生命周期函数表示组件该不该更新,若里面返回 false,就不会给你更新视图,但是数据照常更新

import React, { Component, createRef } from 'react'

export default class Life extends Component {

  constructor () {
    console.log('组件开始加载');
    super()
    this.ref = createRef() // 存放dom
    this.state = {
        count: 1
    }
  }

  handleClick = () => {
    this.setState({
        count: this.state.count + 1
    })
    console.log(this.state.count);
  }

  shouldComponentUpdate() {
    return false
  }

  render() {
    console.log('组件开始渲染', this.ref);
    return (
      <div>
        <h4 ref={this.ref} onClick={() => this.handleClick()}>{this.state.count}</h4>
      </div>
    )
  }
}

打印下 count 的值,发现确实变化了,但是视图没有更新

getSnapshotBeforeUpdate()

这个函数返回的值会给到 componentDidUpdate,它在最近一个渲染输出后调用,也就是 render 完之后立马调用

这么看 react 的生命周期函数总共也就是 8 个,和 vue 的生命钩子数量差不多

Demo:todoList

好了,学到这里可以写一个 todolist 了,实现如下效果,可以试着自己实现,后面贴了代码最后比对下

1.gif

受控组件写法

这里我把 todoList 拆分成两个组件,上面的输入框是一个组件,另一个组件是下面的 ul 列表项,把列表组件导入输入框组件,那就相当于输入框是父组件,列表是子组件

父组件-输入框

import React, { Component } from 'react'
import TodoItem from './TodoItem'

export default class TodoList extends Component {

  render() {
    return (
      <div>
        <header>
            <input type="text" />
            <button>提交</button>
        </header>

        <section>
            <TodoItem />
        </section>
      </div>
    )
  }
}

子组件-列表

import React, { Component } from 'react'

export default class TodoItem extends Component {

  render() {
    return (
      <div>
        <ul>
        	<li></li>
        </ul>
      </div>
    )
  }
}

父组件中写入 list 数据,把其传给子组件展示,因此涉及一个父子传参,另外我需要拿到 input 框中的 value 值,给到 list 中,这里可以用受控组件来实现

拿到 input 框中的值需要绑定一个点击事件,将 valuepushlist 中,需要重新渲染,有三种方法可以导致重新 render,一个是父组件的 New props,这里显然不行,自身就是父组件,一个是 setState,可以,这需要将 list 赋值给 arr,然后 arr 进行一个 push 操作,最后将 arr 赋值回 list,当然也可以用 forceUpdate 进行视图更新

父组件-输入框

import React, { Component } from 'react'
import TodoItem from './TodoItem'

export default class TodoList extends Component {

  state = {
    list: ['html', 'css', 'js'],
    inputVal: ''
  }
 
   handleChange = (e) => {
    this.setState({
        inputVal: e.target.value
    })
    console.log(this.state.inputVal);
  }

  handleClick = () => {
  }

  render() {
    return (
      <div>
        <header>
            <input type="text" value={this.state.inputVal} onChange={this.handleChange} />
            <button onClick={this.handleClick}>提交</button>
        </header>

        <section>
            <TodoItem list={this.state.list} />
        </section>
      </div>
    )
  }
}

子组件-列表

import React, { Component } from 'react'

export default class TodoItem extends Component {

  render() {
    return (
      <div>
        <ul>
            {
                this.props.list.map((item, index) => {
                    return <li key={index}>{item} <button>x</button></li> 
                })
            }
        </ul>
      </div>
    )
  }
}

valuepushlist 中不能直接写 pushpush 的返回值不是数组,而是 push 完数组的长度,下面这种写法是行不通的

this.setState({
	list: this.state.list.push(this.state.inputVal)
})

当然我们可以用解构的方式去写,这样更优雅

this.setState({
    list: [...this.state.list, this.state.inputVal]
})

这是受控组件写法,下面看下非受控组件的写法

非受控组件写法

非受控写法就是通过引入 createRef 来创建一个容器来拿到 dom,拿到 inputdom 后可以通过 .current.value 拿到 value 值,写法更简洁

import React, { Component, createRef } from 'react'
import TodoItem from './TodoItem'

export default class TodoList extends Component {
  inputRef = createRef()

  state = {
    list: ['html', 'css', 'js'],
    inputVal: ''
  }

  handleClick = () => {
    console.log(this.inputRef.current.value);
    this.setState({
        list: [...this.state.list, this.inputRef.current.value]
    })
}

  render() {
    return (
      <div>
        <header>
            <input type="text" ref={this.inputRef} />
            <button onClick={this.handleClick}>提交</button>
        </header>

        <section>
            <TodoItem list={this.state.list} />
        </section>
      </div>
    )
  }
}

其实 vue 也可以这样写,人家也是通过 ref 拿到 dom,只不过人家封装了一个 v-model,完全不需要这样写,很优雅

父组件已经完成了,现在去实现子组件的删除功能

子组件的删除功能

子组件的删除功能,就是删掉父组件的 list,在react中,父组件的数据源仅仅是可读的

想要实现就是一个子父通信了,需要父组件传递一个函数给子组件,子组件将参数信息 index 给到父组件,父组件这个函数自己来实现删除功能

子组件写删除函数需要进行传参,也就是要写括号,写了括号就一定会自动调用,因此写在 onClick 中的箭头函数体中进行调用,这样才是引用效果

父组件

handleDel = (index) => {
    let arr = this.state.list
    arr.splice(index, 1)
    this.setState({
    	list: arr
    })
}
// ……
<TodoItem list={this.state.list} cb={this.handleDel} />

子组件

onDel = (index) => {
	this.props.cb(index)
}
// ……
return <li key={index}>{item} <button onClick={() => this.onDel(index)}>x</button></li> 

丢下代码

这里是受控的组件的写法

父组件-输入框

import React, { Component, createRef } from 'react'
import TodoItem from './TodoItem'

export default class TodoList extends Component {
  inputRef = createRef()

  state = {
    list: ['html', 'css', 'js'],
    inputVal: ''
  }

  handleChange = (e) => {
    this.setState({
        inputVal: e.target.value
    })
  }

  handleClick = () => {
    if (this.state.inputVal) {
        this.setState({
            list: [...this.state.list, this.state.inputVal],
            inputVal: ''
        })
    }
  }

  handleDel = (index) => {
    let arr = this.state.list
    arr.splice(index, 1)
    this.setState({
        list: arr
    })
  }

  render() {
    return (
      <div>
        <header>
            <input type="text" value={this.state.inputVal} onChange={this.handleChange} />
            <button onClick={this.handleClick}>提交</button>
        </header>

        <section>
            <TodoItem list={this.state.list} cb={this.handleDel} />
        </section>
      </div>
    )
  }
}

子组件-列表

import React, { Component } from 'react'

export default class TodoItem extends Component {
  onDel = (index) => {
    this.props.cb(index)
  }

  render() {
    return (
      <div>
        <ul>
            {
                this.props.list.map((item, index) => {
                    return <li key={index}>{item} <button onClick={() => this.onDel(index)}>x</button></li> 
                })
            }
        </ul>
      </div>
    )
  }
}

其实当你可以用一个框架实现一个 todoList 之后,你就可以用这个框架进行项目开发了

最后

react 不同于 vue,没有那么多新的 api,因此上手会比较快。react18 支持我们去写类组件,不过官方还是建议我们去写函数组件,这就像是 vue3 也支持选项式 API 写法,但是官方推荐我们去写组合式 API。接下来的 react 学习就会朝着 hooks 进阶,觉得本期不错,可以关注我,持续带你学习 react

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!