我的带薪学习React笔记

前言:

学习一个新东西的开始就是先运行跑起来,跟着官网敲了一遍棋盘游戏 使用JSX而不是JS

接触到:

① props传递数据 ② 为避免this困扰 用箭头函数 ③ 构造函数以super(props)开头 ④ this.state去记忆 ⑤ React Devtools

  • 为什么不用js而是用jsx

    1
    2
    3
    4
    5
    6
    // 1.jsx
    const VDOM = (
    <h1 id="title">
    <span>Hello</span>
    </h1>
    )
    1
    2
    // 2.js
    const VDOM = React.createElement('h1',{id:'title'},React.createElement('span',{},'Hello'))

    jsx经过babel翻译之后,变成js的格式,可以被识别

    即jsx创建虚拟DOM是js这种创建方式的语法糖

  • 虚拟DOM VS 真实DOM

    关于虚拟DOM:
    1.本质是Object类型的对象
    2.虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性
    3.虚拟DOM最终会被React转化为真实DOM,呈现在页面上

  • jsx语法规则

    1.定义虚拟DOM时,不要写引号

    2.标签中混入JS表达式时要用{}

    3.样式的类名指定不要用class,要用className

    4.内联样式,要用双括号key:value对的形式去写

    5.只有一个根标签,且必须闭合

    6.标签首字母

    (1)若小写字母开头,则将标签转为html中的同名元素,若html中无该标签对应的同名元素,则报错。虽然报错,但是不干扰其他元素的显示【报错信息:good不能被识别,您是否是在用组件,组件应该大写字母开头】

    (2)若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。报错且页面空白【报错信息:找不到这个组件】

  • 一个小案例:以li的方式动态显示数据

    1
    2
    3
    4
    //数据是array   在VDOM中 
    data.map((item,index)=>{
    return <li key={index}>{item}</li>
    })

    要记得写index,类似vue 就是diff算法 需要一个key

1. 基础语法

1.1 组件

1.1.1创建组件的两种方式
  • 函数式组件 【简单组件】
    hooks
1
2
3
4
5
6
7
//1.创建函数式组件  因为是组件所以首字母应该大写 记得return
function Mycomponent(){
console.log(this); //此处this是undefined 因为babel编译后开启了严格模式
return <h1>hello function component</h1>
}
//2.渲染
ReactDOM.render(<Mycomponent/>,document.getElementById("test"))

执行渲染过程:
1.React解析组件标签,找到Mycomponent组件
2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转真实DOM,随后呈现在页面上

  • 类式组件 【复杂组件】复杂就是有state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyComponent extends React.Component {
//构造器只在初始化的时候执行一次
constructor(props) {
super(props)
this.state = { null }
}
//render执行 1+n次
render() {
//render是放在MyComponent的原型对象上,供实例使用
console.log('render中的this:',this); //this是MyComponent组件的实例对象
return <h2>class component</h2>
}
}
ReactDOM.render(<MyComponent/>,document.getElementById('test'))

执行了ReactDOM.render(<MyComponent/>)之后,发生类什么?

 1.React解析组件标签,找到MyComponent组件

 2.发现组件是类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法

3.将返回的虚拟DOM转为真实DOM,随后呈现在页面上
1.1.2组件的三大属性
  • state
  • props
  • refs与事件处理

1.2 state

  • 绑定事件监听

    <h onClick={demo}></h>

    function demo() {}

  • 一个小案例:点击切换页面数据(今天天气hot?cool)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    class MyComponent extends React.Component {
    constructor(props) {
    super(props)
    this.state = { isHot: false }
    //改变this
    this.changeWeather = this.changeWeather.bind(this)
    }
    render() {
    const {isHot} = this.state
    return <h2 id="title" onClick={this.changeWeather}>今天天气很{isHot ? 'hot' : 'cool'}</h2>
    }
    changeWeather() {
    //响应事件写在外面的话 这个地方写this this是undefined 因为babel开启了严格模式 就算不开启严格模式 this也是windows 调不到this.state.isHot
    //现在我将它写到了类的里面 作为方法 结果this为什么还是undefined
    //这是因为changeWeather是作为onClick的回调 所以不是通过实例调用 是直接调用
    //出现undefined 是因为类中的方法默认开启了局部的严格模式

    //解决方式 去通过bind改变this

    //获取原来的isHot值
    const isHot = this.state.isHot
    //严重注意:state不能直接更改 通过一个内置的API去更改
    this.setState({isHot:!isHot})
    }
    }

    ReactDOM.render(<MyComponent/>,document.getElementById('test'))

    即:

    1.通过事件去改变状态 1)要在构造函数中通过响应事件去bind(this) 并把结果赋给onClick的回调          2)箭头函数
    
    2.开启严格模式的话 this会由window变为undefined 。1)类中的方法会开启 2)babel会开启
    
    3.不能直接更改state,需要通过this.setState去更改(合并操作,不会影响this.state里的其他属性)

    进行简化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class MyComponent extends React.Component {
    constructor(props) {
    super(props)
    }
    state = { isHot: false }

    render() {
    const {isHot} = this.state
    return <h2 id="title" onClick={this.changeWeather}>今天天气很{isHot ? 'hot' : 'cool'}</h2>
    }
    changeWeather = ()=>{
    const isHot = this.state.isHot
    this.setState({isHot:!isHot})
    }
    }

    ReactDOM.render(<MyComponent/>,document.getElementById('test'))

1.3 props

1
2
3
4
5
6
7
8
9
10
//对标签属性进行类型,必要性限制
static propTypes = {
name:PropTypes.string.isRequired, //限制名字为string且不能为空
age:PropTypes.number,
speak:PropTypes.func
}
//指定默认标签属性
static defaultProps = {
sex: '未知'
}

进行限制的时候,number string 这些首字母应该小写 否则会与内部的Number,String 产生冲突

function要避免冲突 写为func

function因为this的原因 不能玩state和refs 但是可以玩props。(因为props本身可以作为function的参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(props) {
const {name,age,sex} = props
return (
//
)
}

Person.propTypes = {
//
}
Person.defaultProps = {
//
}

1.4 refs

勿过度使用,其实用别的方式也可以访问DOM或某个组件的实例

Sring的Refs是过时的 不建议使用 存在效率问题 官方推荐:用回调函数或createRef API

  • 回调函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Demo extends React.Component{
    //展示数据
    showData = () => {
    const {input1} = this
    console.log(input1.value);
    }
    render(){
    return(
    <div>
    <input ref={currentNode => this.input1 = currentNode} type="text" placeholder="点击按钮提示数据"/>&nbsp;
    <button onClick={this.showData}>click</button>&nbsp;
    </div>
    )
    }
    }

    ref写回调的含义:拿到当前ref所在的节点 react帮着调这个函数传进去 挂到this实例自身的属性input1上

    上面写的是内联函数,更新过程会被执行2次 第一次null 第二次传入参数DOM

    可以通过类绑定函数的方式避免这个问题,类绑定函数避免这个问题。大多数情况下都是无关紧要的。

  • createRef API

    React.createRef()调用后可以返回一个容器,该容器可以存储被ref所标识的节点,该容器是“专人专用”

    1
    2
    3
    4
    myRef = React.createRef() //创建容器
    console.log(this.myRef.current.value); //拿节点的值

    <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>

1.5 事件处理

(1)通过onXxx属性指定事件处理函数(注意大小写)

a. React使用的是自定义(合成)事件,而不是使用的原生DOM事件-----为了更好的兼容性

b. React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)-----为了更高效

(2)通过event.target得到发生事件的DOM元素对象

ps: 如果数据和事件在同一个节点上 那就可以用这种方式拿数据 可以不用ref

1.6 处理表单

非受控组件 VS 受控组件

非受控组件:

1
2
3
4
5
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username = c} type="text" name="username" />
密码:<input ref={c => this.password = c} type="password" name="password" />
<button>登录</button>
</form>

受控组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//保存用户名到状态
saveUsername = (event)=>{
this.setState({username:event.target.value})
}
//保存密码到状态
savePassword = (event)=>{
this.setState({password:event.target.value})
}

<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username" />
密码:<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>

(1)非受控组件的每一个数据项都要用ref

(2)受控组件的数据可以维护到状态中,也可以从状态中取出来(感觉类似vue的双向绑定)

官网建议用 受控组件

  • 一个收集表单数据的小案例

    • 用高阶函数+函数柯里化的方式收集
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    //创建组件
    class Login extends React.Component {
    state = {
    username:'',
    password:''
    }

    //保存到状态 高阶函数+函数柯里化
    saveFormData = (dataType)=>{
    return (event)=>{
    console.log({[dataType]:event.target.value});
    }
    }

    handleSubmit = (event)=>{
    event.preventDefault() //阻止表单提交
    const {username,password} = this.state
    console.log(`用户名:${username.value},密码:${password.value}`);
    }
    render(){
    //this.saveFormData() 将函数返回值作为onChange回调
    //this.saveFormData 将函数作为onChange回调
    return(
    <form onSubmit={this.handleSubmit}>
    用户名:<input onChange={this.saveFormData('username')} type="text" name="username" />
    密码:<input onChange={this.saveFormData('password')} type="password" name="password" />
    <button>登录</button>
    </form>
    )
    }
    }
    ReactDOM.render(<Login/>,document.getElementById('test'))

    为了抽象出方法 简化代码

    将保存信息的方法进行抽象 —–> 为了区别保存的是谁 —–> 所以将ID作为参数 —–> 因为写了括号 会将函数返回值作为onChange的回调 —–> 所以函数体里面return一个箭头函数 —–> 最后执行的是return里面的内容

  • 也可以不用柯里化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //保存到状态 不用函数柯里化
    saveFormData = (dataType,event)=>{
    this.setState({[dataType]:event.target.value})
    }
    render(){

    return(
    <form onSubmit={this.handleSubmit}>
    用户名:<input onChange={ event => this.saveFormData('username',event) } type="text" name="username" />
    密码:<input onChange={ event => this.saveFormData('password',event) } type="password" name="password" />
    <button>登录</button>
    </form>
    )
    }

    因为onChange需要一个函数—–> 所以()=>{} 通过react去调用 —–> 拿到event.target.value —–> 再把value作为参数传入this.saveFormData去调用

1.7 生命周期

  • 一个小案例:文字按每次0.1渐隐,到opacity为0的时候,再变为1。并且保证按钮可以正常点击,去卸载组件

处理渐隐渐显 和 卸载组件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
death = ()=>{
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}

//组件挂载完毕
componentDidMount(){
setInterval(() => {
//获取原状态
let {opacity} = this.state
//减小
opacity -= 0.1
if(opacity <= 0) opacity=1
//设置新的透明度
this.setState({opacity})
},200)
}

定时器不能放在render

因为render会调用1+n次 ,this.setState({opacity})会无限的调render —-> 就炸了

渐隐和渐显的效果可以出来:

请假条

但是再去点击按钮的时候:

在卸载组件上更新state报错

报错信息:不能在卸载组件上执行状态更新

原因是:按钮是触发了 death 的回调去卸载组件 而componentDidMount里的定时器还在更新state

解决:在卸载之前要先去clear定时器

现在的问题就变成了怎么去清除:通过绑定给this一个属性 将该属性作为定时器的ID去清除

  • 方式一:放在death中 卸载前

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    death = ()=>{
    //清除定时器
    clearInterval(this.timer)
    //卸载组件
    }
    componentDidMount(){
    this.timer = setInterval(() => {
    //
    },200)
    }
  • 方式二: 放在componentWillUnmount中

    1
    2
    3
    4
    5
    //组件将要卸载
    componentWillUnmount(){
    //清除定时器
    clearInterval(this.timer)
    }

总结一下生命周期回调函数:

生命周期

2. 进阶语法

2.1 虚拟DOM

数据—–>虚拟DOM—–>真实DOMß

  • diff比较规则:

    a.旧DOM中找到了与新DOM相同的key:

    ​ i)若虚拟DOM的内容没变,直接使用之前的真实DOM

    ​ ii)若虚拟DOM的内容变了,替换

    b.没有找到相同的key:根据数据创建新的真实DOM,随后渲染到页面

  • 用index作为key可能会出现问题:

    • 效率问题:2000条数据前面加一条,用index将位置作为key ,会造成2001条都要进行更新(显然这是一个大问题,对于原来的2000条数据更新是完全没有必要的)
    • 结构中若包含输入类DOM:产生错误的DOM更新,使得输入框的DOM信息错误,界面有问题

因此index不建议去唯一标识数据

2.2 脚手架

使用在html里面写script引入react核心库,react-dom,babel。然后写type='text/babel' 让浏览器去将写的jsx翻译为js 。这种方式可能会出现一个问题:白屏(代码量大 翻译不过来)

官网的环境配置流程:

1
2
3
npx create-react-app 项目名
cd 项目名
npm start

npm run build 会生成优化版本

jsx文件,安装ES7…插件之后

  • rcc 类定义的组件
  • rfc 函数定义的组件

2.3 react ajax

React本身只关注界面,并不包含发送ajax请求的代码。即没有React.get(url)这种写法

可以通过axios或者fetch去处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetch('url').then(
response => {
console.log('联系服务器成功了');
return response.json() //作为下一个then的实例
},
/*
error => {
console.log('联系服务器失败了',error);
return new Promise(()=>{}) //拦截 不进行下一个then
}
*/
).then(
response => { console.log('获取数据成功了',response); },
//error => { console.log( '获取数据失败了',error ); }
).catch(function(error) {
console.log('request failed',error);
})

2.4 组件通信

  • 父向子:通过props

    1
    2
    父:return (<Child fatherToChild = {data} />)
    子:{this.props.fatherToChild}
  • 使用context可以避免中间元素传递

  • PubSub

和vue里用的消息订阅发布是同一个js库,微信小程序也可以用

1
2
3
4
5
6
//订阅消息  常写在componentDidMount
var token = PubSub.subscribe('事件名',事件)
//发布消息
PubSub.publish('事件名','消息')
//取消订阅
PubSub.unsubscribe(token)

2.5 React路由

  • SPA

    单页面应用,只有一个完整页面,只做局部更新,多组件

  • 路由基本使用:

    1.界面中的导航区,展示区

    2.导航区的a标签改为Link <Link to='/xxx'></Link>

    3.展示区写Route标签进行路径的匹配<Route path='/xxx' component={Demo}/>

    4.<App>最外层包裹

安装:npm i react-router-dom

vue里面是直接叫vue-router

路由:Route

路由器:Router

  • 靠路由链接实现切换组件(编写路由链接):<Link to="/about"></Link>

  • 注册路由:<Route path="/about" component={About} />

  • 最好一个页面用一个去包裹,所以经常就是在index.js里渲染的时候 包裹一下<App/>

注意:

如果直接用Link(或Route)的话 就会出现一个报错(You should not use Link outside a Router)

此时 如果用Router标签去包裹Link 依旧会报错 指向的是Router.js里面的代码

其实 它这个报错信息并不完整 所以造成理解偏差 导致会想着用将Link写到Router标签里面解决

真正的报错是在Router标签

BrowserRouter ///

HashRouter #

要说明是那种Router

路由组件 VS 一般组件:

1.写法不同

一般组件用闭合标签。路由组件用注册路由的方式

2.存放位置不同

一般组件:components. 路由组件:pages

3.接收到的props不同

一般组件:传递了什么 就收到什么

路由组件:接收到路由器传来的3个固定的属性:history,location,match,

路由的严格匹配与模糊匹配:

1.默认使用的是模糊匹配(输入路径必须包含要匹配的路径,且顺序要一致)

2.开启严格匹配exact={true}

3.严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由

都匹配不了的时候 一般在最后写个Redirect. 跳转到Redirect指定的路由

向路由组件传递参数:

1.params参数

2.search参数

3.state参数

  • React-router-dom 相关的API

    • 内置组件

      1
      2
      3
      4
      5
      6
      7
      <BrowserRouter>
      HashRouter
      Route
      Redirect
      Link
      NavLink
      Switch
    • 其他

      history对象

      match对象

      withRouter对象

3. Redux

是一个专门用于做状态管理的JS库。可以在react,angular,vue等项目中,但跟react配合较多。因为vue里面也有优秀的处理共享状态的库Vuex

3.1 三个核心概念

  • action

    type:标识属性,值为字符串,唯一,必要属性

    data:数据属性,值为类型任意,可选属性

  • reducer

    用于初始化状态,加工状态

    加工时,根据旧的state和action 产生新的state的纯函数

  • store

    将state,action,reducer联系在一起的对象

3.2 原理图

redux工作原理

  • 安装 npm add redux

一个有趣的地方:rendux改变状态之后 不进行render 所以我们要去手动调render

1
2
3
4
5
6
7
componentDidMount(){
//检测redux中的状态变化,只要变了 就调render
store.subscribe(()=>{
//假动作 看着去更新state 其实并没有state 更的是空对象 实际是想通过这行代码去骗render去执行
this.setState({})
})
}

但是如何有100个组件,每个组件里面都要写一个componentDidMount去调render吗????

优化:

1
2
3
4
//在index.js里面进行检测更新 
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})

但是这样写的话 如果3000个组件中只有一个变化了 其余2999个也会更新

其实没事 因为render有DOM的diff算法

一个小tips:返回函数:()=>({})

  • action 分为同步 和 异步

    同步是 Object。异步是返回function类型的

    action只能接受Object的 要是写异步的 需要再写一个中间件

    安装redux-thunk:npm add redux-thunk

    在store.sj中引入thunk。再从redux中导入applyMiddleware,最后一起暴露出去

4.react-redux

安装:npm add react-redux

c

  • connect(mapStateToProps,mapDispatchToProps)(CountUI)

  • Provider

    1
    2
    3
    4
    5
    6
    7
    RenderDOM.render(
    //react-redux 不需要自己去检测更新了 而且通过Provider可以批量为组件传递store
    <Provider store={store}>
    <Demo1></Demo1>
    <Demo2></Demo2>
    </Provider>
    )
  • 汇总reducer 用 combineReducers

redux开发者工具:Redux DevTools

chrome安装插件之后 去vscode安装 npm add redux-devtools-extension

然后去store.js里引入

import { composeWithDevTools } from 'redux-devtools-extension'

截屏2021-07-04 下午4.03.02

5.Hooks

前面提到纯函数组件只能做UI,涉及到状态的话,就只能用类组件或者redux
类组件的缺点:遇到简单的页面,代码就会很重,并且每创建一个类组件,都要去继承一个React实例
至于Redux,很久之前Redux作者就说过,“能用React解决的问题就不用Redux”

React Hooks的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码钩进来

4种常见的钩子:

  • useState() 状态管理
    const [name, setName] = useState(null)

  • useContext() 共享状态
    React16.X以后支持,避免了react逐层通过Props传递数据

const AppContext = React.creactContext({})
const A = () => {
  const { name } = useContext(AppContext)
}
const B = () => {
  const { name } = useContext(AppContext)
}
return (
  <AppContext.Provider value=<!--26-->>
  <A/>
  <B/>
  </AppContext.Provider>
)
  • useReducer() Action钩子
    const [state, dispatch] = useReducer(reducer, initalState)

  • useEffect() 监听状态变化进行刷新
    useEffect(() => {},[array])
    第一个参数是要进行异步的操作,第二个参数是一个数组。
    只要这个数组发生变化,useEffect就会执行