阅读 459

React入门

前言

简介一些以往项目使用react相关的知识点,及个人对react简单的看法。此文若有不对之处还望指出。
文章的最后是参考相关文档的链接。

react基础知识点

jsx/tsx

jsx中存在的特殊写法

  • 元素的类名:className
<span className='span_box' />
//原因:class是Javascript的保留关键字
复制代码
  • 属性规定 label 与哪个表单元素绑定:htmlFor
//jsx写法
<form>
  <label htmlFor="male">Male</label>
  <input type="radio" name="sex" id="male" />
</form>

//传统html写法
<form>
  <label for="male">Male</label>
  <input type="radio" name="sex" id="male" />
</form>
复制代码
  • 属性规定表单提交时使用的字符编码:acceptCharset
//jsx写法
<form action="form.html" acceptCharset="ISO-8859-1">
  First name: <input type="text" name="fname"><br>
  Last name: <input type="text" name="lname"><br>
  <input type="submit" value="提交">
</form>

//传统html写法
<form action="form.html" accept-charset="ISO-8859-1">
  First name: <input type="text" name="fname"><br>
  Last name: <input type="text" name="lname"><br>
  <input type="submit" value="提交">
</form>
复制代码
  • 属性提供了content属性的信息/值的HTTP头:httpEquiv
//jsx写法
<meta httpEquiv="X-UA-Compatible" content="ie=edge" />

//传统html写法
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
复制代码

节点条件渲染

interface IProps{
	show: boolean
    [key: string]: any
}

const App: React.FC = (props: IProps) => {
  const { show } = props
  
  return (
    <div className="App">
      {
      	show ? <div>展示内容</div> : <div>隐藏内容</div>
      }
    </div>
  );
  
export default App;

复制代码

虚拟dom

  • 虚拟dom的样子
//react的虚拟dom
const reactVNode = {
	key: null,
    props: {
    	children: [	//子元素
        	{ type: 'span', /*...*/ },
            { type: 'span', /*...*/ }
        ],
        className: 'kisure', //标签上的属性
        onClick: () => {}	//事件
    },
    ref: null,	
    type: 'div'	//标签名或者组件名
    // ...
}
//vue虚拟dom
const vueVnode = {
	tag: 'div', //标签名或者组件名
    data: {
    	class: 'kisure', //标签上的属性
        on: {
        	click: () => {} //事件
        }
    },
    children: [
    	{ tag: 'span', /*...*/ },
        { tag: 'span', /*...*/ }
    ]
    // ...
}
复制代码
  • 虚拟dom的产生
// react jsx
<div className='kisure' onClick={() => {}}>
	<span />
    <span />
</div>
// vue template
<template>
	<div class='kisure' @click="fn">
		<span />
        <span />
	</div>
</template>
复制代码
// react创建虚拟dom 通过babel转化为createElement形式
React.createElement('div', { className: 'kisure', onClick: () => {} }, [
	React.createElement('span', { /*...*/ }),
    React.createElement('span', { /*...*/ })
])
// vue创建虚拟dom(只能再render函数中得到h) 通过vue-loader转化为h形式
h('div', {
	class: 'kisure',
    on: {
    	click: () => {}
    }
}, [ h('span', { /*...*/}) ], h('span', { /*...*/}) ])
复制代码
  • 虚拟dom的优点

(1)减少 DOM 的操作次数和范围:

次数:虚拟 DOM 可以将多次操作合并为一次操作。(例如:在 react 中只需要通过一个数组一次性渲染列表,又比 如:修改了好个节点的值或者属性,react 可以一次性的修改)。换作传统操作 DOM 的方式,如果想要修改几百个节点,需要手动的去修改几百次。
范围:虚拟 DOM 借助 Diff,可以把多余的操作省略掉。(例如:页面中存在上千个列表节点,但是我们想再添加10个节点,如果使用**innerHTML**的方式就会重复渲染。而 react 通过对比知道有 10个是新增的,最终只会新增10个节点)
(2)跨平台:
本质上虚拟 DOM 是一个 js 对象,既然是对象,那么就可以转换 dom 元素,转化为安卓 view 视图等等

  • 虚拟dom的缺点

需要额外的创建函数(React.createElementh),当然为了提高书写方便,可以使用 react 的 jsx 语

法或者 vue 借助**vue-loader**进行转换

state和props

  • state

state是状态,用state来完成对行为的控制、数据的更新、界面的渲染,由于组件不能修改传入的props,所以需要记录自身的数据变化**state**包含的所有属性代表的就是组件UI的变化

//关于state的案例
class Parent extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	count: 1
        }
    }
    
    private addCount() {
    	this.setState((prevState) => {
        	count: prevState.count + 1
        })
    }
    
    render() {
    	return <div>
          	<p>count的值为:{this.state.count}</p>
            <button onClick={() => this.addCount()}>count++</button>
        </div>
    }
}
复制代码
  • props
//react单向数据流值说的就是props,也可用于组件的通讯

//父组件
class Parent extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	count: 1
        }
    }
    
    private addCount() {/*...*/}
    
    render() {
    	const childProps: IChildProps = {
        	emitFunc: this.addCount,
            count: this.state.count
        }
    	return <div>
          	<Child {...childProps}/>
        </div>
    }
}

//子组件
class Child extends React.Component<IChildProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	num: 2
        }
    }
    
    render() {
    	return <Child>
        	<p>父组件传递过来的属性count:{ this.props.count }</p>
            <button onClick={() => this.props.emitFunc()}>count++</button>
        </Child>
    }
}
复制代码
  • state和props的区别

(1)数据来源:**propsstate都是 js 对象,但是props来源于父组件(类似于函数的形参),state**是组件本身所有的。

(2)是否可修改:**props是不能修改的,state也不能直接修改,但可以通过setState**方法来修改的。

生命周期

新版生命周期

  • 执行顺序

(1)组件创建

constructor -> getDerivedStateFromProps -> render -> componentDidMount

(2)组件更新

getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

(3)组件卸载

componentWillUnmount

  • getDerivedStateFromProps

(1)static getDerivedStateFromProps(nextProps,prevState):接收父组件传递过来的**props和组件之前的状态,返回一个对象来更新state或者返回null来表示接收到的props没有变化,不需要更新state**。


> (2)作用:将父组件传递过来的**`props`**映射到子组件的**`state`**上,组件内部就不用再通过**`this.props.xxx`**获取属性值了,统一通过**`this.state.xxx`**获取。映射就相当于拷贝了一份父组件传过 来的**`props`**,作为子组件自己的状态。

> (3)注意:子组件通过**`setState`**更新自身状态时,不会改变父组件的**`props`**。

> (4)生命周期触发时机:
在 React 16.3.0 版本中:在组件实例化、接收到新的**`props`**时会被调用
在 React 16.4.0 版本以后:在组件实例化、接收到新的**`props`**、组件状态更新时会被调用

> (5)配合**`componentDidUpdate`**,可以覆盖**`componentWillReceiveProps`**的所有用法

> (6)为什么是静态的方法?(TODO)
//getDerivedStateFromProps案例
//父组件
class Parent extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	count: 1
        }
    }
    
    private addCount() {
    	this.setState((prevState) => {
        	count: prevState.count + 1
        })
    }
    
    render() {
    	return <div>
        	<button onClick={() => this.addCount()}>count++</button>
          	<Child count={this.state.count}/>
        </div>
    }
}

//子组件
class Child extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	num: 2,
            count: null
        }
    }
    
    static getDerivedStateFromProps(nextProps,prevState) {
    	if (nextProps.count > prevState.num) {
        	return {
            	count: nextProps.count
            }
        }
        
        return null
    }
    
    render() {
    	return <Child>
        	child demo
        </Child>
    }
}

//父级组件,点击按钮增加count状态值,当状态值比子组件的num大的时候,则映射子组件的count状态
复制代码
  • getSnapshotBeforeUpdate

(1)接收父组件传递过来的**props**和组件之前的状态,此生命周期钩子必须有返回值,返回值将作为第三个参数

传递给**componentDidUpdate。必须和componentDidUpdate**一起使用,否则会报错

(2)该生命周期钩子触发的时机:被调用于**render**之后、更新 DOM 和 refs 之前


> (3)该生命周期钩子的作用:它能让你在组件更新 DOM 和 refs 之前,从 DOM 中捕获一些信息 (例如滚动位置)

> (4)配合**`componentDidUpdate`**, 可以覆盖**`componentWillUpdate`**的所有用法

> (5)替代**`componentWillUpdate`**的理由: 在 React 开启异步渲染模式后,在**`render`**阶段读取到的 DOM 元素状态并不总是和**`commit`**阶段相同,这就导致在**`componentWillUpdate`**中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。 而**`getSnapshotBeforeUpdate`**会在最终的**`render`**之前被调用,也就是说在**`getSnapshotBeforeUpdate`**中读取到的 DOM 元素状态是可以保证与**`componentDidUpdate`**中一致的。

旧版生命周期

  • 执行顺序

(1)组件创建

constructor -> componentWillMount -> componentDidMount -> componentWillUnmount

(2)组件更新
componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> componentDidUpdate -> render

(3)组件卸载
componentWillUnmount

生命周期简述

  • componentWillMount

在渲染过程中可能会执行多次

  • componentDidMount
// 在渲染过程中永远只有执行一次,一般是在componentDidMount执行副作用,进行异步操作
componentDidMount() {
	fetch(/*url...*/).then(res => res.json()).then(users => {
    	this.setState(() => {
        	return {
            	users
            }
        })
    })
}
复制代码
  • shouldComponentUpdate

(1)用于性能的优化,返回true代表组件可以更新,返回false则组件不更新。


(2)父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的因此需要在子组件的该生命周期中做判断。

(3)**`setState`**以后,如果**`shouldComponentUpdate`**返回**`false`**,那么**`state`**会发生什么?
发生的结果:**`shouldComponentUpdate`**返回**`false`**不会影响**`state`**的改变,只是不接着进行渲染了而已。
原因:**`setState`**调用后,**`state`**的更新是在**`render`**之前,也就是说在**`render`**被调用之前,**`state`**已经完成了更新。而**`render`**函数的作用是生成一棵描述应用结构的节点树,并保存在内存中。新的树和旧树进行 react diff比对,也就是**`reconciliation`**,用于确定哪些部分需要被更新。而**`shouldComponentUpdate`**则用于拦截组件的渲染,当该方法返回**`false`**以后,**`render`**就不会去执行。
  • componentWillUnmount

(1)在此处完成组件的卸载和数据的销毁


(2)清除你在组建中所有的**`setTimeout`**、**`setInterval`**、监听、订阅
/*
有时候我们会碰到这个warning:
Can only update a mounted or mounting component. This usually means you called 
setState() on an unmounted component. This is a no-op. Please check the code for
the undefined component.

原因:在组件中ajax请求返回setState,而组件销毁的时候,请求还未完成
*/
componentDidMount() {
	this.lock === true
    axios.post().then((res) => {
    	this.isMount && this.setState({   // 增加条件ismount为true时
        	aaa:res
      	})
    })
}

componentWillUnmount() {
    this.isMount === false
}
复制代码
  • 父子组件更新顺序

父组件先触发**componentWillMount**,然后再是子组件。


子组件优先触发**`componentDidMount`**,然后再是父组件

当外部的props改变的时候,如何请求数据和改变状态

  • 方法1:使用componentWillReceiveProps 加载数据(目前已不推荐)
class Demo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	num: 2
        }
    }
    
    componentWillReceiveProps(nextProps) {
    	if (/*满足某些条件*/) {
        	this.loadData()
        }
    }
    
    private loadData() {}
    
    render() {
    	return <Child>
        	child demo
        </Child>
    }
}
复制代码
  • 方法2:使用getDerivedStateFromProps + componentDidUpdate 加载数据
class Demo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        
        this.state = {
        	num: 2
        }
    }
    
    static getDerivedStateFromProps(nextProps,prevState) {
    	if (nextProps.count > prevState.num) {
        	return {
            	count: nextProps.count
                //...
            }
        }
        
        return null
    }
    
    private loadData() {}
    
    componentDidMount() {
    	this.loadData()
    }
    
    componentDidUpdate(prevProps, prevState) {
    	if (/*满足某些条件*/) {
        	this.loadData()
        }
    }
    
    render() {
    	return <Child>
        	child demo
        </Child>
    }
}
复制代码

开发中遇到的常见问题

  • 异步请求应该放在componentDidMount 中

由于 React 未来的版本中推出了异步渲染,在 dom 被挂载之前的阶段都可以被打断重来,导致**componentWillMountcomponentWillUpdatecomponentWillReceiveProps** 在一次更新中可能会被触发多次,因此那些只希望触发一次的副作用应该放在**componentDidMount**。

  • props改变触发的生命周期

最常见的误解就是**getDerivedStateFromPropscomponentWillReceiveProps只会在props改变时才会调用。实际上只要父组件重新渲染时,这两个生命周期函数就会重新调用,不管props**有没有变化都会更新。

生命周期版本迁移

React会在17.0版本开始会去掉的三个生命周期:componentWillMountcomponentWillReceivePropscomponentWillUpdate

PureComponent、Component

(1)**PureComponent提高了性能,因为它减少了应用程序中的渲染操作次数。因为React.PureComponent**内

部实现了**shouldComponentUpdate(),将propsstate的浅比较实现了。而React.Component**没有实现 shouldComponentUpdate()

(2)这里需要注意的是,我们使用**PureComponent时候,需要propsstate**的数据均为基本类型。如果这2者

的数据结构复杂的话(例如:嵌套对象和数组),那可能会渲染出错误的结果。

(3)如果非要使用复杂数据结构,那么只能手动去执行**forceUpdate**强制更新(执行强制更新的前提是你本身知

道数据发生改变了。)

(4)React.PureComponent中的shouldComponentUpdate()将跳过所有子组件树的props更新(也就是 说如果父组件是PureComponent,子组件不管是**PureComponent还是Component,只要父组件不重新 渲染,那么子组件就不会重新渲染),所以使用React.PureComponent的组件,它的所有子组件也必须都为React.PureComponent**

setState

  • 定义

1.将需要处理的变化塞入组件的**state**对象

2.告诉该组件及其子组件需要用更新的状态来重新渲染
3.响应事件处理和和服务端响应更新用户界面的主要方式

  • setState写法
//过去写法
this.setState({
	count: this.count + 1
})

//现在写法
this.setState((prevState) => {
	return { count: prevState.count + 1 }
})

/*
  传递object和function的区别:
  因为props和state存在异步更新
  - 传递object,react会对相同变量进行合并处理,并以最后一次的处理结果为准。
  - 传递function,react会将更新state的function放入到一个队列中,然后函数按照顺序依次调用。
  	该function存在存在2个参数:prevState、props
*/
复制代码
//如下案例以证明上面的观点
class Demo extends Component {
	state = {
    	count: 0
    }
    
    handleObj() {
    	this.setState({
        	count: this.state.count + 1
        })
        
        this.setState({
        	count: this.state.count + 1
        })
    }
    
    handleFunc() {
    	this.setState((prevState, props) => {
        	return {
            	age: prevState.age + 1
            }
        })
        
        this.setState((prevState, props) => {
        	return {
            	age: prevState.age + 1
            }
        })
    }
    
    render() {
    	return <div>
        	<button onClick={() => this.handleObj}>object</button>
            <button onClick={() => this.handleFunc}>function</button>
        </div>
    }
}

//运行结果:当点击object按钮,age的状态值为1,当点击function按钮,age最终的状态值为2
复制代码
  • setState并不会立即改变React的state值
class Kisure extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log => 0

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log => 0

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log => 2

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log => 3
    }, 0);
  }
  
  render() {
    return null;
  }
};
复制代码
  • 直接修改state是无效的

要知道**setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state。如果不通过setState**而直接修改

this.state,那么这个**state不会放入状态队列中,下次调用setState时对状态队列进行合并时,会忽略之 前直接被修改的state,这样我们就无法合并了,而且实际也没有把你想要的state**更新上去。

  • setState以后会发生什么

React在**setState之后,会经对state进行 diff,判断是否有改变,然后去 diff dom 决定是否要更新UI。在短时间内频繁setState。React会将state的改变压入栈中,在合适的时机,批量更新state**和视图,达到提高性能的效果。

  • 如何知道state已更新
//传入回调函数
setState({
    age: 1
}}, function(){
    console.log(this.state.age);
})

//生命钩子
componentDidUpdate(){
    console.log(this.state.age);
}
复制代码

react事件机制

如上图,为react事件的流程图

  • 事件注册和存储

如上图,为事件注册流程:

  • 当组件载入或者卸载的时候,通过**lastPropsnextProps**来决定事件的新增和删除。
  • 如果是新增事件的话,调用**EventPluginHubenqueuePutListener**进行事件存储。
  • 获取**document对象并根据react事件(onClickonCaptureClick)来判断是进行冒泡还是捕获。然后借助addEventListenerdocument注册事件,并设置回调函数为dispatchEvent**(该方法作用是事件分发)

如上图,为事件存储:

  • 因为react组件的装载和更新,就会产生新增的事件或者删除的事件。判断为新增事件,就会进入事物队列中,进行事件的存储(执行**putlistener)。判断为删除事件,则会执行删除监听方法(deleteListener**)。
  • 删除、添加监听的方法,来自于**EventPluginHub,它的作用就是管理React合成事件的callback,可以对listener**进行获取、删除、存储。
  • 事件存储在**listenerBank中,结构为:listenerBank[registrationName][key]。我们借助于EventPluginHubputListener方法用于向listenerBank添加listener,并且获取绑定事件的元素的唯一标识key进行存储:listenerBank[registrationName][key] = listener**。
//listenerBank[registrationName][key] 结构大致如下:
{
    onClick:{
        nodekey1:()=>{/*...*/}
        nodekey2:()=>{/*...*/}
    },
    onChange:{
        nodekey3:()=>{/*...*/}
        nodekey4:()=>{/*...*/}
    }
}
复制代码
  • 事件触发执行

如上图,为事件触发流程:
(1)触发**document注册原生事件的回调dispatchEvent**
(2)获取到触发事件的目标元素

class Demo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
        super(props)
        
        this.state = {/*...*/}
    }
    
    private parentClick = () => {/*...*/}
    
    private childClick = () => {/*...*/}
    
    render() {
      return <div className='parent' onClick={this.parentClick}>
                <div className='child' onClick={this.childClick}>
                  test
                </div>
              </div>
  	}
}
复制代码

如上代码:

(1)遍历该组件所有元素,找到触发事件的目标元素
(2)向上遍历父元素,将每一级的组件存入ancestors数组中
(3)react自定义的冒泡过程:遍历ancestors数组,依次执行**handleTopLevel方法,
(4)构成合成事件,然后将事件存储在
eventQueue队列中
(5)遍历
eventQueue队列,isPropagationStopped代表是否阻止冒泡,如果阻止了,则停止遍历,否则会通过executeDispatch**来执行合成事件
(6)队列遍历完成,则清理掉处理完成的事件

需要注意的是stopPropagation是react自己实现的,该方法不是原生的
复制代码
  • 合成事件的过程

如上图,为合成事件流程:

(1)react提取事件
(2)extractEvents中的EventPluginHub方法拿着遍历到的事件,再去循环遍历EventPlugin(**EventPlugin是用于处理不同事件工具的方法)
(3)符合条件的
EventPlugin会根据事件的类型,产生不同的事件,然后返回给事件池
(4)然后根据元素唯一标识
key和事件类型,从listenerBink**中取出回调函数
(5)最后将得到的回调函数返回给上一步 事件触发执行 中的 eventQueue队列

  • 为什么对组件中的事件绑定this
function invokeGuardedCallback(name, func, a) {
  try {
    func(a);
  } catch (x) {
    if (caughtError === null) {
      caughtError = x;
    }
  }
}
复制代码

如上代码,为**dispatchEvent触发以后调用的方法,其内部func**参数直接被运行,并没有指定具体是哪个组件调用的。因此,需要手动绑定this。否则获取不到this。

//绑定this有如下几种:
class Demo extends React.Component<IProps, IState> {
    constructor(private props: IProps) {
        super(props)
        
        this.state = {/*...*/}
        this.childClick = this.childClick.bind(this) //第一种
    }
    
    private parentClick = () => {/*...*/} //第二种
    
    private childClick () {/*...*/}
    
    private innerChildClick () {/*...*/}
    
    render() {
      return <div className='parent' onClick={this.parentClick}>
                <div className='child' onClick={this.childClick}>
                  <div className='innerChild' onClick={() => this.innerChildClick()}>
                  	第三种绑定方式:onClick={() => this.innerChildClick()}
                  </div>
                  <div className='innerChild' onClick={this.innerChildClick.bind(this)}>
                  	第四种绑定方式:onClick={this.innerChildClick.bind(this)}
                  </div>
                </div>
              </div>
    }
}
复制代码
  • react事件和原生事件
  • DOM原生可以使用**addEventListener**函数在事件流的的不同阶段监听事件。

React绑定事件的属性名使用驼峰形式命名,并且事件处理函数是一个函数,而原生dom事件是函数名称

  • 混合事件使用须知:

(1)响应顺序:DOM事件监听器被执行,然后事件继续冒泡至document,合成事件监听器再被执行。也就是说:dom event > react event
(2)阻止冒泡:如果DOM事件被阻止冒泡了,无法到达document,所以合成事件自然不会被触发


  • react事件的简单总结:
  • React自己实现了一个中间层,因为要避免DOM上绑定过多的事件处理函数,整个页面响应及内存占用可能都会受到影响,同时为了避免底层不同浏览器之间的事件系统差异。当用户在为onClick添加函数时,React并没有将Click时间绑定在DOM上面,而是在**document上监听所支持的所有事件。当事件发生并冒泡到document时,react会使用dispatchEvent**找到对应的事件去处理。
  • 合成事件的监听器是统一注册在**document**上的,且仅有冒泡阶段。所以原生事件的监听器响应总是比合成事件的监听器早。阻止原生事件的冒泡后,会阻止合成事件的监听器执行。

key

class Demo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
        super(props)
        
        this.state = {
        	children: [
            	{ name: 'a', age: 15, id: 0 },
                { name: 'b', age: 16, id: 1 },
                { name: 'c', age: 17, id: 2 },
                { name: 'd', age: 18, id: 3 }
                /*...*/
            ]
        }
    }
    
    render() {
    	const { children } = this.state
    	return <div>
        	{
            	children.map(child => {
                	return <p key={child.id}>{child.name}-{child.age}</p>
                })
            }
        </div>
    }
}
复制代码

如上代码:state中存在默认状态值**children**,借助map方法,产生child列表节点

  • key的作用

react利用key来识别组件,他是一种身份标识,就像每个人有一个身份证来做辨识一样。每个key 对应一个组件,相同的key react认为是同一个组件,这样后续相同的key对应组件都不会被创建。react需要我们提供给一个key来帮助更新,减少性能开销。

  • 不建议使用index作为key的值

key的值是唯一的,key的真实目的是为了标识在前后两次渲染中元素的对应关系,防止发生不必要的更新操作。那么如果我们用index来标识key,数组在执行插入、排序等操作之后,原先的index并不再对应到原先的值。

fragments

const children: Array<{ name: string; age: number; id: number }> = [
	{ name: 'A', age: 10, id: 1 },
    { name: 'B', age: 11, id: 2 },
    { name: 'C', age: 12, id: 3 },
    /*...*/
]

const Demo: React.Fc = (props: IProps) => {
	return (
    	<div className='demo-div'>
            {
            	children.map(item => {
                	return <React.Fragment key={ item.id }>
                    	<p>{ item.name }</p>
                        <p>{ item.age }</p>
                    </React.Fragment>
                })
            }
        <div>
    )
}
复制代码
  • ragments 可以让你聚合一个子元素列表,并且不在DOM中增加额外节点
  • <></><React.Fragment></React.Fragment> 的语法糖,但是<></>语法不能接受键值或属性。所以如果要使用key,则需要使用<React.Fragment></React.Fragment>

错误边界

错误边界的定义

错误边界是一种 React 组件,可以捕获发生在其子组件树任何位置的 JavaScript 错误,并且渲染出备用 UI。错误边界在渲染期间、生命周期和整个组件树的构造函数中捕获错误

使用方式

//第一种错误边界
class ErrorBoundary extends React.Component<IProps, IState> {
    constructor(props: IProps) {
      super(props);
      
      this.state = { hasError: false };
    }
    
    componentDidCatch(error, errorInfo) {
      this.setState({
          error:error,
          errorInfo:errorInfo
      })
    }
    
    render() {
      if (this.state.errorInfo) {
        return <div>
            <p>{this.state.error&&this.state.error.toString()}</p>
            <p>{this.state.errorInfo.componentStack}</p>
        </div>
      }
      
      return this.props.children; 
    }
 }
复制代码
//第二种错误边界
class ErrorBoundary extends React.Component<IProps, IState> {
    constructor(props:IProps) {
      	super(props);
      
      	this.state = { 
      		hasError: false 
      	};
    }
    
    static getDerivedStateFromError(error) {
      	return { 
      		hasError: true 
      	};
    }
    
    render() {
      	if (this.state.hasError) {
        	//显示降级UI
        	return <div>error!</div>
      	}
        
      	return this.props.children; 
    }
 }
复制代码

如上代码:

  • 只要在class中使用**componentDidCatch()(打印错误)、static getDerivedStateFromError()**(渲染备用 UI)其中一个,那么就是错误边界组件
  • 自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。增加错误边界能够让你在应用发生异常时提供更好的用户体验。即其中的某些 UI 组件崩溃,其余部分仍然能够交互。

受控组件和非受控组件

受控组件就是可以被 react 状态控制的组件。

非受控组件是不被react状态控制的组件。
在 react 中,Input textarea 等组件默认是非受控组件(输入框内部的值是用户控制,和React无关)。但是也可以转化成受控组件,就是通过**onChange事件获取当前输入内容,将当前输入内容作为value传入,此时就成为受控组件。
使用受控组件的好处:可以通过
onChange**事件控制用户输入,使用正则表达式过滤不合理输入。

//受控组件
import React, { createRef } from 'react'

class Demo extends React.Component<IProps, IState> {
	constructor(props:IProps) {
        super(props)
      
        this.state = { 
           name: '',
           age: null
        }
        
        this.sexRef = createRef()
    }
    
    private handleNameChange = (name: string) {
    	this.setState(() => {
        	return {
            	name
            }
        })
    }
    
    private handleAgeChange = (age: number) {
    	this.setState(() => {
        	return {
            	age
            }
        })
    }
    
    private handleClick = () {
    	//点击获取sex的值
    	console.log(this.sexRef.current.value)
    }
    
    render() {
    	return (
        	<form className='form-div'>
            	<div>
                	<label for='name'>姓名:</label>
                    <input type='text' id='name' value={this.state.name} onChange={this.handleNameChange}/>
                </div>
                <div>
                	<label for='age'>年龄:</label>
                    <input type='number' id='age' value={this.state.age} onChange={this.handleAgeChange} />
                </div>
                <div>
                	<label for='sex'></label>
                    <input type='text' id='sex' ref={this.sexRef}/>
                </div>
                <div>
                	<button onClick={this.handleClick}>点击获取sex的值</button>
                </div>
            </form>
        )
    }
}
复制代码

如上代码:

  • 受控组件,**inputvalue属性绑定的是对应的状态的值,同时使用onChange**进行注册事件。
  • 非受控组件,因为不能通过状态获取,所以可以借助**ref来获取。在constructor中调用React.CreateRef()来声明ref,然后将声明的ref赋值给input对应的ref**

refs

什么是refs

  • Refs 是一个获取DOM节点或React元素实例的工具。在特殊情况下(正常情况是父级修改传递给子组件的props属性),需要强制修改子组件。
  • Refs 的使用场景:(1)因为 Refs 可以获取子组件的实例,那么可以调用子组件的方法。(2)因为 Refs 可以获取 DOM 节点,则可以对 DOM 节点做一些操作,例如:input 元素的焦点控制,非受控组件获取其中的 value 值
  • Refs 的值根据节点的类型表现是不同的:(1)当 Refs 属性用于HTML元素时,Refs 接收底层 DOM 元素作为其 current 属性。(2)Refs 属性用于 class 组件时,Refs 接收组件的挂载实例作为其 current 属性。
  • 注意:我们使用**React.createRef()创建的ref是不能够赋给函数组件的,因为函数组件没有实例。如果非要获取,则可以使用forwardRef方式(下一个章节会提到),或者useRef**(react hooks)

refs的使用

  • 创建refs
import React, { createRef } from 'react'
import CourseTreeContainer from '@/....'

class Demo extends React.Component<IProps, IState> {
    constructor(props:IProps) {
        super(props)
      
        this.state = { 
           name: ''
        }
        
        this.sexRef = createRef()
        this.courseTreeRef = React.createRef()
    }
    
    private handleClick = () {
        //点击获取sex的值
        console.log(this.sexRef.current.value)
        //input处于focus状态
        this.sexRef.current.focus()
    }
    
    // 刷新数据
    private refreshDataSource = () => {
    	// 调用子组件方法:loadFirstLayerMenu
    	(this.courseTreeRef! as any).current!.loadFirstLayerMenu();
        /*...*/
    }
    
    render() {
        return (
            <form className='form-div'>
                <div>
                    <label for='sex'></label>
                    <input type='text' id='sex' ref={this.sexRef}/>
                </div>
                <div>
                    <button onClick={this.handleClick}>点击获取sex的值</button>
                </div>
            </form>
            
            <div className='table-tree'>
                <CourseTreeContainer {...courseTreeProps} ref={this.courseTreeRef}/>
            </div>
            <button onClick={this.refreshDataSource}>刷新数据</button>
        )
    }
}
复制代码

如上代码,Refs 使用**React.createRef()创建,通过ref**属性附加到React元素。

如果作用于 DOM 元素,那么Refs 接收底层 DOM 元素作为其 current 属性。
如果作用域 class 组件,可以获取**CourseTreeContainer实例,那就可以调用它的内部方法:loadFirstLayerMenu**。

forwardRef

在使用**forwardRef**之前,你需要知道的:

  • **forwardRef**的作用:创建一个 React 组件,该组件能够将其接收的 ref 属性转发到内部的一个组件中
  • **ref的作用是获取实例,可能是 DOM 实例,也可能是 class 的实例。但是如果是函数组件的话,使用ref**去传递就会报错。


  • props 是不能传递 ref 的,所以需要借助**React.forwardRef**来创建一个React组件。
  • 为什么props不能传递ref呢
const ErrorDemo = ({ref}) => {
    return <div ref={ref}>
        <p>hahahha</p>
    </div>
}

const RightDemo = forwardRef((props, ref:IRef) => {
    return <div ref={ref}>
        <p>hahahha</p>
    </div>
})

class Demo extends React.Component<IProps, IState> {
	private ref: React.Ref<any>
    
    constructor(props:IProps) {
    	super(props)
        
        this.ref = React.createRef();
    }
    
    render() {
        return <>
        	<ErrorDemo ref={this.ref}/>
            <RightDemo ref={this.ref}/>
        </>	
    }
}
复制代码

如上代码:针对于**ErrorDemo组件this.ref永远是{current: null},在Test子组件也发现,传进来的ref是undefined。而RightDemo**组件是可以获取到的。

  • forwardRef使用方式
const FancyButton = React.forwardRef((props: IProps, ref: IRef) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
))

const ref = React.createRef()

<FancyButton ref={ref}>Click me!</FancyButton>

console.log('FancyButton', FancyButton)
复制代码

如上代码:
React 会将 <FancyButton ref={ref}> 元素的 ref 作为第二个参数传递给**React.forwardRef** 函数中的渲染函数。该渲染函数会将 ref 传递给 <button ref={ref}> 元素。
当 React 附加了 ref 属性之后,ref.current 将直接指向 <button> DOM 元素实例

  • frowardRef原理分析
export default function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  //__DEV__可不看
  if (__DEV__) {
    /*...*/
  }

  if (render != null) {
	/*...*/
  }

  return {
    //被forwardRef包裹后,组件内部的$$typeof是REACT_FORWARD_REF_TYPE
    $$typeof: REACT_FORWARD_REF_TYPE,
    //render即包装的FunctionComponent,ClassComponent是不用forwardRef的
    render,
  };
}
复制代码

如上代码:
最终会返回一个对象


render是个方法,在函数组件更新的时候,会被调用(这部分涉及到**renderWithHooksbailoutHooksreconcileChildren,后面会有专门的解析)。
注意:
FancyButton是一个对象,而平时创建出来的class组件,那是一个ReactElement**对象:

//ReactElement对象
const element = { 
	$$typeof: REACT_ELEMENT_TYPE,
    type: type,
    /*...*/
}
复制代码

同时只有函数组件被**forwardRef包裹以后才会变成REACT_FORWARD_REF_TYPE类型,class组件是不会变成REACT_FORWARD_REF_TYPE**类型的。

无状态组件和有状态组件

无状态组件:

const FuncComponent:React.Fc = props => <div>{props.name}</div>;
复制代码

优势是打包后体积小,写法清爽。劣势是无状态(state)和生命周期,无法利用生命周期减少render,也就是说只要props发生变化就会重新渲染。
无状态组件主要用来定义模板,接收来自父组件props传递过来的数据,使用{props.xxx}的表达式把props塞到模板里面。无状态组件应该保持模板的纯粹性,以便于组件复用

有状态组件:

也就是class组件,有状态(state)和生命周期。劣势是体积大(相同功能下,打包以后class组件的代码比函数组件的代码大)。
有状态组件主要用来定义交互逻辑和业务数据。

组件通讯

  • props进行父子组件传递通讯
//父组件
class Parent extends React.Component<IProps, IState> {
    constructor(private props: IProps) {
        super(props)
        
        this.state = {
            count: 1
        }
    }
    
    private addCount() {/*...*/}
    
    render() {
        const childProps: IChildProps = {
            emitFunc: this.addCount,
            count: this.state.count
        }
        return <div>
            <Child {...childProps}/>
        </div>
    }
}

//子组件
class Child extends React.Component<IChildProps, IState> {
    constructor(private props: IProps) {
        super(props)
        
        this.state = {
            num: 2
        }
    }
    
    render() {
        return <Child>
            <p>父组件传递过来的属性count:{ this.props.count }</p>
            <button onClick={() => this.props.emitFunc()}>count++</button>
        </Child>
    }
}
复制代码

如上代码:父组件可以通过props向子组件传递属性和方法。**this.props.coun是父级传递给子级组件的信息以完成父级向自己的通讯。this.props.emitFunc**是父级传递给子组件的方法,子组件一旦触发符合的条件即可执行该方法,并传入相关的参数,则父级组件即可接受到子组件提供的信息以完成子级向父级的通讯

  • context进行跨组件传递通讯

context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
context存在3个api:React.createContext()ProviderConsumer
(1)React.createContext():用来创建context对象,并包含Provider、Consumer两个组件。
(2)Provider:数据的生产者,通过value属性接收存储的公共状态,来传递给子组件或后代组件。
(3)Consumer:数据的消费者,通过订阅Provider传入的context的值,来实时更新当前组件的状态。

//创建context
//其中defaultValue是默认值,只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。
//注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
const defaultVal = {/*...*/}
const DemoContext = React.createContext(defaultVal);
/*...*/
复制代码
//数据产生
import DemoContext from '../..'

class DemoProvider extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
        super(props)
        
        this.state = {
            /*...*/
            value: {
            	color: 'pink',
                'font-size': 27
            }
        }
    }
    
    render() {
    	const { value } = this.state
    	return (
        	<DemoContext.Provider value={ value }>
            	{ this.props.children }
            </DemoContext.Provider>
        )
    }
}
复制代码
//第一种数据消费方法:
import DemoContext from '../../'

class DemoConsumer extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
        super(props)
        
        this.state = {
            /*...*/
        }
    }
    
    render() {
    	const { value } = this.state
    	return (
        	<DemoContext.Consumer>
            	{
                	value => (
                    	<div style={{ border: `1px solid ${value.color}` }}>
                          hello kisure
                          /*...*/
                        </div>
                    )
                }
            </DemoContext.Consumer>
        )
    }
}

//第二种数据消费方法
class DemoConsumerTwo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
        super(props)
        
        this.state = {
            /*...*/
        }
    }
    
    static contextType = DemoContext
    
    render() {
    	const { color } = this.context
    	return (
        	<div style={{ border: `1px solid ${color}` }}>
                hello kisure
                /*...*/
            </div>
        )
    }
}
复制代码

如上几处代码:
(1)Context.Provider:Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层数据优先级高,所以会覆盖外层的数据。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数。
(2)Context.Consumer:存在2种消费方式,第一种为render props形式(后面小节会提到),第二种直接挂载在class上**static contextType = DemoContext,如此就可以通过this.context**进行访问value中的值。

  • redux进行非嵌套组件间通信(需要配合使用react-redux)
//reducer
function counter(state = 0, action) {
	switch (action.type) {
    	case 'INCREMENT':
      		return state + 1;
    	case 'DECREMENT':
      		return state - 1;
    	default:
      		return state;
  	}
}

//创建store
import { createStore } from 'redux'
let store = createStore(counter);

//action
store.dispatch({ type: 'INCREMENT', value: { /*..*/ } });
复制代码
//将redux和react-redux结合起来
import * as React from 'react'
import {connect} from 'react-redux'
import { bindActionCreators } from 'redux';
import { updateInfo } from 'store/.../action'; 
/*...*/

class Demo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
        super(props)
        
        this.state = {
            /*...*/
        }
    }
    
    private handleClick = () => {
    	this.props.updateInfo({
        	/*...*/
        })
    }
    
    render() {
    	return (
        	<div>
            	<p>从redux上映射出来的值:{ this.props.name }</p>
            	{ /*...*/ }
                <button onClick={this.handleClick}>点击更新redux状态数据</button>
            </div>
        )
    }
}

//将状态映射到props上
function mapStateToProps(state: any) {
    return {
    	name: state.name
    	/*...*/
    };
}

//将action映射到props上
function mapDispatchToProps(dispatch: any) {
    return {
        updateInfo: bindActionCreators(updateValue, dispatch)
    };
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Demo);
复制代码

如上代码:需要了解**reduxreact-redux**为前提,文章后面会提到。

HOC和render props

  • HOC高阶组件
const HOCFactory = WrappedComponent => {
    return class HOCDemo extends React.Component<IProps, IState> {
     	constructor(private props: IProps) {
        	super(props)
            /*...*/
        }

      	render(){
        	return <WrappedComponent {...this.props} />
      	}
    }
}
复制代码

如上代码,HOC就是特点:接受一个组件作为参数,返回一个新的组件。它的作用:(1)属性代理 (2)反向继承

属性代理:把高阶组件接收到的属性传递给传进来的组件。

  • 修改**props的内容值,对传递进来的组件WrappedComponent**的props进行控制。
function DemoHOC(WrapperComponent) {
    return class extends React.Component<IProps, IState> {
    	private defaultVal: IDefaultVal
    	constructor(private props: IProps) {
            super(props)
            this.defaultVal = {/*...*/}
        }
        render() {
            const { name, ...props } = this.defaultVal
            return <WrapperComponent {...props} name={name || 'kisure'} />
        }
    }
}
复制代码

如上代码,就是覆盖和添加了**WrapperComponentprops**。

反向继承:继承自传递过来的组件,主要用于渲染劫持和控制**state**。

  • 反向继承允许高阶组件通过**this关键词获取WrappedComponent,所以我们可以获取到 stateprops,组件生命周期钩子,render**等方法,所以我们主要用它来做渲染劫持,比如在渲染方法中读取或更改react树的结构。
function DemoHOC(WrapperComponent) {
	//继承了传入组件
	return class extends WrapperComponent {
    	//重写 componentDidMount 方法
        componentDidMount(){
        	// 修改count状态值
      		this.setState({ count: 2 });
          	/*...*/
        }
    	render() {
        	//根据部分参数去决定是否渲染组件
          	if (this.props.showContent) {
            	return (
                	<p>渲染劫持展示内容</p>
                )
            }
            //使用 super 调用传入组件的 render 方法
          	return super.render();
        }
    }
}
复制代码

如上代码,借助反向继承,可以修改生命周期里面的写法,根据**props**的值进行选择性的渲染内容也就是说的渲染劫持。

  • render props

将一个组件内的**state作为props**传递给调用者,调用者可以动态的决定如何渲染。此模式,让使用者不用再关注组件的内部实现(在组件内部已经做好了一切能做到的),只要把数据通过函数传出去就好,实现自定义的渲染即可。

import RenderPropsComponent from '../..'

class Demo extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        /*...*/
    }
    
    render () {
    	return <div>
        	{ /* 传递一个function,并给予一个参数。就能获取到 RenderPropsComponent 传递过来的数据 */ }
        	<RenderPropsComponent render=((str: string) => (
            	<>
                	<div>
                		<p>当前获取到的字符串的值为:{str}</p>
                	</div>
                </>
            )) />
        </div>
    }
}
复制代码
class RenderPropsComponent extends React.Component<IProps, IState> {
	constructor(private props: IProps) {
    	super(props)
        /*...*/
    }
    
    render() {
        const { render} = this.props;

        return (
            <div>
              {render('hello kisure this is a rende rprops demo')}
            </div>
        )
  	}
}
复制代码

如上代码,renderRenderPropsComponent组件的props。该组件内部,位于**render()的地方,将通过props拿到了传递过来的render方法进行执行。这样我们在Demo**中,就能拿到对应的数据。

Suspense 和 React.lazy()

  • React.lazy()

实现码分割以达到延迟加载 react 组件的效果,是 react 自己的核心库。当然也可以使用**react-loadable**,效果是同样的。

  • Suspense

**Suspense是和React.lazy()一起使用的。它用来包裹住延迟加载组件。多个延迟加载的组件可被包在一个 suspense 组件中。它也提供了一个 fallback 属性,用来在组件的延迟加载过程中显式某些 react 元素(因为延迟加载的缘故,加载是需要时间的,如果网络不好,加载时间长,这个时候Suspense**的好处就体现出来了,给予一个加载中的页面,提高用户体验)。

//子组件
const ChildComponent: React.Fc = (props: IProps) => {
	return (
    	<div>
        	<p>hello this is a child component</p>
        </div>
    )
}

//父组件
const Child = React.lazy(() => import('./ChildComponent'));

const Parent: React.Fc = (props: IProps) => {
	return (
        <div>
            <Suspense fallback={<div>拼命加载中,请等待...</div>}>
              <Child />
            </Suspense>
        </div>
    );
}
复制代码
  • 原理分析
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
  let lazyType = {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1,
    _result: null,
  };

  return lazyType;
}

function readLazyComponentType(lazyComponent) {
  const status = lazyComponent._status;
  const result = lazyComponent._result;
  switch (status) {
    case Resolved: { // Resolve 时,呈现相应资源
      const Component = result;
      return Component;
    }
    case Rejected: { // Rejected 时,throw 相应 error
      const error = result;
      throw error;
    }
    case Pending: {  // Pending 时, throw 相应 thenable
      const thenable = result;
      throw thenable;
    }
    default: { // 第一次执行走这里
      lazyComponent._status = Pending;
      const ctor = lazyComponent._ctor;
      const thenable = ctor(); // 可以看到和 Promise 类似的机制
      thenable.then(
        moduleObject => {
          if (lazyComponent._status === Pending) {
            const defaultExport = moduleObject.default;
            lazyComponent._status = Resolved;
            lazyComponent._result = defaultExport;
          }
        },
        error => {
          if (lazyComponent._status === Pending) {
            lazyComponent._status = Rejected;
            lazyComponent._result = error;
          }
        },
      );
      // Handle synchronous thenables.
      switch (lazyComponent._status) {
        case Resolved:
          return lazyComponent._result;
        case Rejected:
          throw lazyComponent._result;
      }
      lazyComponent._result = thenable;
      throw thenable;
    }
  }
}
复制代码
function import(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}
复制代码
class Suspense extends React.Component {
  state = {
    promise: null
  }

  componentDidCatch(e) {
    if (e instanceof Promise) {
      this.setState({
        promise: e
      }, () => {
        e.then(() => {
          this.setState({
            promise: null
          })
        })
      })
    }
  }

  render() {
    const { fallback, children } = this.props
    const { promise } = this.state
    return <>
      { promise ? fallback : children }
    </>
  }
}
复制代码

如上三处代码:
(1)为什么能够实现代码分割

动态导入,如同**Promise执行机制, 具有PendingResolvedRejected**三种状态。 import()返回的是promise,Webpack 解析到import()语法时,会自动进行代码分割。

(2)React.lazy()分析

readLazyComponentType()React.lazy()核心代码,React.lazy() 执行后会返回的 LazyComponent 对象,该对象存在**_status属性,初始默认值为-1。初始渲染的时候,会执行readLazyComponentType中的default的代码,也就是执行了import(url),至于多处地方存在throw,是因为Suspense能够catch到throw出来的信息,如果catch到它是一个 Promise 的话,就会将 children 都渲染成 fallback 的值,一旦 Promiseresolve 则会继续渲染一次,并得到最终的结果。default**步骤 和 **Pending步骤 都是针对Suspense**的场景。

(3)Suspense分析

因为**React.lazy()throw错误信息,所以Suspense就需要使用生命周期ComponentDidCatch,来捕捉子组件树中的错误异常。所以当父组件渲染子组件时发现异步请求,直接抛出错误,捕获Promise对象时候,渲染fallback。当resolve时重新render,遇到下一个异步请求重复上面操作直到整个父组件的抛出的promise对象都为resolve,将loading**替换为真正的组件。

hooks

  • hooks产生的原因

(1)class组件中方法需要绑定this。

(2)复用一个有状态的组件难度太大。
(3)由于业务需求的改变,函数组件不得不改为类组件。

  • useState

**setState的作用:使得函数组件存在class组件的statesetState**的功能。

function UseStateDemo: React.Fc = (props: IProps) => {
	const [count, setCount] = useState(1)
    
    const addCount = (count: number) => {
    	setCount(count)
    }
    
    return (
    	<div>
        	<p></p>
            <button onClick={() => addCount(count + 1)}>点击添加计数</button>
        </div>
    )
}
复制代码

如上代码:
使用**useState的时候,需要注意,useState要按照顺序去执行。如果放在if或者循环语句中使用useState,那么当某个情况下不满足条件时候,就会跳过当前的useState。这样造成的影响就是state的值会错乱(例如:有ABC三个useState,B在if条件中,如果因为某些原因使得if中条件不满足,那么就会跳过B,继续执行C。这回造成B的状态,在C中会被获取)。造成这样的情况是因为useState产生的hooks对象其内部是链表的结构,获取state状态值取决于next(next指向的是下一个state**),后面小节会有对useState的源码分析。

  • useEffect

**useEffect**的作用:承担函数组件生命周期的作用。

function UseEffectDemo: React.Fc = (props: IProps) => {
	const [count, setCount] = useState(1)
    
    useEffect(() => {
    	conosle.log('componentDidMount')
        return () => {
        	console.log('componentWillUnmount')
        }
    }, [])
    
    useEffect(() => {
    	console.log('componentDidUpdate')
    }, [count])
    
    useEffect(() => {
    	console.log('componentDidUpdate')
    })
    
    return (
    	<div>
        
        </div>
    )
}
复制代码

如上代码:
(1)我们在使用**useEffect的时候,如果第二个参数是一个空数组,那么它的行为就是componentDidMount。如果在useEffect的回调函数中return一个function,那么这个return的行为就是componentWillUnmount。如果在useEffect的第二个参数中传入数组并且其中的内容为状态字段或者什么都不传递,那么它的作用就是componentDidUpdate。也就是说数据一旦发生变化,经过react检测以后react实现更新,就会重复调用。
(2)
useEffect**还存在一切隐藏的规则:

  • **useEffect里面使用到的state的值, 固定在了useEffect内部, 不会被改变,除非useEffect刷新,重新固定state**的值。
function UseEffectDemo: React.Fc = (props: IProps) => {
	const [count, setCount] = useState<number>(0)
    useEffect(() => {
        console.log('use effect...', count)
        const timer = setInterval(() => {
            console.log('timer...count:', count)
            setCount(count + 1)
        }, 1000)
        return ()=> clearInterval(timer)
    },[])
    return (
    	<div>{count}</div>
    )
}
复制代码
  • 判断条件中不能包裹**useEffect**
function UseEffectDemo: React.Fc = (props: IProps) => {
	const [count, setCount] = useState<number>(0)
    if(0){
       useEffect(() => {
            console.log('use effect...',count)
            const timer = setInterval(() => setCount(count +1), 1000)
            return ()=> clearInterval(timer)
        }) 
	}
    return (
    	<div>{count}</div>
    )
}
复制代码
  • **useEffect**不能被打断
function UseEffectDemo: React.Fc = (props: IProps) => {
	const [count, setCount] = useState<number>(0)
    useEffect(() => {
        /*...*/
    })
    return
    useEffect(() => {
    	/*...*/
    })
    return (
    	<div>{count}</div>
    )
}
复制代码
  • useRef

useRef的作用:(1) 只能在函数组件中使用,同时用于操作dom。(2)相当于全局作用域,一处被修改,其他地方全更新

// 作用1:操作dom
const MyInput:React.Fc = (props: IProps) => {
	const formRef = createRef<HTMLFormElement>()
	const inputRef = useRef<HTMLInputElement>()
    const [ count, setCount ] = useState<number>(0)
    useEffect(() => {
    	console.log(inputRef)
    	inputRef.current.focus()
    }, [])
    return (
    	<form ref={}>
        	<input type='text' ref={inputRef}/>
        </form>
    )
} 

// 作用2:全局作用域
const MyInput:React.Fc = (props: IProps) => {
  const [count, setCount] = useState(0)
  const countRef = useRef(0)
  useEffect(() => {
      console.log('use effect...',count)

      const timer = setInterval(() => {
          console.log('timer...count:', countRef.current)
          setCount(++countRef.current)
      }, 1000)

      return ()=> clearInterval(timer)
    },[])

	return (
    	<div>全局作用域</div>
    )
}
复制代码

如上代码:
针对于全局作用域,因为useEffect 里面使用到的state的值, 固定在了useEffect内部,不会被改变,除非useEffect刷新,重新固定state的值。所以

  • useContext

  • useReducer

  • useCallback

  • useMemo

react其余的一些简单API

参考文献:

  • setState传递object和function的区别:https://xuexi.erealmsoft.com/2019/09/17/react/react-set-state/
  • 解读为什么直接修改this.state无效 www.jianshu.com/p/1e7e956ec…
  • 生命周期 www.jianshu.com/p/b331d0e4b…
  • state和propshttps://juejin.im/post/6844903599235940360
  • React 渲染优化:diff 与 shouldComponentUpdate juejin.im/post/684490…
  • React事件机制:https://juejin.im/post/6844903790198571021
  • key:https://juejin.im/post/6844903700209598477
  • 受控组件和非受控组件:https://www.jianshu.com/p/c4fb11f42252
  • refs:https://juejin.im/post/6844903809274085389
  • forwardRef: juejin.im/post/684490…
  • 高阶组件HOC:https://juejin.im/post/6844904050236850184
  • react.lazy:https://thoamsy.github.io/blogs/react-lazy/
  • react hooks:https://juejin.im/post/6844904072168865800#heading-1