全文所使用的版本为React17版本。
全文为本人学习的随笔,以官网提供的内容为准,参考不可保证其正确性与权威性。
1.1.create-react-app脚手架
什么是cra:与vue-cl是同一类工具,为react脚手架。 先在开发环境中安装cra。
yarn global add create-react-app
在安装完成后可以进入需要存放项目文件夹,创建react项目。
create-react-app project-name
在创建完项目后,可以按照提示试运行。运行后可以看到初始化的页面,旋转的react图标。
cd project-name
yarn start
1.2.index.js
将src中除index.js外的文件全部删除。并在index.js中删减至如下内容。
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = document.querySelector('#root')
const App = React.createElement('div',{className: 'red'}, n)
ReactDOM.render(App,root)
App为一个由React.createElement()创建的节点。 经过ReactDOM.render()渲染到了id为root的div上。
1.3.React.createElement()
React.createElement()是用于创建React元素的函数。其接受的参数为: 1. 节点标签类型 2. 选项,className为其中一个选项 3. 节点内部的内容 React.createElement()的第三个参数为节点的内部内容,其如果有多个内容,则接受数组的形式 React.createElement()创建的是一个React元素,又称虚拟DOM对象,不能直接插入到页面中进行渲染,需要借助ReactDOM.render()进行渲染。
1.4.累加器
向App的createElement的第三个参数中,添加一个累加的按钮。
React.createElement('button',
{onClick: ()=>{
n += 1
}},
'+1'
)
将其放入App的createElement的第三个参数中。
const App = React.createElement('div',{className: 'red'},[
n,
React.createElement('button',
{onClick: ()=>{
n += 1
}},
'+1'
)
])
此时在页面中点击+1按钮,无法在页面上生效。
1.5.React手动渲染
App是一个const变量,其createElement中第三参数使用的n的值,在其定义声明时就已经确定,不再变更,要实现数据驱动页面,则需要实现App中n的值的变更,与页面的重新渲染。 ReactDOM.render()能够实现页面的重新渲染,此时需要再实现App中更新n的值。 将App写成一个返回react元素的函数,可以实现更新n的值
const App = ()=> React.createElement()
此时当n的值变更时,调用App()函数,就能更新n的值。我们将App()的调用及其渲染写入到按钮的onClick事件函数中。
const App = ()=> React.createElement('div',{className: 'red'},[
n,
React.createElement('button',
{onClick: ()=>{
n += 1
ReactDOM.render(App(), root)
}},
'+1'
)
])
1.6.DOM Diff算法更新渲染机制
ReactDOM.render()在更新渲染时,会使用DOM Diff算法。 DOM Diff算法会判断更新前后的两个React元素的差异,只重新渲染不同的地方。
2.1.JSX
Vue内的vue-loader使Vue能够在Vue-cli中的.vue文件内写template模板。 而React中有与Vue的template类似的JSX。babel-loader会将JSX语法转化为js
import React from 'react'
const App = () =>{
return <>
<div className="red">n</div>
</>
}
上述JSX会被babel-loader转译为:
React.createElement('div',{className: 'red'}, 'n')
JSX的语法中没有实质写到React的引用,但依然需要引入React才能生效。 return后需要接<></>或(),并将html内容写入其中,<></>内可以写入多个标签,()内只能有一个根标签。 class需要写成className,以避免js中的关键字冲突,因为JSX本质依然是js。 JSX语法中标签内的所有JS代码需要使用{}包裹。 变量需要使用{}包裹。 对象需要使用{}包裹。
2.2.JSX中的v-if/if...else
JSX语法能够实现根据条件,返回不同的React元素。 实现与Vue-template中v-if一样的效果。
const Component = ()=> {
return n%2 - - - 0
?
<div>n是偶数</div>
:
<div>n是基数</div>
}
注意上述强调的,标签内的js需要用{}包裹,如下述所写,div.wrapper内部会被当作标签的内容,而不会当作js,需要在外部加上{}包裹。
const Component = ()=> {
return (
<div class="wrapper">
n%2 - - - 0
?
<div>n是偶数</div>
:
<div>n是基数</div>
</div>
)
}
const Component = ()=> {
return (
<div class="wrapper">
{
n%2 - - - 0
?
<div>n是偶数</div>
:
<div>n是基数</div>
}
</div>
)
}
标签内的js内容用{}包裹 也可以在内部写入变量:
const Component = ()=> {
const content = (
<div>
{
n%2- - -0
?
<div>n是偶数</div>
:
<div>n是奇数</div>
}
</div>
)
return content
}
或者直接往内部写入if...else...:
const Component = ()=> {
let inner
if(n%2- - -0){
inner = <div>n是偶数</div>
}else{
inner = <span>n是奇数</div>
}
const content = (
<div>
{inner}
</div>
)
return content
}
JSX为写法提供了很多自由性。
2.3.JSX中的遍历取值
const Component = (props) => {
return props.numbers.map((n,index)=>{
return <div>下标{index}的值为{n}</div>
})
}
JSX能够使用js的map完成遍历,组件需要接受一个能够遍历的参数,返回其map用于遍历。 map接受一个函数,函数接收两个形参,第一个形参为值,第二个形参为下标。 获取其每次遍历的形参运用到标签中返回,实现了遍历。
2.4.JSX中的遍历存值
const Component = (props) => {
const array = []
for(let i=0;i<props.numbers.length;i++){
array.push(<div>下标{i}值为{props.numbers[i]}</div>)
}
return <div>{array}</div>
}
通过for循环配合array.push,能实现将标签push入数组中。
3.1.元素与组件
再先前实现累加器中,在手动渲染数据时,将App写成了函数的形式。 App如果其写成变量形式,其值为React元素,其为元素。 如果写成函数形式,其返回值为React元素,其为组件。 组件在命名上通常以大写字母开头。
const div = React.createElement('div',...)
const Div = ()=> React.createElement('div',...)
3.2.两种组件
<Welcome name="world">
函数组件与类组件都能够在引用的时候以如上方式引用。 函数组件会将Welcome标签内的属性与值,转化为键值对作为参数。props:{ name:'world' }
function Welcome(props){
return <h1>Hello, {props.name}</h1>
}
类组件也会将Welcome标签内的属性与值,转化为键值对作为参数。当组件在取值时,需要继承React.Component对象,从其props属性中取值。
class Welcome extends React.Component{
render(){
return <h1>hello, {this.props.name}</h1>
}
}
3.3.标签翻译
在JSX写入标签时,其会被翻译为js。 div标签会被翻译为React.createElement('div'),向createElement()中传入的为字符串,并创建标签。
welcome组件标签会被翻译为React.createElement(Welcome),向createElement()中传入的为Welcome函数。 React.createElement()根据接受的不同参数进行重载。 参数为字符串则创建对应标签。 参数为函数,则调用该函数获取其返回值。 参数为类,则会执行其构造器,并获取其组件对象,调用其render方法,再获取其返回值。
<div className="red", title="hi">Ogas</div>
//上述会被转译为:
React.createElement('div',{
className: 'red',
title: 'hi'
}, 'Ogas')
const Welcome = ()=> {return <div>hj</div>}
<Welcome>
var Welcome = function Welcome(){
return React.createElement('div',null,'hi')
}
React.createElement(Welcome,null)
3.4.数据父传子方式props
父组件向子组件传值时,需要使用props。 当父组件通过props传值给函数组件时,函数组件的第一个形参为props,props为一个对象,内部存储传入的值。
const theFather = ()=>{
const message = 'Are you winning son?'
return <>
<div>
这里是父组件。
<Son messageForSon={message}>
</div>
</>
}
const theSon = (props)=>{
return <>
<div>
{props.messageForSon}
Yes,my Dad.
</div>
</>
}
当父组件通过props传值给类组件时,属性会被放入类组件所继承的React.compoent类的props属性中,通过this.props.获取。
class Son extends React.component{
render(){
return <>
<div>
{this.props.messageForSon}
Yes,my Dad.
</div>
</>
}
}
3.5.类组件内部数据state
类组件在构造函数中的state内声明数据。构造函数的super()的作用是用于初始化执行构造函数,必须写入在构造函数中。
class Son extends React.component{
constructor(){
super()
this.state = {
n: 0
}
}
}
类组件通过this.state.获取声明在state内的数据。
class Son extends React.component{
constructor(){
super()
this.state={
n: 0
}
}
render(){
return <>
<div>
数值n:{this.state.n}
</div>
</>
}
}
类组件通过this.setState()函数修改state内的数据。 如果通过赋值方式修改state内的数据,不会被监听,而Vue能实现直接修改data的数据是因为其自动监听了开发者的赋值。
class Son extends React.component{
constructor(){
super()
this.state = {
n: 0
}
}
add(){
this.setState({ n: this.state.n + 1})
}
render(){
return <>
<div>
数值n:{this.state.n}
<button onClick={()=> this.add()}> +1 </button>
</div>
</>
}
}
this.setState()接受一个新的对象用于代替state,因此有如下写法。 该写法虽然在编写上简便,但是会违反React数据不可变的原则,因此不会用这种写法。
add(){
this.state.n += 1
this.setState(this.state)
}
对于React开发的熟练者,不会向setState()中直接传入对象,而是将一个函数作为参数传入setState()中。该函数返回一个对象。
add(){
this.setState((state)=>{
const n = state.n + 1
return {n}
})
}
setState()是一个异步的函数,不会第一个时间变更值,而采取如上写法能够第一时间获取到变更的n的值。 如下写法方式输出的n为变更前的值,无法第一时间获取到变更后的值。
add(){
this.setState({ n: this.state.n + 1 })
console.log(this.state.n)
}
3.6.函数组件内部数据state
在声明定义数据的同时需要声明更改其值的函数。 React.useState()用于定义state数据,其接受的参数为数据值,并返回一个数组。 数组的第一项n用于读数据,为只读数据。 数组的第二项setN为函数,用于修改数据。 setN()其接受的参数为改变后的n值。 setN()与this.setState()都是异步改变数据,但setN()本质上不会真正改变n的值。
const Son = () => {
const [n,setN] = React.useState(0)
return <>
<div>
数值n:{n}
<button onClick={() => setN(n+1)}> +1 </button>
</div>
</>
}
3.7.setState()特性
setState()中的对象仅有一部分属性修改,其他属性则会沿用旧值。
this.state = {
n: 0,
m: 0
}
this.setState({n: this.state.n + 1})
//m并不会因为传入的对象内没有属性m而为undefined,而是沿用m: 0。
3.8.函数组件使用useState()声明对象
React.useState()内部的参数传入一个对象,即声明一个对象
const [state,setState] = React.useState({
n: 0,
m: 0
})
当通过useState()声明对象时,setState()修改对象内其中一个属性时,其他属性不会沿用旧值,其不具备setState()的特性。
<button onClick={() => setState({n:state.n + 1})}>
而解决此问题的方式时,在修改部分属性时先拷贝原有对象的属性。
<button onClick={() => setState({...state,n:state.n+1 })}>
3.9.类组件的state内数据为对象的情况
类组件的state内的属性为对象时,修改user.name,user.age不会沿用旧值。
class Son extends React.Component {
constructor(){
super;
this.state = {
user:{
name: 'Ogas',
age: 18
}
}
render(){
return <>
<div>姓名: {this.state.user.name}</div>
<div>年龄: {this.state.user.age}</div>
<button onClick={()=> this.setState({user:{
name: 'Unclotho'
}})}>更改姓名
</button>
</>
}
}
}
其解决方式依然是使用...运算符拷贝对象内的属性。
<button onClick={
()=> this.setState({
user:{
...this.state.user,
name: 'jack'
}
})
}>
4.1.类组件事件绑定方式演变过程 可不看
下面是事件绑定动作函数的四种写法,其中第一种写法是泛用的,后三种写法存在弊端。
addN(){
this.setState({n: this.state.m + 1})
}
<button onClick={() => this.addN()}> n+1 </button>
<button onClick={this.addN}> n+1 </button>
<button onClick={this.addN.bind(this)}> n+1 </button>
this._addN = () => this.addN()
<button onClick={this._addN}> n+1 </button>
第二种写法中addN内的this是指向window,此时函数内部有this.setState()这样的语句,会引发this指向错误。 第三种写法中相当于对第二种写法的修正,重新修正了this指向,但太麻烦。 第四种写法相当于对第一种写法取别名,但依然是编写麻烦的。 下面是对于事件绑定的最优写法
constructor(){
this.addN = ()=> this.setState({n: this.state.n + 1})
}
render(){
return <button onClick={this.addN}> n+1 </button>
}
将事件绑定的动作函数写入到构造器中,此时在事件绑定时可以直接写入this.addN。 写入在构造器内的this.addN是一个箭头函数,其不会引发this指向的变更。 将动作函数写成箭头函数形式不会引发this变更,因此jsx给出了在类组件写入箭头函数的语法。
addN = ()=> this.setState({n: this.state.n + 1})
<button onClick={this.addN}> + 1 </button>
4.2.类组件事件绑定最优写法
class Son extends React.component{
constructor(){
super()
this.state = {
n: 0
}
}
add = () => {
this.setState({ n: this.state.n + 1})
}
render(){
return <>
<div>
数值n:{this.state.n}
<button onClick={this.add}> +1 </button>
</div>
</>
}
}
将动作函数写为箭头函数,事件绑定动作函数时不需要加括号。
5.1.类组件详解
5.1.2.类组件创建方式
class B extends React.Component{
constructor(props){
super(props)
}
render(){
return <>
<div> hi </div>
</>
}
}
export default B
5.1.3.类组件props
父组件可以将其变量与函数的引用传入子组件的props。 传入子组件的变量会被包装为一个对象,该对象为props。
class Parent extends React.Component {
constructor(props){
super(props)
this.state = {name:'frank'}
}
onClick = ()=>{}
render(){
<B
name={this.state.name}
onClick={this.onClick}
>hi</B>
}
}
子组件的constructor接受props。
constructor(props){
super(props)
}
父组件只会将引用传给子组件,因此props是只读的,不允许更改,能够完成的更改不过是使引用不可用。 通过this.props.xxx = 'something'的方式去更改,不符合规范,组件的数据更改只允许由组件本身去修改,父组件传给子组件的数据应当由父组件更改。
5.1.3.componentWillReceiveProps钩子
componentWillReceiveProps函数(以下简称cwrp钩子函数)是一个写入在组件内的钩子函数。 其有两个形参nextProps与nextContext。 用于监视props的变化,当props变化后会触发该钩子函数。
componentWillReceiveProps(nextProps, nextContext){
console.log(this.props)
//旧的props
console.log(nextProps)
//新的props
}
在cwrp钩子函数内部,this.props是旧的props,因此props变动触发钩子在写值前。 第一形参nextProps是新的props。 cwrp钩子函数已经因为其存在问题被不推荐使用,因此更名为UNSAFE_componentWillReceiveProps。
5.2.函数组件详解
5.2.1.创建方式
使用箭头函数声明与函数组件声明的方式都能完成函数组件的声明。
const Hello = (props) => {
return <div>{props.message}</div>
}
function Hello(props){
return <div>{props.message}</div>
}
5.2.2.函数组件与类组件的差异
函数组件没有state。 函数组件没有生命周期。 函数组件的过简会使其可控性比类组件要弱。 没有生命周期意味着没有钩子函数用于调试。 同时也说明函数组件更加简便。 React v16.8推出了Hooks API用于加强函数组件。 例如useState、useEffect。
6.1.React生命周期
在推出Hooks前仅有类组件有生命周期,因此以类组件为例。
let div = document.createElement('div')
div.textContent = 'hi'
document.body.appendChild(div)
div.textContent = 'hi2'
div.remove()
上述是一段DOM操作,可以反应出生命周期的过程。 声明变量创建节点。 向节点内写入内容,引入数据。 向节点内写入节点,挂载节点。 更改节点内容,修改数据。 删除节点。 React的生命周期符合这样一个大致流程。
create / construct
state init
mount
update
unmount
创建构造组件 初始化state数据 挂载视图节点 更新state数据渲染视图 组件卸载
6.2.React生命周期钩子函数
组件在创建后,调用下面函数。
constructor()
组件在需要更新时,调用下面函数。 函数接收两个形参:nextProps为更新后的props,nextState为更新后的state。 当函数返回值为false阻止此次更新,返回值为true时允许此次更新,且一定要返回bool。
shouldComponentUpdate(nextProps, nextState)
用于完成组件的渲染。 当视图的html只有一个根元素,使用()包裹。 当视图的html有多个根元素,不能使用()包裹,需要使用<React.Fragment>包裹,可以简写为<></>
render()
组件在完成节点挂载后,调用下面函数。 该函数一般用于获取节点挂载之后的信息或进行操作。 例如在节点挂载后对节点进行DOM操作。 请求的发起一般写入在该函数中。
componentDidMount()
组件在完成数据更新后,调用下面函数。 请求的发起一般写入在该函数中。 在该函数内写入setState会引发循环。 当函数返回值为false时,不会执行函数内容。 分别拥有两个形参,第一形参prevProps为旧的props,第二形参prevState为旧的state。
componentDidUpdate()
组件在确定卸载,执行卸载前,调用下面函数。 一般在该函数中进行先前建立的监听的取消,计时器的消除,请求的取消。
componentWillUnmount()
7.useState原理
7.1.setN()如何渲染视图
const [n,setN] = React.useState(0)
函数组件中是使用useState去声明内部数据,通过useState()得到数据的读写。 React页面更新渲染是通过比对虚拟DOM,决定是否更新。 而setN()能够完成在视图上的更新渲染。 因此setN()执行render()。 而setN()的渲染是直接调用函数组件进行的。 例如在App组件中调用setN(),setN()则会通过调用App()的方式渲染。
7.2.setN()如何修改数据
从感观上看setN()对n进行了修改,但实际过程并非如此。 setN()对n的修改方式是,将自身的参数传给useState()。 useState()接受setN()的参数作为自己的参数后,执行而完成了对n的修改。 setN()会帮助useState()接受参数,而存放这些参数的变量为state。 即setN()异步调用render(),而调用render()会重新执行useState()。 在重新执行useState()时,其接受的参数来自setN()所接受的参数。 这个参数就是函数组件的state。
7.3.state的特点
上述提及修改数据会重新调用函数组件进行渲染。 这会引发一个问题,如果state作为函数内的数据,重新调用函数组件,是否会初始化state。 显然是不会的,如果每次调用都初始化state就没有办法完成对数据的修改。 因此state相当于是一个写入在函数组件外的变量。
7.4.useState与if的问题
组件用多个变量需要多次调用useState()时。 多个变量的n,setN会被存储入state中,意味着state是一个数组。 由此useState的调用顺序必须需要与前一次渲染一致。
const [n,setN] = React.useState(0)
const [m,setM] = React.useState(1)
const [x,setX] = React.useState(2)
当上一次渲染的useState执行顺序为n,m,x时。 其在数据更新后的执行useState的顺序也必须是n,m,x保持一致。 因此React.useState()是不能够写入在条件中,会引发执行顺序不一致的问题。
8.useRef()
8.1.useRef()变量声明及读写方式
如下声明一个ref变量,变量名为refN,其值为useRef()的参数0。 声明方式与useState()相似,但不给出写值的setN函数。
const refN = React.useRef(0)
当ref变量需要写值时,通过给变量的.current属性赋值的方式实现。
refN.current += 1
useState()中提供的setN能够完成视图渲染。 但useRef提供的.current不会渲染视图
8.2.ref与state两者差异
useState在做变量修改时,实际上是用新的变量获取新的值,更改引用为新的变量。 因此state的变量修改并不是对原有变量进行修改。 而ref是对原有的变量进行更改。
8.3.通过.current写值并触发渲染的方式
在通过.current改值时,同时触发API去渲染,但React没有直接提供这样的API。 state的变量是通过setN来渲染的,ref变量可以利用setN完成更新。
const nRef = React.useRef(0)
const [n,setN] = React.useState(null)
onClick={
()=>{
nRef.current += 1
setN(nRef.current)
}
}
向useState()内传入null作为参数,这样state变量n是不存在的。 我们可以将setN取出作为更新nRef的方式。 n是不需要的,因此在取出setN时采用另一种写法。
const nRef = React.useRef(0)
const update = React.useState(null)[1]
onClick = {
()=>{
nRef.current += 1
update(nRef.current)
}
}
这样获得了一个名为update的函数用于更新ref变量。 当哪个ref变量通过.current写值后,调用update函数以其.current为参数。 就能够实现ref变量的渲染。
9.useContext
useContext提供了<.Provider>标签,用于标记一块区域提供变量。 标签名可以自定义。 该标签的属性value用于接受state变量,及其变量的set函数。 标签内部能够使用value提供的变量内容,并且内部引用的子组件也能够使用。
function App(){
const {theme, settheme } = React.useState('red')
return <>
<themeContext.Provider value={ {theme, settheme} }>
<ChildA />
<ChildB />
</themeContext>
</>
}
被调用的子组件获取父组件<.provider>提供的变量需要使用useContext()结合析构语法。 useContext()接受的参数为.Provider标签名。
fucntion ChildA() {
const { setTheme } = React.useContext('themeContext')
}
10.函数组件没有生命周期钩子的问题
类组件提供了多样的生命周期钩子用于开发的调试。 而函数组件并不存在这样的生命周期钩子。 自React v16.8.0后,推出了useEffect用于模拟生命周期钩子。
craete constructor(props)
state init shouldComponentUpdate(nextProps, nextState) render()
mount componentDidMount()
update componentDidUpdate()
unmount componentWillUnmount()
上述列出了类组件在其生命周期阶段对应的常用钩子函数。 可以用useEffect()模拟以下三个钩子函数。 componentDidMount()、componentDidUpdate()、componentWillUnmount()
10.1.componentDidMount()
useEffect(()=>{
console.log('挂载了组件')
},[])
useEffect()内接受第一个形参为一个函数,在组件更新时则会调用。 接受的第二个形参为[]时,则useEffect()只会在组件初次挂载时调用。 由此通过uesEffect()的第二形参为[],在函数组件中模拟了componentDidMount()。
10.2.componentDidUpdate()
const [n,setN] = useState(0)
useEffect(()=>{
console.log('n更新了')
},[n])
useEffect(()=>{
console.log('n或者m更新了')
},[n,m])
useEffect()接受的第二形参为包含state变量的数组时。 当数组内的任意state变量更新,则会调用第一形参。 当第二形参不传入时,意味着组件更新时则会调用第一形参。 即任意变量更新都会调用第一形参。 使用该方式模拟componentDidUpdate(),会在变量初始化的时候触发一次。 如果需要变量初始化时不触发,则需要进行计数。
const [n,setN] = useState(0)
const [nUpdateCount, setNUpdateCount] = useState(0)
useEffect(()=>{
setNUpdateCount(nUpdateCount => nUpdateCount + 1)
},[n])
useEffect(()=>{
if(nUpdateCoutn > 1){
console.log('n更新了')
}
},[n])
通过uUpdateCount变量为n的更新计数,当第一次初始化时不触发模拟的钩子函数。
10.3.componentWillUnmount()
useEffect(()=>{
return ()=>{
console.log('组件销毁了')
}
})
当useEffect()接受的第一形参的返回值为一个函数时。 组件销毁时会调用该函数。
10.4.模拟componentDidUpdate()的优化封装
const [n,setN] = useState(0)
const [nUpdateCount, setNUpdateCount] = useState(0)
useEffect(()=>{
setNUpdateCount(nUpdateCount => nUpdateCount + 1)
},[n])
useEffect(()=>{
if(nUpdateCoutn > 1){
console.log('n更新了')
}
},[n])
如果每个变量都需要准确的模拟该钩子,则需要为每个变量设置计数。 可以将其优化封装为一个所有变量通用的更新计数器。
const useX = (n) => {
const [nUpdateCount, setNUpdateCount] = useState(0)
useEffect(()=>{
setNUpdateCount( nUpdateCount => nUpdateCount+1 )
}, [n])
return {
nUpdateCount, setNupdateCount
}
}
const { nUpdateCount, setNupdateCount } = useX(n)
useEffect(() => {
if(nUpdateCount > 1){
console.log('n更新了')
}
},[nUpdateCount])
我们将为n设置计数器的部分用useX()封装。 因为useX()内使用到了useEffect()这样的hook函数,所以其函数名需要以use开头。 因为外部需要获取计数,需要将nUpdateCount返回,外部用析构语法取出。 而useX()的参数为需要被计数的变量。
10.5.进一步优化封装模拟componentDidUpdate()
useX对需要监视的变量n进行监听,抛出计数。 而useEffect由对useX抛出的计数nUpdateCount进监听,完成动作函数。 可以将useEffect的动作函数作为参数传入useX,而免除对于计数的监听与计数的传递。 而外部只需要传入被监听的变量及动作函数即可。 将useX更名为useUpdate,由此封装了一个通用的模拟componentDidUpdate()接口。
const useUpdate = (n, fn) => {
const [nUpdateCount, setNUpdateCount] = useState(0)
useEffect(()=>{
setNUpdateCount( nUpdateCount => nUpdateCount+1 )
}, [n])
useEffect(() => {
if(nUpdateCount > 1){
fn()
}
},[nUpdateCount])
return {
nUpdateCount, setNupdateCount
}
}
const [n,setN] = useState(0)
const nUpdateFunction = ()=> {
console.log('n更新了')
}
useUpdate(n, nUpdateFunction)
在使用hook提供的函数时,例如useEffect()。 其第一形参为动作函数,第二形参为存放在数组的变量。 因此如果需要符合这一规范,需要对useUpdate进行修改。
const useUpdate = (fn, array) => {
const [nUpdateCount, setNUpdateCount] = useState(0)
useEffect(()=>{
setNUpdateCount( nUpdateCount => nUpdateCount+1 )
}, [...array])
useEffect(() => {
if(nUpdateCount > 1){
fn()
}
},[nUpdateCount])
return {
nUpdateCount, setNupdateCount
}
}
const [n,setN] = useState(0)
const nUpdateFunction = ()=> {
console.log('n更新了')
}
useUpdate(nUpdateFunction, [n])
由此封装了一个符合hook函数传参规律,准确模拟componentDidUpdate()的接口。 useUpdate()会报出一个警告,来源是其中array参数无法确保其为数组。 常规的处理方式是通过/eslint/去隐藏这个警告。 或者为了确保稳定性将第二形参的类型更改为单个参数,而不是参数数组。
11.Hooks列举
状态:useState
副作用:useEffect useLayoutEffect
上下文:useContext
Redux:useReducer
记忆:useMemo useCallback
引用:useRef useImperativeHandle
自定义:Hook useDebugValue
12.useState
12.1.state对象变量无法局部更新
const [n,setN] = React.useState(0)
const [user,setUser] = React.useState({name: 'Ogas'})
state可以是任何数据类型,但state变量为对象时存在一些特点。 当state变量为对象类型时,对象无法局部更新。
const [user,setUser] = React.useState({
name: 'Ogas',
age: 18
})
setUser({
name: 'Unclotho'
})
目前知道上述写法setUser()传入的对象属性不全,useState不会自动补全属性。
setUser({
...user
name:'Unclotho'
})
采用...语法,先拷贝原有的属性,在对拷贝的属性覆盖,实现了局部更新。
12.2.state对象变量引用变更
setState(obj)
通过setState去更新对象时,对象的引用也会变更。 即用新的对象取代了旧的对象。 如果state对象变量的引用没有变更,则React认为其数据没有变化。
12.3.useState()传入函数
const [user,setUser] = useState(
() => {
return {
name: 'Ogas',
age: 18
}
}
)
当useState声明state变量,这个变量初始化过程复杂时。 可以传入一个函数,将该函数的返回值作为参数。
const [age,setAge] = useState({age: 9+9})
const [age.setAge] = useState(()=> ({age: 9+9}))
上述情况两种写法将会有细微的差别。 直接传入对象的写法,代码每次运行到state时,都会计算9+9、 而传入函数的写法,代码只会在第一次计算9+9,并将结果记录,下次沿用上次结果。
12.4.setState()传入函数
const onClick = ()=>{
setN(n + 1)
setN(n + 2)
}
上述模拟在一个动作函数中,需要连续使用两次setState作两次变量更新的情况。 目前我们知道,setN()对变量的更新并非是即刻生效的。 因此连续多次的setState()只有最后一次是生效的。
const onClick = ()=>{
setN(n => n+1)
setN(n => n+2)
}
当setN传入函数时,该函数会接受一个形参,即为n。 此时setN()传入的函数会被记录,在正式更新state变量时执行。 因此连续的setState(),传入的参数为函数时能够生效。 理论上setState()传入函数的更新state方式才是更泛用的写法。
13.useReducer
useReducer可以用Flux/Redux思想进行变量操作。 该思想将变量的操作分为了四步:
- 创建初始值 - initial
- 创建所有操作 - reducer
- 获得读写API - useReducer
- 调用读写操作类型 - state/dispatch
const initial = {
n: 0
}
在initial中完成对Reducer变量的声明,并赋初始值。
const reducer = (state, action) => {
if(action.type === 'add'){
return {n: state.n + 1}
}else if(action.type === 'multi') {
return {n: state.n*2}
}else {
throw new Error('未知操作')
}
}
在reducer中完成对操作的定义。 形参state为initial声明的变量。 形参action为操作类型。 通过对action操作类型进行条件判断重载reducer的返回值。
const [state, dispatch] = useReducer(reducer, initial)
将reducer与initial传给useReducer。 通过析构语法得到state与dispatch。
const {n} = state
从state中可以读reducer变量。
const onClick = () =>{
dispatch({type:'add'})
}
在dispatch中传入action,完成对变量的操作。 Redux的变量思想将变量与操作分离, 并将操作集合聚集。 表单中的变量操作是固定或有限的, 并且一份表单数据可以对应一份reducer, 因此Redux变量思想很符合表单的特性。