likes
comments
collection
share

React之类组件核心及原理

作者站长头像
站长
· 阅读数 16

一、类组件渲染过程

1.创建一个构造函数(类)

  • 要求必须继承React.Component/PureComponent这个类
  • 我们习惯于使用ES6中的class创建类「因为方便」
  • 必须给当前类设置一个render的方法「放在其原型上」:在render方法中,返回需要渲染的视图

2.从调用类组件的内部原理

「new Vote({...})」开始,类组件内部发生的事情

2.1. 初始化属性 && 规则校验

先规则校验,校验完毕后,再处理属性的其他操作!!

  • 方案一:
 constructor(props) {
  super(props); //会把传递进来的属性挂载到this实例上
  console.log(this.props); //获取到传递的属性
}
  • 方案二:

即便我们自己不再constructor中处理「或者constructor都没写」,在constructor处理完毕后,React内部也会把传递的props挂载到实例上;

所以在其他的函数中,只要保证this是实例,就可以基于this.props获取传递的属性!

同样this.props获取的属性对象也是被冻结的{只读的} Object.isFrozen(this.props)->true

2.2. 初始化状态

状态:后期修改状态,可以触发视图的更新

需要手动初始化,如果我们没有去做相关的处理,则默认会往实例上挂载一个state,初始值是null

  • 手动处理:
state = {
  ...
};
  • 修改状态,控制视图更新

this.state.xxx=xxx :这种操作仅仅是修改了状态值,但是无法让视图更新

想让视图更新,我们需要基于React.Component.prototype提供的方法操作

@1 this.setState(partialState) 既可以修改状态,也可以让视图更新 「推荐」
  + partialState:部分状态
  this.setState({
    xxx:xxx
  });
@2 this.forceUpdate() 强制更新

2.3. 触发componentWillMount

周期函数(钩子函数):组件第一次渲染之前

钩子函数:在程序运行到某个阶段,我们可以基于提供一个处理函数,让开发者在这个阶段做一些自定义的事情

  • 此周期函数,目前是不安全的「虽然可以用,但是未来可能要被移除了,所以不建议使用」
    • 控制会抛出黄色警告「为了不抛出警告,我们可以暂时用 UNSAFE_componentWillMount」
  • 如果开启了React.StrictMode「React的严格模式」,则我们使用 UNSAFE_componentWillMount 这样的周期函数,控制台会直接抛出红色警告错误!!

React.StrictMode VS "use strict"

  • "use strict":JS的严格模式
  • React.StrictMode:React的严格模式,它会去检查React中一些不规范的语法、或者是一些不建议使用的API等!!

2.4. 触发render周期函数:渲染

2.5. 触发 componentDidMount

周期函数:第一次渲染完毕

  • 已经把virtualDOM变为真实DOM了「所以我们可以获取真实DOM了」
  • ...

二、组件更新的过程--第一种

组件内部的状态被修改,组件会更新

1. 触发 shouldComponentUpdate 周期函数:是否允许更新

shouldComponentUpdate(nextProps, nextState) {
  // nextState:存储要修改的最新状态
  // this.state:存储的还是修改前的状态「此时状态还没有改变」
  console.log(this.state, nextState);

  // 此周期函数需要返回true/false
  //   返回true:允许更新,会继续执行下一个操作
  //   返回false:不允许更新,接下来啥都不处理
  return true;
}

2. 触发 componentWillUpdate 周期函数:更新之前

  • 此周期函数也是不安全的
  • 在这个阶段,状态/属性还没有被修改

3. 修改状态值/属性值「让this.state.xxx改为最新的值」

4. 触发 render 周期函数:组件更新

  • 按照最新的状态/属性,把返回的JSX编译为virtualDOM
  • 和上一次渲染出来的virtualDOM进行对比「DOM-DIFF」
  • 把差异的部分进行渲染「渲染为真实的DOM」

5. 触发 componentDidUpdate 周期函数:组件更新完毕

特殊说明:如果我们是基于 this.forceUpdate() 强制更新视图,会跳过 shouldComponentUpdate 周期函数的校验,直接从 WillUpdate 开始进行更新「也就是:视图一定会触发更新」!

三、组件更新的逻辑--第二种

父组件更新,触发的子组件更新

1. 触发 componentWillReceiveProps 周期函数:接收最新属性之前

  • 周期函数是不安全的
UNSAFE_componentWillReceiveProps(nextProps) {
  // this.props:存储之前的属性
  // nextProps:传递进来的最新属性值
  console.log('componentWillReceiveProps:', this.props, nextProps);
}

2. 触发 shouldComponentUpdate 周期函数

3. ......

四、组件卸载的逻辑

  1. 触发 componentWillUnmount 周期函数:组件销毁之前

  2. 销毁

  3. 父子组件嵌套,处理机制上遵循深度优先原则:

父组件在操作中,遇到子组件,一定是把子组件处理完,父组件才能继续处理

  • 父组件第一次渲染 父 willMount -> 父 render「子 willMount -> 子 render -> 子didMount」 -> 父didMount
  • 父组件更新: 父 shouldUpdate -> 父willUpdate -> 父 render 「子willReceiveProps -> 子 shouldUpdate -> 子willUpdate -> 子 render -> 子 didUpdate」-> 父 didUpdate
  • 父组件销毁: 父 willUnmount -> 处理中「子willUnmount -> 子销毁」-> 父销毁
  1. 生命周期总结:
  • 初次渲染:componentWillMount > render > componentDidMount
  • 组件自身状态变化的更新:shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate
  • 父组触发子组件更新:componentWillReciveProps > shildComponentUpdate > componentWillUpdate > render > componentDidMount
  • 不安全的生命周期:3个带will的:componentWillMount componentWillUpdate componentWillReciveProps

五、特性和原理

1.setState

用法:

this.setState([partialState],[callback])

partialState:支持部分状态更改

this.setState({
    x:100 //不论总共有多少状态,我们只修改了x,其余的状态不动
});

callback:在状态更改/视图更新完毕后触发执行「也可以说只要执行了setState,callback一定会执行」

  • 发生在componentDidUpdate周期函数之后「DidUpdate会在任何状态更改后都触发执行;而回调函数方式,可以在指定状态更新后处理一些事情;」
  • 特殊:即便我们基于shouldComponentUpdate阻止了状态/视图的更新,DidUpdate周期函数肯定不会执行了,但是我们设置的这个callback回调函数依然会被触发执行!!
  • 类似于Vue框架中的$nextTick!!

在React18中: setState操作都是异步的「不论是在哪执行,例如:合成事件、周期函数、定时器...」

  • 实现状态的批处理「统一处理」
  • 有效减少更新次数,降低性能消耗
  • 有效管理代码执行的逻辑顺序
  • ...

原理:利用了更新队列「updater」机制来处理的

  • 在当前相同的时间段内「浏览器此时可以处理的事情中」,遇到setState会立即放入到更新队列中!
  • 此时状态/视图还未更新
  • 当所有的代码操作结束,会“刷新队列”「通知更新队列中的任务执行」:把所有放入的setState合并在一起执行,只触发一次视图更新「批处理操作」

React16中: 在同步代码块中是异步的,而在异步代码块中则是同步的

  • 如果在合成事件「jsx元素中基于onXxx绑定的事件」、周期函数中,setState的操作是异步的!!
  • 但是如果setState出现在其他异步操作中「例如:定时器、手动获取DOM元素做的事件绑定等」,它将变为同步的操作「立即更新状态和让视图渲染」!!

原理:所有setState都默认在下一个微任务队列中执行,如果你让他本身就在异步队列中执行,那么它会跳过默认的任务队列直接执行。

2.PureComponent和Component

  • Component就是正常写法,没有其他特性
  • PureComponent会自动添加一个shouldComponentUpdate周期函数,并且在该函数中,对新老状态做了一个钱比较,如果状态值一样,则跳过更新!
  • 在pureComponent中你如果还是想更新,可以调用forceUppdate强制更新
  • 你也可以自己实现一个深度比较的方法,在component的shuoldComponentUpdate中自己进行判断
// 检测是否为对象
const isObject = function isObject(obj) {
    return obj !== null && /^(object|function)$/.test(typeof obj);
};
// 对象浅比较的方法
const shallowEqual = function shallowEqual(objA, objB) {
    if (!isObject(objA) || !isObject(objB)) return false;
    if (objA === objB) return true;
    // 先比较成员的数量
    let keysA = Reflect.ownKeys(objA),
        keysB = Reflect.ownKeys(objB);
    if (keysA.length !== keysB.length) return false;
    // 数量一致,再逐一比较内部的成员「只比较第一级:浅比较」
    for (let i = 0; i < keysA.length; i++) {
        let key = keysA[i];
        // 如果一个对象中有这个成员,一个对象中没有;或者,都有这个成员,但是成员值不一样;都应该被判定为不相同!!
        if (!objB.hasOwnProperty(key) || !Object.is(objA[key], objB[key])) {
            return false;
        }
    }
    // 以上都处理完,发现没有不相同的成员,则认为两个对象是相等的
    return true;
};

shouldComponentUpdate(nextProps, nextState) {
  let { props, state } = this;
  return !shallowEqual(props, nextProps) || !shallowEqual(state, nextState);
}

3.ref

1.受控组件和非受控组件

  • 受控组件:基于修改数据/状态,让视图更新,达到需要的效果 「推荐」
  • 非受控组件:基于ref获取DOM元素,我们操作DOM元素,来实现需求和效果「偶尔」

2.基于ref获取DOM元素的语法

  1. 给需要获取的元素设置ref='xxx',后期基于this.refs.xxx去获取相应的DOM元素
// 「不推荐使用:在React.StrictMode模式下会报错」
<h2 ref="titleBox">...</h2>

获取:

this.refs.titleBox
  1. 把ref属性值设置为一个函数
ref={x=>this.xxx=x}
  • x是函数的形参:存储的就是当前DOM元素
  • 然后我们获取的DOM元素“x”直接挂在到实例的某个属性上(例如:box2)

获取:

this.xxx
  1. 基于React.createRef()方法创建一个REF对象
this.xxx=React.createRef();  //=> this.xxx={current:null}
ref={REF对象(this.xxx)}

获取:

this.xxx.current

3.原理:在render渲染的时候,会获取virtualDOM的ref属性

  • 如果属性值是一个字符串,则会给this.refs增加这样的一个成员,成员值就是当前的DOM元素
  • 如果属性值是一个函数,则会把函数执行,把当前DOM元素传递给这个函数「x->DOM元素」,而在函数执行的内部,我们一般都会把DOM元素直接挂在到实例的某个属性上
  • 如果属性值是一个REF对象,则会把DOM元素赋值给对象的current属性

4.其他

获取函数组组件的ref 需要结合 forwardRef + useImperativeHandle