从Vue到React —— React 开发相关知识点

295 阅读16分钟

前言

前端项目是由一个个页面组成的,对于Vue来说,一个页面是由多个组件构成的,页面本身也是一个路由组件。对于React来说也是如此。Vue会提供一系列技术支持来完成一个组件的开发,可以从这一系列技术支持出发,去React中寻找对应的技术支持来入门React,比如React中如何开发组件的UI,React中如何使用组件,React中如何定义组件数据等等。

1、脚手架

UmiJS 这个脚手架和VueCli较类似,至少路由配置和Vue Router很类似。 用UmiJS搭建React工程步骤如下:

  • mkdir myapp && cd myapp
  • npm create @umijs/umi-app 创建React工程
  • npm install 安装依赖
  • 执行命令 npm run start 启动项目

myapp 目录工程结构如下:

├── package.json
├── .umirc.ts
├── .env
├── dist
├── mock
├── public
└── src
    ├── .umi
    ├── layouts/index.tsx
    ├── pages
        ├── index.less
        └── index.tsx
    └── app.ts

。umirc.ts 文件内容如下:

import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    { path: '/', component: '@/pages/index' },
  ],
  fastRefresh: {},
});

路由配置与 Vue Router 很相似

2、React 中开发组件

2.1 组件写法

ES6 的 class 形式

import React from 'react';
export default class HelloWorld extends React.Component {
    constructor(props){
        super(props);
    }
    
    render(){
        return (
            <div>hello world</div>
        )
    }
}

函数形式,称为函数组件:

export default function HelloWorld(){
    return (
        <div>hello world</div>
    )
}

**这里要注意函数名的首字母要大写

2.2 绑定 Class 和 Style

React中用 className 来绑定 class,用 style 来绑定 Style。其中 style 接受的值是一个对象,且用 {} 传入,而且对象的属性名只能用驼峰式来命名

类组件写法:


consturctor(props){
    super(props);
    this.state = {
        styleData: {color: 'red' , fontSize: '16px'} , 
        isHead: true , 
        className: 'title'
    }
}

render(){
    <div
        className={`${this.state.className} ${this.state.isHead ? 'head': ''}`}
        style={this.state.styleData} />
        hello world
    </div>
}

函数式组件

export default function HelloWorld(){
    const [styelData] = useState({color: 'red' , fontSize: '16px'});
    const [isHead] = useState(true );
    const [className] = useState('title');
    
    return (
        <div 
            className={`${className} ${isHead ? 'head': ''}`}
            style={styleData} />
            hello world;
        </div>
    )
}

在React中是使用 {} 给属性赋值变量,且 className 只接受字符串,不接受数组或者对象,可以用ES6的模板字符串来拼接变量生成字符串

3、React中使用组件

与Vue一样,需要 import 引入后直接使用 在React中组件的命名必须以大写字母开头,因为React会将以小写字母开头的组件视为原生 DOM 标签

4、React 中如何定义组件数据

在React中内部数据称为 state , 把参数数据称为 props

4.1 定义内部数据 state

this.state = {
    styleData: {} , 
    className: 'title' 
}
//或 
const [styleData] = useState({})
const [className] = useState()

推荐使用函数形式来开发React组件,可以使用 React Hook,来避免学习 class 中的 this 的指向问题,从而降低门槛。

4.2 参数数据 props

  • 类组件写法
class HelloWorld extends React.Component{
    constructor(props){
        super(props);
    }
}
HelloWorld.defaultProps = {
    title: 'hello world'
}

在类组件中的构造函数 constructor 接受 props 作为传入的参数数据集合,并调用 super(props) 把props传给 React.Component 构造函数,这样类组件才能接受参数数据集体 props,再用 this.props.title 来读取 title 参数数据的值,另外可以用 defaultProps 来定义 title 参数数据的默认值。

  • 函数组件写法
export default function HelloWorld(props){
    const {title = 'hello world'} = props ;
}

函数组件接收一个 props 作为传入组件参数数据的集合,利用ES6解构赋值的功能,来获取组件的参数数据,并可以给参数数据设置默认值。 注意:在Vue的 template 模板中使用 {{}} 来使用数据,而在React中是统一用 {} 来使用数据的

5、React中实现组件传递数据

  • 传递动态数据
<HelloWorld 
    style={styleData}
    isHead={isHead}
    className={className}
  • 传递字符串数据
<HelloWorld title="title" />
  • 传递数字类型数据
<HelloWorld num={1} />
  • 传递Boolean类型数据
<HelloWorld isHead={false} />
  • 传递数组类型数据
<HelloWorld className={['title' , 'head']} />
  • 传递对象类型的静态数据
<HelloWorld styleData={{color: 'red' , fontSize: '16px'}} />

6、React中监听DOM事件

  • 类组件写法
import React from 'react'
export default class Hello extends React.Component{
    constructor(props){
        super(props);
        this.click = this.click.bind(this);
    }
    click(){
    }
    
    render(){
        return (
            <div onClick={this.click} /></div>
        )
    }
}
  • 函数式组件写法
export default function Hello(){
    const click = () =>{
    }
    return (
        <div onClick={this.click} /></div>
    )
}

在 React 中用 onClick 来监听点击事件。onClick 是一个合成事件,在Vue中 @ 表示的是监听事件,但属性原生事件;React中的 onClick 为合成事件,表示React重写的事件。

** 监听组件的事件与上面监听DOM事件类似**

7、React中改变组件数据

React中的Props数据与Vue数据一样都是不可以修改的,因此只能修改 state 的数据;想要修改 props的数据只需要在父组件中修改,然后通过组件通讯传递给子组件。

  • 类组件写法
class Hello extends React.Component{
    constructor(props){
        super(props)
        this.state = {
            count: 0
        }
        this.click = this.click.bind(this);
    }
    
    click(){
        this.setState({
            count: 1
        })
    }
}

在 this.setState 中可以传递一个函数或一个对象,建议传递一个函数 (state , props) => {} , 函数可以接受内部数据 state 和 参数数据 props 作为参数,而且 state 和 props 只读无法修改,每次调用 this.setState 时读取到的 state 和 props 都是最新的,特别适用多次调用 this.setState 修改同一个 state 的场景。最后函数返回一个对象,对象的内容为要修改的 state。

在React中称内部数据为 state,使用 useState(param) 定义一个 state时,可以通过参数 param 设置 state 的默认值,其返回一个数组,数组的第一个值是 state ,数组的第二个值是改变 state 的函数,可以调用该函数来改变 state

另外用 useState 定义的数据是响应式的,若页面有使用数据,该数据改变后页面会重新渲染。

8、组件通讯

父往子组件传递数据与Vue一样,通过 props 进行传递

若想要实现子组件修改父组件的数据方法,只能通过父组件中传递方法到子组件,然后子组件调用父组件的方法进行修改与处理(其实与Vue中的 emit 类似)

  • 类组件写法
//父组件
class Index extends React.Component{
    constructor(props){
        super(props);
        this.click = this.clicl.bind(this);
        this.state = {
            title: 'titleInfo'
        }
    }
    
    click(data){
        this.setState(state => {
            return {
                title: data
            }
        })
    }
    
    render(){
        return (
            <Hello title={this.state.title} changeTitle={this.click} />
        )
    }
}
// 子组件
class Hello extends React.Component{
    constructor(props){
        super(props);
        this.changeClick = this.changeClick.bind(this);
    }
    changeClick(){
        this.props.changeTitle('data');
    }
    render(){
        return {
            <div onClick={this.changeClick}></div>
        }
    }
}
  • 函数式组件写法
// 父组件
function Index(){
    const [title , setTitle] = useState('title');
    const click = (data) => {
        setTitle(data);
    }
    return (
        <Hellow title={title} changeTitle={click} />
    )
}
// 子组件
function Hello(props){
    const { title = 'title' , changeTitle } = props;
    const handleChangeClick = () =>{
        changeTitle('data');
    }
    
    return (
        <div onClick={handleChangeClick}>{title}</div>
    )
}

9、React 中监听数据的变化

  • 类组件写法 在类组件中用 componentDidUpdate 这个生命周期方法来实现,该方法首次渲染时不会执行,在后续 propsstate 改变时会触发,其接受参数分别为:prevPropsprevState 分别代表改变前的 props 和改变前的 state.

  • 函数式写法 在函数式组件中,可以使用 useEffect 这个 Hook 来监听数据变化,但无法像类组件写法中获取改变前的数据。因此需要自定义 Hook 函数(useWatch)。如下:

如何获取改变前的旧数据,可以在第一次数据改变时触发 useWatch 时用一个容器把旧数据存储起来,下次再触发 useWatch 时通过读取容器中的值就可以获取改变前的旧数据。容器可以用 useRef 这个 Hook 来创建。

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

import {useEffect , useRef} from 'react'
export function useWatch(value , callback){
    const oldValue = useRef();
    useEffect( () => {
        callback( value , oldValue.current ) ;
        oldValue.current = value;
    } , [value] )
}

但是 useEffect 会在组件初次渲染后就会调用一次,导致 callback 回调函数会被执行一次,另外在 Vue 中可以用 immediate 配置来控制在组件初次渲染后马上执行 callback 回调函数,且默认不会在组件初次渲染后执行 callback ,同时,Vue 的 watch 还返回一个 unwatch 函数,调用 unwatch 函数可以停止监听该数据;因此完整代码如下:

import {useEffect , useRef} from 'react'
export function useWatch(value , callback , config = {immediate: false}){
    const oldValue = useRef();
    const isInit = useRef(false);
    const isWatch = useRef(ture);
    useEffect( () => {
        if(isWatch.current){
            if(!isInit.current){
                isInit.current = true ; 
                if(config.immediate){
                    callback(value , oldValue.current);
                }
            }else{
                callback(value , oldValue.current)
            }
            oldValue.current = value;
        }
    } , [value] )
    const unwatch = () =>{
        isWatch.current = false ;
    }
    return unwatch ; 
}

10、React中父组件调用子组件的方法

实现方法与 Vue3 中的实现方式基本一致

  • 类组件方法:使用 React.createRef() 方法
//父组件
class Index extends React.Component{
    constructor(props){
        super(props);
        this.myCom = React.createRef();
        this.click = this.click.bind(this);
    }
    click(){
        this.myCom.current.handle();
    }
    render() {
        return (
            <Hello ref={this.myCom} />
            <div onClick={this.click}> Hello World </div>
        )
    }
}

//子组件
class Hello extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            title: 'hello'
        }
    }
    handle(){
        this.setState(state => {
            title: 'myCom'
        })
    }
    render() {
        return (
            <div> {title} </div>
        )
    }
}
  • 函数式组件写法

useRef() 无法使用在函数组件上使用,在函数组件中要先使用 useImperativeHandle 定义要暴露给父组件的实例值,另外要把函数组件传入 forwardRef 处理后再导出

//父组件
function Index(){
    const myCom = useRef();
    changeTitle = () => {
        myCom.current.handleChangeTitle();
    }
    return (
        <Hello ref={myCom} />
        <button onClick={changeTitle}>改变标题</button>
    )
}
//子组件
function Hello(props , ref){
    const [title , setTitle] = useState('hello world');
    useImperativeHandle(ref , () => ({
        handleChangeTitle: ()=>{
            setTitle('two')
        }
    }))
    
    return (
        <div></div>
    )
}

11、组件插槽

  • 普通插槽 可以直接通过 this.props.children获取;实现方式分类组件与函数式组件,区别无外乎是获取 props 内容的区别

  • 具名插槽

可以通过 props 给子组件传递一个函数,如果这个函数最后返回 React 元素,其 React 元素是用 JSX 语法编写的,这样就间接实现具名插槽的功能。

  • 类组件写法
//父组件
import React from 'react';
import HelloWorld from './HelloWorld.js';
class Index extends React.Component{
  constructor(props) {
    super(props);
  }
  info(){
    return(
      <span>hello World</span>
    )
  }
  render() {
    return (
      <HelloWorld element={this.info}></HelloWorld>
    )
  }
}
export default Index;
// 子组件
import React from 'react';
class HelloWorld extends React.Component{
  constructor(props){
    super(props)
    this.elementSlot = "";
    if (this.props.element) {
      this.elementSlot = this.props.element();
    }
  }
  render(){
    return (
      <div>
        {this.elementSlot}
      </div>
    );
  }
}
export default HelloWorld;

  • 函数组件写法
//父组件
import HelloWorld from './HelloWorld.js';

export default function Index(){
  const info = () =>{
    return (
      <span>hello World</span>
    )
  }
  return (
    <HelloWorld element={info}></HelloWorld>
  )
}
// 子组件
export default function HelloWorld(props) {
  const { children, element } = props;
  let elementSlot = "";
  if (element) {
    elementSlot = element();
  }
  return (
    <div>
      {elementSlot}
    </div>
  );
}

12、React 中如何实现 v-model双向数据绑定

在普通受控组件中是通过 value 与 React 的合成事件 onChange 来实现的

在自定义的通用件中是通过父子组件通信以及子组件调用父组件方法的形式实现的。

13、React 中的 v-show

这个不用多说,在进行 render(或 return)时指定组件的样式即可

14、React 中的 v-if 与

通过 JS 的 if与else 进行不同组件的返回处理

15、React 中的 v-for

是通过数据的 map 函数来实现的,但是要注意在 map 方法中的元素需要设置 key 属性,否则会引起警告错误。

16、生命周期

在Vue组件的生命周期分为创造、挂载、更新、销毁四大阶段,并在生命周期每个阶段的前后会触发各自的钩子函数。

而React组件的生命周期分为挂载、更新和卸载阶段,了解生命周期最主要的是弄清楚每个生命周期的阶段会触发哪些钩子函数。

1、React 组件挂载阶段

在React组件挂载阶段会按顺序调用 constructor / getDerivedStateFromProps / componentDidMount / render 这些钩子函数

1.1 constructor

constructor 其实是 React.Component 子类的构造函数,在其中我们一般做三件事情。

  • 在其他语句之前调用 super(props) ,否则 this.props 为 undefined
  • 通过给 this.state 赋值对象来初始化 state ;
  • 为事件处理函数绑定实例,否则函数中无法使用 this ;

此外需要注意两点:

  • 不能使用 this.setState 来初始化内部 state
  • 不能将 props 直接赋值给 state ,然后使用 state , 而不直接使用 props,这样做当props更新时对应的state不会更新

1.2 getDerivedStateFromProps

这是一个不常用的钩子函数,其作用是用组件的 props 来派生出一个新的 state 。getDerivedStateFromProps 钩子函数接收组件的 props 和 state 作为参数,函数最后返回一个对象或者null,若返回一个对象,则用这个对象来更新 state ,若返回 null 则不更新 state

使用 getDerivedStateFromProps 钩子函数时要注意以下三点:

  • 要派生出新的 state,不要修改原来的 state
  • 函数最后必须返回一个对象或者 null
  • 钩子函数中无法使用 this。

1.3 render

render 函数应该为纯函数,在其中不应该去修改 state 和 props 。在用JSX写React元素时,通过 state 和 props 来给 React 元素绑定数据。最后必须返回一些 React 元素,且这些React元素必须只有一个根元素。若不想在DOM中额外增加一个无用的标签,可以使用 <React.Fragment> (或 <> )作为根元素

1.4 componentDidMount

componentDidMount 会在组件挂载后(插入 DOM 树中)立即调用。

我们一般可以做以下操作:

  • 获取 DOM 元素
  • 请求服务端数据
  • 监听事件,必须在 compoentWillUnMount() 中取消监听
  • 可以调用 this.setState() 来改变 state 数据

2、React 组件更新阶段

React 组件更新阶段会按顺序调用:getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate 这些钩子函数。

React 中有三个操作会引起组件更新:

  • 组件 props 变化
  • 执行 this.setState()
  • 执行 this.forceUpdate():强制让组件重新渲染;执行它会引起组件更新,会路过 shouldComponentUpdate 钩子函数。但其子组件会触发正常的生命周期钩子函数,包括 shouldComponentUpdate 钩子函数

2.1 getDerivedStateFromProps

这个钩子函数在组件挂载和更新阶段都会被调用,且函数接收的 state 和 props 都是平板后的。

那么在其派生出来的 state ,完全受 props 控制,即使用 this.setState() 改变也不起作用

2.2 shouldComponentUpdate

shouldComponentUpdate 钩子函数接收更新之后的 state 和 props ,通过和更新前的 state 和 props 对比,来判断是否更新组件,如果函数最后返回 true 则更新组件,反之;一般用于性能优化

使用 shouldComponentUpdate 钩子函数需要注意以下三点:

  • 在组件中执行 this.forceUpdate() 触发组件更新,则不会执行该钩子函数
  • 在其中执行 this.setState() 时,必须在一个条件语句中,否则会陷入死循环,导致程序崩溃
  • 函数必须返回 true 或 false ,若返回 false,后续 render、 getSnapshotBeforeUpdate , componentDidUpdate 钩子函数不再被调用.

2.3 render

不解释了

2.4 getSnapshotBeforeUpdate

这个钩子函数相当于Vue中的 beforeUpdate 钩子函数; 此函数调用时,props和state已经更新了,因此它接收更新前的props和state作为参数,作为比较使用;

此钩子函数最后返回一个值,该值会被 componentDidUpdate 钩子函数的第三个参数 snapshot 接收;

此钩子函数在组件重新渲染后挂载到 DOM 之前被调用,故在此函数中获取到DOM还是更新前的DOM,一般用组件UI更新前后的交互操作

使用此函数需要注意以下三点:

  • 在其中执行 this.forceUpdate 或 this.setState() 时,必须在一个条件语句中,否则会陷入死循环
  • 函数最后必须返回一个值或null,否则代码会报错
  • 必须和 componentDidUpdate 钩子函数一起调用,否则会报错

2.5 componentDidUpdate

此钩子函数是在组件重新渲染后并挂载到DOM中后才执行的,函数参数接收更新前的 state 和 props ,还用 snapshot 参数接收 getSnapshotBeforeUpdate 钩子函数返回的值

使用此函数时要注意以下两点:

  • 在其中执行 this.forceUpdate 或 this.setState() 时,必须在一个条件语句中,否则会陷入死循环
  • 如果 shouldComponentUpdate 钩子函数返回值为 false,则不会调用此函数

3、React 组件卸载阶段

3.1 componentWillUnMount

在组件销毁之前调用,一般处理以下事项:

  • 清除定时器
  • 取消网络请求
  • 解绑在 componentDidMount 钩子函数中监听事件

17、React 组件更新的优化

React 中父组件更新,不管子组件的 state 和 props 是否发生变化,都会被迫更新。React 这种更新机制可能导致性能问题,可以用 React.PureComponent 来创建那种更新计算开锁很大的子组件来优化性能。

React.PureComponent 会创建一个自行调用 shouldComponentUpdate 钩子函数组件,故在此组件中不能再次调用 shouldComponentUpdate 钩子函数。

在 shouldComponentUpdate 钩子函数中自动浅层对比 props 和 state ,若数据有变化,返回 true,触发组件更新。

「浅层对比」:只对比到 this.props 的属性值的一层,比如属性值是数组或对象时不去对比里面的嵌套数据。换句话说,只要该属性值引用地址不改变,就认为该属性值未改变。数组和对象都是引用类型。

那么用 React.PureComponent 来创建的子组件在父组件中,只有传递给子组件的 props 经过浅层对比后发现在改变,才会触发子组件的更新,避免父组件数据变动时子组件也被迫一起更新,从而优化了性能。

另外,可以调用 this.forceUpdate 强制触发子组件的更新。

18、React函数组件的生命周期

在函数式组件中无法使用上述文档中的钩子函数,因此可以使用 React Hook 模拟这些钩子函数。

18.1 constructor

通过解构赋值与 useState 来模拟 constructor ,这个就不解释了

假如state的初始值需要经过一系列复杂的计算才能得到,可以传一个函数给 useState

const [amount , setAmount] = useState( ()=>{
    return getAmount(num);
} )

state 的初始值在组件挂载时才会用到,为了避免组件更新时再次调用 getAmount 函数获取 amount 初始值,故把 getAmount 函数包裹在 () => { retur getAmount(num) } 中,再传递给 useState

18.2 getDerivedStateFromProps

18.3 render

不解释了

18.4 componentDidMount

用 useEffect 来模拟。因为 componentDidMount 钩子函数在组件的生命周期中只执行一次,那么要执行只运行一次的 useEffect (仅在组件挂载时执行),可以传递一个空数组作为第二个参数给 useEffect ,而第一个参数接收一个函数,在函数中执行和 componentDidMount 钩子函数中执行的事项。

18.5 shouldComponentUpdate

18.5.1 React.memo 模拟

React.memo 包裹一个组件来对它的 props 进行比较,且这个组件只能是函数组件。当组件的 props 发生变化才会去更新这个组件

React.memo 仅检查包裹的组件的 props 变更 ,不比较组件的state,若组件的 state 发生变更 ,无法阻止组件更新。

const Hello = React.memo( (props)=>{
   
} )

其中对 props 的比较只是浅比较,如果要进行深比较,可以自定义的比较函数通过第二个参数传递 React.memo 实现,比较函数接收变化前后的 props 作为参数:

import React, {useEffect } from 'react';
const HelloWorld = React.memo((props) =>{
  const {num} = props;
  useEffect(() =>{
    console.log('更新')
  })
  return(
    <div>{num}</div>
  )
},(prevProps, nextProps) =>{
  //更新
  return false
  //不更新
  return true
})
export default HelloWorld;

当比较函数返回 true 是不更新,返回 false 时则更新

18.5.2 用 useMemo 模拟

useMemo(() => fn, deps)

useMemo 的第一个参数是个函数,第二个参数是数组,里面是要监听的数据,当数据发生改变时,重新执行第一个参数;另外 useMemo 返回的是第一个参数执行后返回的值。

若在第一个参数函数中返回了一个组件,那这个组件的更新由第二个参数数组中的数据来控制,这样也间接实现了 shouldComponentUpdate 钩子函数。

但由于 useMemo 中不能使用 useState 定义组件的 state ,所以还是只能比较 props 来决定是否更新组件。

import React from 'react';
const HelloWorld = (props) =>{
  const {num} = props;
  return (
    <div>{num}</div>
  )
}
export default HelloWorld;


import React,{useMemo,useState} from 'react';
import HelloWorld from './HelloWorld';
const Index = (props) =>{
  const [num,setNum] = useState(1)
  const Hello = useMemo(()=>{
    return (
      <HelloWorld num={num}></HelloWorld>
    )
  },[num])

  return (
    <div>
      {Hello}
    </div>
  )
}
export default Index;

上面的 HelloWorld 组件,只有在 num 这个 prop 改变时才会更新。

使用 useMemo 包裹一个组件时,组件中用到的 props ,一定添加到第二个参数数组中,否则当 props 更新了,组件中 props 还是旧数据。

18.5.3 用 useCallbacl 模拟

useCallback和useMemo的区别是。useCallback返回的是一个函数,useMemo返回的是一个值。

useCallback的第一个参数是个函数,useCallback返回的就是这个函数。第二参数是数组,里面是要监听的数据,当数据发生改变时,才会重新返回第一参数。

若在第一参数函数是一个函数组件,那这个组件的更新由第二参数数组中的数据来控制,这样也间接实现了shouldComponentUpdate钩子函数。

import React from 'react';
const HelloWorld = (props) =>{
  const {num} = props;
  return (
    <div>{num}</div>
  )
}
export default HelloWorld;


import React,{useCallback,useState} from 'react';
import HelloWorld from './HelloWorld';
const Index = (props) =>{
  const [num,setNum] = useState(1)
  const Hello = useCallback(()=>{
    return (
      <HelloWorld num={num}></HelloWorld>
    )
  },[num])

  return (
    <div>
      {Hello()}
    </div>
  )
}
export default Index;

上述分别用useMemo和useCallback来处理HelloWorld组件,返回Hello。唯一不同的是用useCallback处理时,要这样{Hello()}使用HelloWorld组件。 useCallback(fn, deps)相当于useMemo(() => fn, deps)。

使用useCallback包裹一个组件时,组件中有用到的props,一定添加到第二参数的数组中,否则当props更新了,组件中props还是旧数据。

19、模拟componentDidUpdate

用useEffect来模拟,useEffect第一参数接受一个函数,第二参数是个数组,把要监听的props或state添加进去。当监听的props或state发生变化时,useEffect的第一参数就会执行。可以把componentDidUpdate钩子函数中要执行的代码,放在useEffect的第一参数函数中执行。

import React,{useState} from 'react';
const HelloWorld = (props) =>{
  const [num,setNum]=useState(1);
  const changNum = () =>{
      setNum(2);
  }
  useEffect(() =>{
    console.log('num数据发生变化')
  },[num])
  return (
    <div onClick={changNum}>{num}</div>
  )
}
export default HelloWorld;

20、模拟componentWillUnmount

与 #19 中的信息一致

21、组件间的通讯

React 组件之间的关系可以分为 父子关系、跨级关系、非嵌套关系。

21.1 父子关系

  • 将父组件的state赋值给子组件的props实现父组件向子组件的通讯。
  • 将父组件中改变自己数据的函数赋值给子组件的props,在子组件中调用该函数实现子组件向父组件的通讯

21.2 跨级关系

在Vue可以使用 provide / inject 来实现 在React中也有类似的功能叫 Context。

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

先用 React.createContext 创建一个 context ,然后用 Context.Provider 给祖先组件注入一些数据。然后在后代组件中用 Class.contextType 或者 Context.Consumer 来获取这些数据。

  • 类组件的写法

先在 context.js 中创建一个 Context

import React from 'react'   
export const MyContext = React.createContext(
    //在这里可以设置一些默认值
    {
        title: 'hello world'
    }
)

在祖先组件中引用 MyContext ,然后用 MyContext.Provider 包裹子组件Father,并把 「值」 赋值给MyContext.Proivder 上的属性 value ,相当于注入数据

import React from 'react';
import Father from './Father.js';
import { MyContext } from './context.js';
export default class Grandfather extends React.Component {
  constructor(){
    super();
    this.state={
      info:{
        title:'hello react'
      }
    }
  }
  render() {
    return (
      <MyContext.Provider value={this.state.info}>
        <Father>
        </Father>
      </MyContext.Provider>
    )
  }
}

//在Father组件中引入子组件 Son
import React from 'react';
import Son from './Son.js';
export default class Father extends React.Component{
  render(){
    return(
      <Son></Son>
    )
  }
}

此时,组件Son就是组件Grandfather的后代组件。可以看到组件Father并没有通过props从祖先组件Grandfather中获取数据info,再通过props传递给组件Son,那么在组件Son中怎么才能获取到数据info呢?

在类组件中,先把MyContext赋值到类组件中的静态属性contextType,然后用this.context来获取该组件的上级组件中最近一个被MyContext.Provider包裹的组件中传递给MyContext.Provider的属性value的数据。

import React from 'react';
import { MyContext } from './context.js';
export default class Son extends React.Component {
  static contextType = MyContext;
  render() {
    return (
      <div>
        {this.context.title}
      </div>
    )
  }
}
  • 函数式组件

在函数组件中,用MyContext.Consumer包裹一个函数式组件,该函数式组件的参数,可以接收该组件的上级组件中最近一个被MyContext.Provider包裹的组件中传递给MyContext.Provider的属性value的数据。

import React from 'react';
import { MyContext } from './context.js';
export default class Son extends React.Component {
  static contextType = MyContext;
  render() {
    return (
      <MyContext.Consumer>
        {value => (<div>{value.title}</div>)}
      </MyContext.Consumer>
    )
  }
}

还记得用React.createContext创建Context,可以接受一些数据,作为默认数据。如果使用Class.contextType或者Context.Consumer的组件的上级组件中没有一个被MyContext.Provider包裹,那么this.context和value可以获取到这些默认数据。

21.3 非嵌套关系的React组件的通讯

利用共同的组件,不解释

21.4 Event Bus

React 中没有提供类似Vue中的 Vm.onVm.on Vm.off Vm.$emit 等API,但可以利用 Node.js 提供的 event.js 来实现。

首先执行 npm install events -S 下载安装。

再创建一个 EventBus.js ,引入 event.js,并创建 EventEmitter 实例。

import { EventEmitter } from "events";
export default new EventEmitter();

EventEmitter实例提供了类似vm.onvm.on、vm.off、vm.$emit的实例方法,

emitter.on(eventName, listener)对应vm.$on( event, callback );

emitter.off(eventName, listener)对应vm.$off( [event, callback] );

emitter.emit(eventName[, ...args])对应vm.$emit( eventName, […args] );

其中eventName是要监听事件的名称,listener被监听事件的回调函数,...args传递给监听事件的回调的参数。

那么,例如两个非嵌套关系的组件A和组件B,也可以通过events.js来实现通讯,可以在组件A的挂载阶段用emitter.on绑定要监听事件,在组件的卸载阶段用emitter.off解绑要监听事件,在组件B用emitter.emit触发被监听事件,把emitter.emit第二参数开始的参数,当作参数传递给emitter.on绑定的监听事件的回调函数,即实现组件A中接收到组件B向组件A发起通讯传递数据的功能。

函数组件的实现:

组件A

import React,{useEffect} from 'react';
import emitter from "./ev"
const A = () =>{
  const handleChange = (param1,param2)=>{
    console.log(param1);
    console.log(param2);
  }
  useEffect(() =>{
    emitter.on('chang',handleChange);
    return () =>{
      emitter.off('chang',handleChange);
    }
  },[])
  return(
    <div>A组件</div>
  )
}
export default A;

组件B:

import React from 'react';
import emitter from "./ev"
const B = () =>{
  const handleChange = () =>{
    emitter.emit('chang','参数1','参数2')
  }
  return(
    <div onClick={handleChange}>B组件</div>
  )
}
export default B;

React全家桶配置参考地址:

React全家桶参考地址