React学习 | 青训营笔记

36 阅读15分钟

这是我参与「第四届青训营 」笔记创作活动的第1天

React

React入门

原生JS痛点

  • 原生JS操作DOM繁琐、效率低。
  • 使用JS直接操作DOM,浏览器会进行大量重绘重拍。
  • 原生JS没有组件化编码方案,代码复用效率低。

React的特点

  • 采用组件化模式、声明式编码,提高开发效率及组件复用率。

    • 命令式:喝热水 => 打开水壶,倒水,开火,等待,倒热水
    • 声明式:喝热水 => 喝热水,over
  • 在React Native中可以使用React语法进行移动端开发。

  • 使用虚拟DOM和优秀的Diffing算法,尽量减少与真实DOM的交互。

    • Diffing算法:寻找新虚拟DOM树(JS维护的对象)和旧虚拟DOM树(浏览器维护)的差别。
    • 通过Diffing算法找出两颗树的不同之处,只部分地更新真实DOM。

HelloReact

  • 创建虚拟DOM

  • 渲染虚拟DOM到页面

    <div id="app">
      
    </div>
    ​
    引入React核心库、React-DOM、babel
    ​
    <!--babel用来将jsx语法翻译成js-->
    <script type="text/babel">
      // 创建虚拟DOM(jsx语法)
      const VDOM = <h1>Hello React!</h1>
      
      // 不用jsx,纯js创建虚拟DOM,text/babel换掉
      // const VDOM = React.createElement("标签名", {标签属性}, "标签体内容");
      // babel就是将react做的的jsx语法翻译成这种,浏览器才认识
      
      // 渲染虚拟DOM到页面
      ReactDOM.render(虚拟DOM, 节点);
    </script>
    

    关于VDOM:

    • 本质是js的Object类型的对象。
    • 与document.getXxx获得的真实DOM对象相比,虚拟DOM身上的属性比较少,真实DOM多很多。
    • 最终会被React转化成真实DOM,呈现在页面上。

JSX

  • 全称:JavaScript XML

  • 是React定义的一种JS的扩展语法

  • 语法规则:

    • 定义虚拟DOM时不要写引号

    • 标签内如果想要读取js变量,要用大括号

      <h1 id={myId.toLowerCase()}>{value}</h1>
      
    • 如果想用样式类名,不要用class,要用className。为了避开ES6的类

    • 如果想用内联style,要用大括号,里面一个对象,且短线改大驼峰

      <div style={ {color: "red", marginTop: "10px"} }></div>
      
    • 只能有一个根标签

    • 标签必须闭合

    • 标签首字母:

      • 小写字母开头,转为HTML中同名元素,若无该标签对应同名元素,则报错。
      • 大写字母开头,react去渲染对应的组件,若组件没有定义,则报错。

React面向组件编程

函数式组件

通过函数来定义组件:

function Demo() {
  // this是undifine(babel翻译完开启严格模式,严格模式禁止自定义函数this指向window)
  return <h1>Hello Component!</h1>
}
  • 名字大写

  • 使用:<Demo/><Demo></Demo>

  • 执行ReactDOM.render(<Demo/>, document.getElementById("app"))之后,发生了什么:

    • react解析组件标签,找到了Demo组件。
    • 发现组件是函数定义的,调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。

类式组件

通过类来定义组件:

class MyComponent extends React.Component { // 继承
  // 需要初始化就写构造函数
  // 不需要就不写
  // 写了就要super
  
  // 一般函数render必须要写,render必须有返回值
  render() {
    return <h1>Hello Component!</h1>
  }
}
​
ReactDOM.render(<MyCOmponent/>, document.getXxx....);
  • 继承

  • render函数

  • render函数的返回值

  • 执行ReactDOM.render(<MyCOmponent/>, document.getXxx....);后:

    • react解析组件标签,找到了MyComponent组件
    • react做了new实例对象的操作
    • 调用了实例对象的render方法,得到虚拟DOM
    • render中的this:当前实例对象 => 组件实例对象

State

  • 简单组件:没有状态(State)
  • 复杂组件:有状态(State)
  • 状态中存有数据。组件自己的:state,外部传进来的:prop,组件唯一标识:ref。组件实例的三大属性。 (既然是实例,那就只有类组件才有实例,函数组件没有,不过新版本React函数组件有hook,也可以有这三个东东,后面说)
class Weather extends React.Component {
  // 有自己的状态就要构造器,有构造器就要super
  constructor(props) {
    super(props);
    // props不是State这里的目的,下面才是:自己的状态
    this.state = {isHot: true};
  }
  
  render() {
    // return <h1>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h1>
    // es6解构赋值:
    const {isHot} = this.state;
    return <h1>今天天气很{isHot ? "炎热" : "凉爽"}</h1>
  }
}

事件:React重新设计了原生的所以onxxx事件,名字成了onXxx。

<h1 onClick={demo}></h1>
......
function demo() {
  console.log("点击...");
}

注意不要写:onClick={demo()},这样写的意思是,执行demo()函数,把该函数的返回值交给onClick。最后函数执行,返回值是undifine。

函数中要拿到state怎么办?是不是应该把事件回调写到类里面。

JS类中方法的this指向:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  speak() {
    console.log(this);
  }
}
​
const p1 = new Person("小明", 18);
p1.speak(); // this是实例对象
const x = p1.speak;
x();    // this是undifine

原因:x()调用的时候不是实例调用,是直接调用,直接调用的时候this就是window,如果开了严格模式就是undifine,类中的方法自动会开局部严格模式。

写到类里面:

class Weather extends React.Component {
  // ...构造函数...
​
  render() {
    return <h1 onClick={this.changeWeather}></h1>
  }
  
  changeWeather() {
    console.log(this) // 点击h1的时候执行这个方法,this是谁?undifine!
  }
}
  • changeWeather放在Weather的原型对象上,供实例调用。
  • changeWeather作为onClick的回调,不是通过实例调用的,而是直接调用。
  • 类中的方法默认开启局部严格模式,所以为undifine。

解决this指向:

class Weather extends React.Component {
  constructor() {
    // 等号右边:顺着原型链找到changeWeather函数,调用bind方法
    // bind方法生成了一个新的函数,且新函数的this是传的参数
    // 构造器中的this一定是实例对象
    // 最终实例对象身上多了一个changeWeather方法,内容和等号右边一样,且里面的this是实例对象自身
    // 点击的时候的changeWeather就是自身且this是实例的这个,就不会因为没找到而顺着原型找了
    this.changeWeather = this.changeWeather.bind(this);
  }
​
  render() {
    return <h1 onClick={this.changeWeather}></h1>
  }
  
  changeWeather() {
    console.log(this) 
  }
}

改:理解清楚了就知道怎么改。

class Weather extends React.Component {
  constructor() {
    // 等号右边:顺着原型链找到changeWeather函数,调用bind方法
    // bind方法生成了一个新的函数,且新函数的this是传的参数
    // 构造器中的this一定是实例对象
    // 最终实例对象身上多了一个changeWeather方法,内容和等号右边一样,且里面的this是实例对象自身
    // 点击的时候的changeWeather就是自身且this是实例的这个,就不会因为没找到而顺着原型找了
    this.haha = this.changeWeather.bind(this);
  }
​
  render() {
    return <h1 onClick={this.haha}></h1>
  }
  
  changeWeather() {
    console.log(this) 
  }
}

bind:

function demo() {
  console.log(this)
}
​
demo(); // 输出:this -> window
const x = demo.bind({a:10,b:"haha"});
x();    // 输出:this -> {a:10,b:"haha"}

最后改state不能直接改:

const isHot = this.state.isHot  // 可以取到isHot的值
this.state.isHot = !isHot       // 可以修改isHot的值

但是:React不认可这样的更改!!即这样更改无法响应式,即页面不会因为数据改变而改变!!

正确地:

const isHot = this.state.isHot
this.setState({
  isHot:!isHot
})

这个和微信小程序很像,微信小程序整体和vue很像,但是改数据这里vue可以直接改,而微信小程序和react都需要setData / setState来改。

react状态更新是替换还是合并:

this.state = {isHot:false,wind:"微风"}
//.....
//.....
this.setState({isHot:true})

执行后isHot变为true,而wind还在,是合并不是替换。同名的复写掉,不同名的任然在。

构造器只调用一次(new一次实例),render()调用 1 + n 次,最开始第一次,后面state通过setState改变,则进行调用,渲染更新页面。

简写

class Weather extends React.component {
  state = {isHot:false,wind:"微风"}
  
  changeWeather = () => {
    const isHot = this.state.isHot
    this.setState({isHot:!isHot})
  }
  
  render() {
    const {isHot,wind} = this.state
    return <h1 onClick="{this.changeWeather}">天气:{isHot ? "炎热" : "凉爽"},{wind}</h1>
  }
}
  • 类里面直接写赋值语句:所以实例身上都有这个属性。

    • JS中函数也可以作为变量赋值 => 这样写changeWeather由原型对象上变为了实例身上。
  • 箭头函数的特点:没有自己的this,但是函数体内也可以使用this,使用this会去找其外层的this作为自己的this使用。

    • 所以必须使用箭头函数才能确保this是Weather实例对象,普通函数依然是:如果是对象调用的就是实例对象;如果是直接调用的就是undifine。
  • 构造器里边写this.state = {isHot:false}就是为了让每个实例身上都有state属性,可以看到构造器里的state又不是说接外面的,就是一定让每个实例构造的时候都有这个state属性,那写外面直接写赋值语句不就所以实例都有了。

  • 注意:类里面直接写赋值语句:所以实例身上都有这个属性 => 和静态属性不同,不是静态属性还是实例属性!!

Prop

从外部传入数据到组件。

传:<Person name="tom" age="18" sex="男" />

用:this.props.name...,解构赋值:const {name,age,sex} = this.props

批量传入:

const p = {name:"xm",age:18,sex:"男"}
<Person {...p} />

...运算符:

let arr1 = [1,2,3,4]
let arr2 = [5,6,7,8,9]
console.log(...arr1)  // 展开数组:1 2 3 4
let arr3 = [...arr1, ...arr2] // 连接数组function sum(...numbers) {  // 不定参数
  console.log(numbers)  // 数组
  return numbers.reduce((preValue, currentValue) => {
    return preValue + currentValue
  });  
}
​
console.log(sum(1,2,3,4,5,6))
​
let person = {name:"xm",age:19,sex:"男"}
// console.log(...person) 报错,此运算符不能用于展开对象
let person2 = {...person} // 复制person的属性
let person3 = {...person,name:"lisi"} // 复制的同时修改个别属性

注意:react里<Person {...p} />,这个{}是react的语法,即这里写的JS就是...p,本来原生JS确实不可以展开对象,但是react + babel就可以了。不过也仅仅适用于标签属性的传递。

对props的限制:

  • 限制必须传某个属性
  • 限制传递的属性的类型
  • 给属性指定默认值
  class Person extends ... {
​
    render() {
      ......
    }
  }
​
  // 组件Person身上的一个属性,名字不能随便取,React会找这个属性的值来对props作规则限制
  Person.propTypes = {
    // 下面的PropTypes是React身上的,注意区分
    name: React.PropTypes.string.isRequired,  // props.name必须是string类型且必须
    age: React.PropTypes.number // 表示props.age必须是数字类型
    // 限制函数:React.PropTypes.func
  }
​
  // 组件Person身上的一个属性,名字不能随便取,React会找这个属性的值来对props作默认值
  Person.defaultProps = {
    sex: "男"
  }

React15.5之前,PropTypes都是React身上的,React16以后,因为加上PropTypes以后React感觉太大了,而PropTypes又不是每次都必须,所以React16开始PropTypes不再在React身上,而是专门引入一个依赖库来使用PropTypes。

注意:在组件内部,props是只读的,不允许修改。

JS的类上:

class A {
  // 实例对象上的属性a1,a2,有没有由创建对象时传不传决定
  constructor(a1,a2) {
    this.a1 = a1;
    this.a2 = a2;
  }
  
  // 所有实例对象上都加上a3,a4属性
  a3 = 10
  a4 = "hello"
  
  // 给类加上静态属性
  static a5 = 999
}

之前的事件方法是给所有组件实例加,每个由组件类构造的组件实例有各自的事件方法。而这次的规则限制是给所有组件实例都有同样的规则限制,所以这次写到类里边的时候应该用static加。

只要保证类自身有那两个属性,React就能添加规则限制。

讲讲构造器:

class Person extends React.Component {
  // 官网说构造器会接到props,把它交给super。
  constructor(props) {
    super(props)
  }
  // 但其实下面这样也完全没问题(都可以省略了)
  // constructor() {
  //   super()
  // }
  // 区别就是下面这种,如果要再在构造器里通过this访问props的话会出bug访问不到
}

React官网说:构造器仅仅适用于以下两种情况使用:

  • this.state赋值来初始化state
  • 为事件处理函数绑定this

那么这两种已经都用过了,同时也都可以不使用构造器实现,所以构造器是完全可以省略的。

函数组件的Prop

组件实例的三大属性:State、ref、Prop,而函数没有什么实例,所以三大属性不适用于函数,但是函数有props,因为函数可以接受参数。

function Person(props) {
  const {name, age} = props	// 解构赋值
  // 相当于 const name = props.name;const age = props.age;
  return (
  	<ul>
    	<li>{name}</li>
      <li>{age></li>
    </ul>
  )
}

函数式组件只有props,没有state和refs,不过React最新版本提出了函数式组件的hooks,实现了state和refs,后面讲。

对props进行限制还是可以的,不过一定要写在外面:

function Person() {
  ...
}
Person.propTypes = {}
Person.defaultProps = {}

refs

相当于原生htmlid属性,唯一标识一个组件,只要打了标识就能把html标签收集到this.refs(实例对象的refs属性)身上。拿到的就是真实DOM节点,虚拟DOM对应的真实DOM节点。

字符串形式的ref(已经不被React官方推荐了):

class Demo extends React.Component {
  clickBtn = ()=>{
    console.log(this.refs.btn)
  }
  
  render() {
    // 字符串形式的ref
    return <button ref="btn" onClick={clickBtn}>点我</button>
  }
}

为什么字符串形式的ref不推荐?存在效率问题,效率不高,可能会在未来的版本移除。

回调形式的ref:

回调函数:我定义的,我没调用,最终执行了。

class Demo extends React.Component {
  clickBtn = ()=>{
    console.log(this.myBtn)
  }
  
  render() {
    // 回调形式的ref
    return <button ref={ (currentNode)=>{this.myBtn = currentNode} } onClick={clickBtn}>点我</button>
  }
}

回调函数的调用次数:如果回调函数是以内联函数的方式定义的,第一次渲染(调用render)调用一次,参数是当前节点。以后每次更新页面(调用render)都会调用两次,第一次参数是null,第二次才是当前节点。

为什么这样做?每次更新渲染时都会重新调用render,render里遇到ref的时候,发现后面是个函数,因为不是第一次调了,所以React先传入null把之前的清空,再调一次传入新的。

如果不是回调函数就不会调两次:

class Demo extends React.Component {
  clickBtn = ()=>{
    console.log(this.myBtn)
  }
  
  haha = (c)=>{
    this.myBtn = c
  }
  
  render() {
    // 回调形式
    return <button ref={ this.haha } onClick={clickBtn}>点我</button>
  }
}

这是一个无关紧要的问题。

createRef API形式的ref:

class Demo extends React.Component {
  // 调用后返回一个容器,容器可以存储被标识的节点
  // 容器是专人专用的,即只能存储一个,再存会替换
  // 所以一个ref一个容器
  myRef = React.createRef()
  
  clickBtn = ()=>{
    console.log(this.myRef.current)
  }
  
  render() {
    // 新api形式的ref
    return <button ref={ this.myRef } onClick={clickBtn}>点我</button>
  }
}

React中的事件处理

  • 通过onXxx属性指定事件处理函数。

    • React使用的是自定义(合成)事件,而不是原生的DOM事件。——为更好的兼容性。
    • React事件是通过事件委托的方式处理的(委托给组件最外层的元素)。——为了更高效。
  • 通过event.target可以拿到发生事件的DOM元素对象。——勿过度使用ref。

受控组件与非受控组件

  • 非受控组件:页面中所有输入类标签的内容现用现取。(点击登录后才收集数据)。

  • 受控组件:将数据通过onChange事件绑定到state。(就像Vue双向绑定,datav-model:xxx

    React需要手动绑定,就像这样:

    class Demo extends React.Component {
    ​
      state = {username: "", password: ""}
    ​
      saveUsername = (e)=>{
        this.setState({username: e.target.value})
      }
    ​
      savePassword = (e)=>{
        this.setState({password: e.target.value})
      }
    ​
      render() {
        return (
          <div>
            用户名<input onChange={this.saveUsername} text="type" name="username" /><br/>
            密码<input onChange={this.savePassword} text="password" name="password" />
          </div>
        )
      }
    }
    

高阶函数

上面的代码是否可以优化:

class Demo extends React.Component {
​
  state = {username: "", password: ""}
​
  saveFormData = (e)=>{
    console.log(e.target)
  }
​
  render() {
    return (
      <div>
        用户名<input onChange={this.saveFormData} text="type" name="username" /><br/>
        密码<input onChange={this.saveFormData} text="password" name="password" />
      </div>
    )
  }
}

前面说过,事件处理:onChange={xxx},这里xxx不能加括号,因为这样写的意思是把xxx交给onChange作为事件发生的回调,xxx是一个函数。如果写成xxx()则是调用xxx函数,并把xxx函数的返回值作为事件发生的回调。

那么,我们收集数据的时候都用同一个函数saveFormData,标识到底是username还是password,就要通过参数来标识,所以就会写成saveFormData("username"),那么很自然,函数就必须返回一个函数:

saveFormData = (valueType)=>{
  return ()=>{
    // ...
  }
}

那么,参数valueType就是传过来的username或password,而返回值是一个函数,执行saveFormData函数,返回一个函数,这个返回的函数就是事件回调函数,那么它不就可以接到参数:

saveFormData = (valueType)=>{
  return (e)=>{
    // ...{valueType: e.target.value}
  }
}

那么,只需要这样就能实现功能:

saveFormData = (valueType)=>{
  return (e)=>{
    this.setState({
      [valueType]: e.target.value
    })
  }
}

为什么要[],搞清楚JS对象:

let obj = {
  "username": "xiaoming"  // 左边的username是字符串,这是最本质的写法,我们习惯简写把双引号省略
}
let obj2 = {
  username: "xiaoming"  // 和上面完全等价
}
​
// 那么
let a = "username";
let obj3 = {
  a: "xiaoming" // 显然这个对象是:{a:"xiaoming"}
}
​
// 怎么办?
// 想想访问对象属性
let temp = obj2.username
​
// 本质上?
let temp2 = obj2["username"]
​
// 那么
let obj4 = {
  ["username"]: "xiaoming"
}
// 显然
let obj5 = {
  [a]: "xiaoming"   // obj5:{username: "xiaoming"}
}

最终完成了组件:

class Demo extends React.Component {
​
  state = {username: "", password: ""}
​
  saveFormData = (valueType)=>{
    return (e)=>{
      this.setState({
        [valueType]: e.target.value
      })
    }
  }
​
  render() {
    return (
      <div>
        用户名<input onChange={this.saveFormData("username")} text="type" name="username" /><br/>
        密码<input onChange={this.saveFormData("password")} text="password" name="password" />
      </div>
    )
  }
}

高阶函数:

  • saveFormData一样,它是一个函数,它的返回值还是一个函数,这样的函数就属于高阶函数。
  • 如果A函数接受的参数是一个函数,那么A函数也属于高阶函数。
  • 常见的高阶函数:Promise、setTimeout、setInterval、数组身上的方法...

函数的柯里化:通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编码形式。saveFormData就是函数的柯里化。

不用柯里化实现:

class Demo extends React.Component {
​
  state = {username: "", password: ""}
​
  saveFormData = (valueType, e)=>{
    this.setState({
        [valueType]: e.target.value
    })
  }
​
  render() {
    return (
      <div>
        用户名<input onChange={(event)=>{this.saveFormData("username", event)}} text="type" name="username" /><br/>
        密码<input onChange={e => this.saveFormData("password", e)} text="password" name="password" />
      </div>
    )
  }
}

组件的生命周期

React找到组件类,创建组件实例对象,用实例调方法,哪些方法?生命周期方法和render方法。

之前讲过的render:初始化渲染、状态更新以后。

组件挂载完毕时:componentDidMount()。对应Vue的mounted()

组件将要卸载时:componentWillUnmount()。对应Vue的destroyed()

旧版生命周期:

组件挂载:

  • constructor
  • componentWillMount
  • render
  • componentDidMount

组件更新:

  • setState更新状态

    • shouldComponentUpdate:组件是否应该被更新,返回true更新,否则不更新(下面的生命周期都不会走),不写这个函数默认返回true,写了函数返回值不是布尔报错
    • componentWillUpdate
    • render
    • componentDidUpdate(oldProps, oldState)
  • forceUpdate强制更新状态:绕过shouleComponentUpdate,其他和上面一样。

    this.forceUpdate()  // 页面状态没有改变,强制更新一下
    
  • 父组件render

    • componentWillReceiveProps(props):组件将要接收新的props,第一次不算,以后(父组件状态更新了)才算
    • 后面的和setState一样

组件卸载:

  • componentWillUnmount

    • 代码卸载组件:

      ReactDOM.unmountComponentAtNode(document.get....节点)
      

新版生命周期(React17版本以后,还是能用旧的,只是控制台有警告)

弃用三个:三个componentWillXxx的前面加上UNSAFE_。(一共四个willXxx,除了componentWillUnmount

为什么?——React正在研究异步渲染,unsafe不是表示“不安全”,而是在未来版本异步渲染的时候可能有BUG。

新增两个(这两个用法也很罕见):

  • 挂载和更新时:getDerivedstateFromProps(props, state)

    • 这个钩子不是给实例.调用的,是静态方法,static
    • 还要有返回值,返回状态对象state Objectnull。(不能undifine
    • 返回了状态对象,该状态对象会派生到组件实例的state里。
    • 用于:state的值在任何时候都取决于props。
  • 更新时:getSnapshotBeforeUpdate(oldProps, oldState)

    • Snapshot:快照

    • 必须返回一个快照值snapshot valuenull。(不能undifine

    • snapshot value:随便什么值,数字、字符串等都可以

    • 返回的值会传给componentDidUpdate()

      • componentDidUpdate实际上还有一个参数,共三个。
      • componentDidUpdate(oldProps, oldState, snapshot)
    • 在进行页面更新之前获取一些已有的旧信息,并可以通过快照往下传。(例如滚轮滚动位置等)

常用的生命周期: