深入React框架

第一节 JSX语法

1. JSX映射为DOM的原理

1.1 createElement方法

arduino 复制代码
export function createElement(type, config, children);
csharp 复制代码
export default createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;

// props 变量用于储存元素属性的键值对集合
const props = ;

// key、ref、self、source 均为 React 元素的属性,此处不必深究
let key = null;
let ref = null;
let self= null;
let source = null;

// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、 key、self 和 source 属性赋值
    if (hasValidRef(config)) {
        ref = config ref;
    }
   if (hasValidKey(config)) {
       key = " + config. key;
   }
   self= configself === undefined ? null : config  self;
   source = config_source =-= undefined ? null : config source;
 ... ...
}        
  • type:标识节点的类型
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储于config对象中
  • children:以对象形式传入,记录了组件标签之间嵌套的内容

1.2 ReactElement方法

rust 复制代码
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT ELEMENT TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    
    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,
    
    // 记录创造该元素的组件
    _owner: owner,
  }
 
  ...
 
  return element;
}

1.3 ReactElement对象

ReactElement方法的返回值为一个element对象,即所谓的虚拟DOM,格式如下所示:

1.4 ReactDOM.render方法

ReactDOM.render方法可以把拿到的虚拟DOM元素渲染到页面中。

csharp 复制代码
ReactDOM.render(
    // 需要渲染的元素 ReactElement
    element,
    // 元素挂载的目标容器(一个真实的DOM)
    container,
    // 可选的回调函数,用来处理渲染结束后的逻辑
    [callback]
)

1.5 JSX转化为真实DOM完整流程

第二节 生命周期

1. 核心概念

1.1 虚拟DOM

虚拟DOM又称为核心算法的基石

1.2 组件化

组件化:工程化思想在框架中的实现。

每个组件可以是封闭的,也可以是开放的。

  • "封闭"针对渲染工作流而言,在组件渲染工作流中,每个组件只处理它自身的渲染逻辑
  • "开放"针对组件通信而言,React允许开发者基于"单向数据流"的原则完成组件间通信,而组件间通信又将改变通信双方/某一方内部的数据,进而对渲染结果产生影响

2. 生命周期流程

2.1 render方法

如果把render方法比喻为React整个生命周期的"灵魂",那么其他钩子则为"躯干"。

scala 复制代码
class LifeCircle extends React.Component {
    render(){
        console.log("render方法执行")
        return(
            <div>...</div>
        )
    }
}

2.2 React15生命周期

1)Mounting阶段

组件的初始化渲染阶段(挂载)

2)Updatng阶段

组件更新,包括子组件内部状态改变导致的更新和父组件更新引发的子组件更新

  • componentWillReceiveProps方法
scss 复制代码
componentWillReceiveProps(nextProps)

componentWillReceiveProps方法不是由props的变化触发的,而是由父组件的更新触发的。

  • shouldComponentUpdate方法
scss 复制代码
shouldComponentUpdate(nextProps,nextState)

React组件根据shouldComponentUpdate方法的返回值决定是否执行之后的生命周期钩子函数,进而决定是否执行组件的re-render(重渲染)。该方法的默认值是true,表示会重新渲染组件。可以通过自动设置内部的实现逻辑,来控制组件的re-render,或者引入pureComponent方法等,进一步实现性能优化。

3)Unmounting阶段

组件卸载阶段,该阶段可以执行一些收尾工作。

组件卸载场景:子组件被移除、组件包含key值且key发生改变、切换路由等

2.2 React16生命周期

1)Mounting阶段

组件的初始化渲染阶段(挂载)

2)Updatng阶段

组件更新,包括子组件内部状态改变导致的更新和父组件更新引发的子组件更新

3)Unmounting阶段

组件卸载阶段,该阶段可以执行一些收尾工作。

2.3 生命周期优化

1)新增了getDerivedStateFromProps方法

⚠️与此同时,废弃了componentWillMount方法

getDerivedStateFromProps不是componentWillMount的替代品,其出现是为了代替componentWillReceiveProps,其用途有且仅有一个,"使用props来派生/更新state"。

getDerivedStateFromProps方法在componentWillReceiveProps的基础上做了减法,该方法并不能完全代替componentWillReceiveProps,但是其安全性较componentWillReceiveProps得到了很大的提升。

scss 复制代码
static getDerivedStateFromProps(props,state)

特点如下:

  • 静态方法,内部访问不到this
  • 接收两个参数,props和state。props为父组件传递的外部props,state是组件内部初始化的state变量
  • 必须返回一个对象格式的值,React会利用返回值派生/更新组件内部的state,如果没有返回值,可以用null代替

getDerivedStateFromProps对state的更新不是覆盖式的,而是定向更新某个属性,新属性和state原有属性会组合成为state的新值

2)新增了getSnapshotBeforeUpdate方法

⚠️与此同时,废弃了componentWillUpdate方法

javascript 复制代码
getSnapshotBeforeUpdate(prevProps,prevState){}

该方法类似于getDerivedStateFromProps方法,区别是getSnapshotBeforeUpdate方法的返回值回作为参数传递给componentDidUpdate,其执行时机在render方法之后,真实DOM更新之前。在此阶段可以获取到更新前的真实DOM和更新前后的state&props信息。

💡一般在使用时要配合componentDidUpdate方法。

3. 生命周期优化本质

3.1 Fiber

Fiber是React16对React核心算法的一次重写,其原本同步的渲染过程变成异步的。

React15及之前的版本中,diff算法会对新旧虚拟DOM树进行递归遍历,查找发生变化的部分,从而实现组件的更新。整个递归过程是同步的,且不可中断,对于浏览器性能要求很高,在此过程中,浏览器无法处理用户的任何操作,从而造成用户交互行为无响应的现象即页面卡顿等性能问题。

React16的Fiber通过将一个大的任务拆解为许多小的任务,从而避免了任务过长导致的页面卡顿问题。Fiber架构的重要特征就是可以被打断的异步渲染的重要模式,根据能否被打断这一标准,React16的生命周期被划分为render阶段和commit阶段。其中commit阶段又被划分为pre-commit和commit两部分。

  • render阶段:执行过程中允许暂停、终止和重启,其生命周期会被重复执行
  • commit阶段:同步执行,不允许中断

🤔1.为什么废弃了componentWillMount、componentWillUpdate、componentWillReceiveProps三个生命周期?

🙋:首先是因为Fiber模式导致的render中的生命周期会重复执行,而这三个钩子的滥用极其影响性能。其次是这三个钩子中的操作可以完全转移到其他钩子中,常用的是componentDidMount和componentDidUpdate两个。并且在新增加的两个钩子中,获取不到this,所以可以避免一些误操作导致的bug。

第三节 组件间数据流动

1. 基于props单向数据流

组件,从概念上类似于JavaScript函数。接受任何入参(即props)并返回用于描述页面展示内容的React元素。

单向数据流要求组件的state以props的形式流动时,只能流向组件树中比自己层级更低的组件。

1.1 父-子组件通信

父组件可以将自身的this.props传递给子组件,实现父子组件间的通信。

1.2 子-父组件通信

父组件传递一个绑定自身上下文的函数给子组件,子组件在调用函数时可以将数据作为 参数传递给函数,从而实现子父组件间的通信。

1.3 兄弟组件通信

🤔props层层传递实现组件通信不好么?

🙋:不好,不仅会增加代码量,还会破坏中间层级组件的数据结构,导致后续维护成本增加,所以不推荐使用。

2. "发布-订阅"模式驱动数据流

2.1 理解发布订阅模式

发布订阅模式是解决任意组件间通信的良好解决方案。常见的实现如下:

  • socket.io模式,实现跨端的发布订阅模式
  • Node.js中的EventEmitter
  • Vue.js中全局事件总线EventBus

JavaScript中的事件监听也可以理解为一个简单的发布订阅模式。

bash 复制代码
element.addEventListener(type,function,useCapture)

发布订阅模式的设计思路中,包括事件的监听和事件的触发。

  • on():负责注册事件的监听器,指定事件触发时的回调函数
  • emit():负责触发事件,通过传递参数实现在触发时携带数据
  • off():负责移除事件监听器

2.2 实现发布订阅模式

typescript 复制代码
constructor(){
    // 存储事件和监听函数间的关系
    this.eventMap = {}
}

on(type,handler){
    if(!(handler instanceof Function)){
        throw new Error('必须传入一个函数')
    }
    if(!this.eventMap[type]){
        // 队列不存在监听函数,则创建队列
        this.eventMap[type] = [];
    }
    // 添加事件
    this.eventMap[type].push(handler);
}

emit(type,params){
    // 如果包含某监听事件,则遍历列表执行
    if(this.eventMap[type]){
        this.eventMap[type].forEach(f =>{
            f(params);
        })
    }
}

off(type,handler){
    if(this.eventMap[type]){
        this.eventMap[type].splice(
            this.eventMap[type].indexOf(handler) >>> 0,
            1
        )
    }
}

2.3 发布订阅模式的数据流动

3. Context API维护全局状态

Context API是React官方提供一种组件树全局通信的方式,其包含三个重要成员:Context对象、Provider、Consumer。

3.1 React.createContext

ini 复制代码
const context = React.createContext(defaultValue);
arduino 复制代码
const { Provider, Consumer} = context;
  • Provider:数据提供者,使用时包裹需要消费数据的组件
  • Consumer:数据消费者,使用时需要通过函数返回需要渲染的React元素
xml 复制代码
<Provider title='数据'>
    <Component />
</Provider>

<Consumer>
    {value => <div>{value.tile}</div>}
</Consumer>

💡即使shouldComponentUpdate返回false,context仍然可以穿透组件,继续向后代组件传播,进而确保数据提供者和数据消费者之间数据的一致性。

3.2 Redux数据流框架

Redux是JavaScript的状态容器,提供可预测的状态管理。

  • store:一个只读的单一数据源
  • action:对变化的描述
  • reducer:负责对变化进行分发和处理

⚠️:Redux在整个工作流程中,数据是严格单向的!

1)使用createStore创建store对象

ini 复制代码
const store = createStore(
    reducer,
    initial_state,
    applyMiddleWare(middleWare1,middleWare2,...)
)

2)通过reducer创建一个新的state返回给store

javascript 复制代码
const reducer = (state,action)=>{
    return newState
}

3)通过action通知reducer"让改变发生"

go 复制代码
const action = {
    type:"ADD_ITEM",
    payload:"<li>{text}</li>"
}

4)通过dispatch派发action

ini 复制代码
store.dispatch(action);

第四节 React-Hooks

1. React-Hooks设计动机

React-Hooks是React团队在16.8版本中推出的,其中包含了对类组件和函数组件的理解和侧重。

1)类组件

scala 复制代码
class App extends React.Component{
    constructor(){
        super(props);
    }
}

类组件是面向对象编程的一种思想表现。

  • "封装":将一类属性和方法聚拢到一个Class中
  • "继承":新的Class通过继承其他Class,实现对某一类属性和方法的复用

⚠️:类组件在全面的API作用下也带来了高昂的学习成本和不利于拆分和复用的缺点

2)函数组件

javascript 复制代码
const App = function(){
    return(
        <div>{Text}</div>
    )
}

函数组件较类组件而言,更加轻量和灵活,利于代码拆分和逻辑复用。但是更为重要的一点区别是"函数组件会捕获render内部的状态"!

函数组件与类组件在底层的区别其实是"函数式编程"和"面向对象编程"思想的区别,而通过React框架公式也能体现出函数组件更符合React框架的设计思想
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> U I = r e n d e r ( d a t a ) UI = render(data) </math>UI=render(data)

React组件本身的定位就是函数,给定输入就会返回输出。React底层会把声明式的代码转化为命令式的DOM操作,把数据层面的描述映射到UI中,实现页面的更新。

3)两者对比

  • 类组件需要继承React.Component,函数组件不需要
  • 类组件可以访问生命周期方法,函数组件不可以
  • 类组件可以获取实例化后的this,并基于this做各种操作,函数组件不可以
  • 类组件可以定义和维护state状态,函数组件不可以

4)函数组件会捕获render内部的状态解读

在类组件中虽然props本身是不可变的,但是this却是可变的,其绑定的数据可以被修改。如果通过setTimeout将渲染动作推迟,则this.props捕获到的数据和渲染的数据不一致,从而导致页面更新错误。

函数组件会捕获render内部的状态其实是指函数组件可以将props和渲染绑定在一起,从而实现数据和页面的一致性。

所以React官方也觉得函数组件是一个更加匹配其设计理念,更加有利于逻辑拆分和重用的组件表达形式。

2. React-Hooks初识

React-Hooks是一套能够使函数组件更强大更灵活的钩子,增加了类似于类组件的生命周期等API。

Hooks就像是一个工具箱,任由开发者按需引入和使用,从而壮大其函数组件能力。

2.1 useState

scss 复制代码
const [state,setState] = useState(initialState);

作用:为函数组件引入状态,类似于类组件的this.state。

💡在React中调用React.useState时,会给当前组件关联一个状态

2.2 useEffect

scss 复制代码
useEffect(()=>{
    ...
},[]);

作用:允许函数组件执行副作用,类似于类组件的componentDidMount,componentDidUpdate,componentWillUnmount三个生命周期钩子。

执行时机:

  • 每一次渲染都执行的副作用
scss 复制代码
useEffect(callback);
  • 仅在挂载阶段执行一次的副作用
ini 复制代码
useEffect(callback,[]);
  • 仅在挂载阶段和卸载阶段执行的副作用
scss 复制代码
useEffect(()=>{
    // 挂载阶段执行
    ... ...
    
    // 卸载阶段执行
    return ()=>{
       ... ... 
    }
},[]);
  • 每一次渲染都执行,且卸载阶段也会执行的副作用
javascript 复制代码
useEffect(()=>{
    // 渲染阶段执行
    ... ...
    
    // 卸载阶段执行
    return ()=>{
       ... ... 
    }
});
  • 依赖项改变才会触发的副作用
scss 复制代码
useEffect(()=>{
    // 依赖项改变时执行
    ... ...
},[state1,state2,...]);

3. React-Hooks优点

1)告别难以理解的Class

难点一:this

this在获取和使用时都需要格外注意,如果直接使用函数时,不会获取到this,需要使用箭头函数或者bind改变this指向问题。

难点二:生命周期

对于开发者而言,生命周期的学习成本过高,而且容易形成滥用,导致性能降低。

2)利于业务逻辑拆分和复用

Hooks出现之前逻辑复用采用的是HOC(高阶组件)和 Render Props两个组件设计模式,但是在实现组件复用逻辑的同时,由于嵌套过深也破坏了组件的结构。

Hooks可以在不破坏组件结构的前提下,实现逻辑复用。

3)函数式编程更符合React框架思想

同"React-Hooks设计动机"一节,此处省略。

4. React-Hooks局限

  • 没有完全补全类组件的能力
  • 不能很好的消化"复杂",在过于复杂和过度拆分中不好把握
  • 在使用层面有严格的规则约束

5. React-Hooks设计原理

React-Hooks在使用时需要遵守两大原则:

  • 只能在函数组件中使用Hook
  • 不能在循环、条件和嵌套函数中调用Hook

React底层需要确保Hook的调用顺序保持不变,否则会报错

1)useState调用流程

  • 初始化阶段
  • 更新阶段
  • mountState方法:构建链表并渲染
  • updateState方法:依次遍历链表并渲染

💡Hooks并不是以数组的形式存在于底层,而是以链表的形式。

第五节 虚拟DOM和diff算法

1. 虚拟DOM

1.1 虚拟DOM概要

虚拟DOM本质上是JS和DOM之间的一个映射缓存,形态上为一个对象,可以描述了DOM元素和属性信息。

1)挂载阶段

React会结合JSX的描述构建出虚拟DOM树,然后通过ReactDOM.render实现虚拟DOM到真实DOM的映射。

2)更新阶段

页面的变化会先作用于虚拟DOM,虚拟DOM会在JS层借助算法先算出具体有哪些真实DOM需要被改变,然后再将这些改变作用于真实DOM上。

💡:虚拟DOM的劣势在于diff计算的耗时,但是DOM操作的能耗和JS计算的能耗不在一个量级

⚠️:虚拟DOM的优势不在性能,而在别处

1.2 虚拟DOM价值体现

  • 研发体验和研发效率:开发者无需在手动操作原生DOM,即可实现数据驱动视图的更新
  • 跨平台问题:将真实DOM转化为一套虚拟DOM,即可支持不同终端,降低成本
  • 批量更新:虚拟DOM通过batch函数实现批量的更新。batch函数的作用是缓存每次生成的补丁集,并暂存在队列中,并在最后一次性完成所有更新

2. 调和与diff

2.1 调和

虚拟DOM是一种编程概念,在这个概念里,UI以一种理想化的或者虚拟的形式存在于内存中,并通过ReactDOM等类库使之与真实DOM同步,这一同步过程叫做"调和"。

2.2 diff算法

1)diff策略的设计思想

传统diff:找出两个树结构之间的不同,需要进行遍历递归对树节点之间进行一一对比,时间复杂度为O(n^3)。

改良diff:在原有思想的前提下,提出了三个新的原则

  • 跨层级的节点操作忽略不计
  • 若两个组件属于同一类型,它们将拥有相同的DOM树型结构
  • 处于同一层级的一组子节点,可以设置key作为唯一标识符,从而维持各个节点在不同渲染过程中的稳定性

2)diff策略的逻辑

  • diff算法性能突破的关键点在于"分层对比"
  • 类型一致的节点才有进行diff的必要
  • key属性的设置,可以帮助重用同一层级内的节点

官方对key属性的定义如下:key是帮助React识别哪些内容被更改、添加或者删除。key需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果key值发生了变更,React会触发UI的重渲染。所以这是一个非常有用的特性。

javascript 复制代码
const todoItem = todos.map(item =>{
    return <div key={item.id}>{item.text}</div>
})

2.3 调和和diff区别

  • 调和:使虚拟DOM和真实DOM一致
  • diff:在新旧虚拟DOM中找不同

调和分为Core、Render、Reconciler三部分,其中Reconciler(调和器)所做的工作在组件挂载、卸载、更新等过程中,而diff可以看作是调和过程中最具代表性的一环。

第六节 setState更新原理

1. 批量更新

触发setState后,会同时引发组件生命周期的一些钩子函数,如下:

由于render对于性能消耗特别大,所以React底层的setState在正常情况下是异步批量更新的。

⏰批量更新的逻辑:每触发一个setState就把对应的任务 放入队列 中,等到时机成熟,就把积攒的所有setState结果做合并 ,最后只针对最新的state值一次更新流程。

2. setState工作流

1)setState入口函数

kotlin 复制代码
ReactComponent.prototype.setState = function(partialState,callback){
    this.updater.enqueuesetState(this,partialState);
    if(callback){
        this.updater.enqueuesetCallback(this,callback,'setState');
    }
}

2)enqueuesetState关键函数

ini 复制代码
enqueueSetState = function(publiclnstance, partialState){
    // 根据this拿到对应的组件实例
    var internallnstance = getlnternallnstanceReadyForUpdate(publiclnstance,'setState');
    
    // 这个queue对应的就是一个组件实例的state数组
    var queue = internallnstance._pendingStateQueue || (internallnstance._pendingStateQueue = [];
    queue.push(partialState);
    
    // enqueueUpdate用来处理当前的组件实例
    enqueueUpdate(internallnstance);
}

主要工作如下:

  • 将新的state任务放入组件的状态队列中
  • 使用enqueueUpdate处理将要更新的组件实例对象

3)enqueueUpdate更新组件的函数

scss 复制代码
function enqueueUpdate(component) {
    ensurelnjected();

    // isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
    if(!batchingStrategy.isBatchingUpdates){
        batchingStrategy.batchedUpdates(enqueueUpdate,component);
        return;
    }
    // 否则,先把组件塞入 dirtyComponents 队列里,让它"再等等'
    dirtyComponents.push(component);
    if (component._updateBatchNumber === null){
        component._updateBatchNumber = updateBatchNumber + 1
    }
}

4)batchingStrategy控制批量更新的关键锁

javascript 复制代码
var ReactDefaultBatchingStrategy = {
    // 全局唯一的锁标识
    isBatchingUpdates:false,
    
    // 发起更新动作的方法
    batchedUpdates:function(callback, a, b, c, d, e){
        // 缓存初始锁变量
        var alreadyBatchingStrategy = ReactDefaultBatchingStrategy.isBatchingUpdates
        // 把锁"锁上"
        ReactDefaultBatchingStrategy.isBatchingUpdates = true
        
        if(alreadyBatchingStrategy){
            callback(a, b, c, d,e);
        }else{
        // 启动事务,将callback放进事务里执行
        transaction.perform(callback, null, a, b, c, d, e);
       }
   }
}

ReactDefaultBatchingStrategy.isBatchingUpdates状态值区分:

  • true:当前处于批量更新状态,会将state任务放入组件事件队列中,等待更新
  • false:当前没有处于批量更新状态,会立即执行state任务

3. setState异步更新本质

1)正常调用------异步更新

kotlin 复制代码
increment = ()=> {
    // 进来先锁上
    isBatchingUpdates = true;
    
    console.log('increment setState前的count',this.state.count);
    this.setState({
        count: this.state.count + 1
    });
    
    console.log('increment setState后的count',this.state.count);
    
    // 执行完函数再放开
    isBatchingUpdates = false;
}

2)setTimeout包裹时------同步更新

javascript 复制代码
increment = ()=> {
    // 进来先锁上
    isBatchingUpdates = true;
    
    // isBatchingUpdates的开关属于主线程任务,所以无法约束setTimeout内部代码的执行
    setTimeout(()=>{
        console.log('increment setState前的count',this.state.count);
        this.setState({
            count: this.state.count + 1
        });
        console.log('increment setState后的count',this.state.count);
    },0)
    
    // 执行完函数再放开
    isBatchingUpdates = false;
}

第七节 Fiber架构

1. Fiber架构思想

1.1 Stack Reconciler

1)React定位

React官方认为,React是用JavaScript构建快速响应的大型Web应用程序的首选方式。

但是在React15及之前版本的Stack Reconciler在交互体验等方面显出疲态,大型页面卡顿问题明显。在React16.x 版本中将其最为核心的diff算法进行完全的重写,使其以"Fiber Reconciler"的全新面貌示人,从而向其快速响应目标更进一步。

2)单线程的JS与多线程的浏览器

多线程的浏览器除了要处理JavaScript线程以外,还需要处理各种各样的任务线程,如处理DOM的UI渲染线程等。由于JavaScript线程也是可以操作DOM的,所以这两个线程在运行时是相互排斥的,即当其中一个线程执行时,另一个线程只能挂起等待。

如果JS线程执行长任务,则会导致渲染线程一直处于等待状态,界面就会长时间得不到更新,带给用户的体验就是所谓的"卡顿"。

3)页面卡顿的原因

React15的栈调和机制下的diff算法其实是树深度优先遍历的过程。Reconciler调和器会重复"父组件调用子组件"的过程直到最深的一层节点更新完毕,才慢慢向上返回。

Stack Reconciler过程的致命性问题在于其是同步的,不可以被打断,所以需要的调和时间会很长,导致JavaScript线程长时间地霸占主线程,进而导致上文中所描述的渲染卡顿/卡死、交互长时间无响应等问题。

1.2 Fiber

1)概念

Fiber就是比线程还要纤细的一个过程,也就是所谓的"纤程"。纤程的出现意在对渲染过程实现更加精细的控制。

2)价值

  • 从架构角度来看,Fiber是对React核心算法的重写
  • 从编码角度来看,Fiber是React内部所定义的一种数据结构
  • 从工作流的角度来看,Fiber节点保存了组件需要更新的状态和副作用

2. Fiber核心

2.1 架构层面的Fiber

React15的Stack Reconciler工作流程如下:

React16的Fiber工作流程如下:

Scheduler:调度器,其工作流程大致如下:

每一个封信任务都会赋予一个优先级,当更新任务抵达调度器时,高优先级的任务会优先进入Reconciler层。(设置优先级

此时如果有新的更新任务抵达调度器,调度器会比较其优先级,若发现B的优先级高于当前任务A,那么当前处于Reconciler层的A任务就会被中断。将更高优先级的B任务推入Reconciler层。当B任务执行完毕后,就会进入下一轮的任务调度。(可中断

之前被中断的A任务会被重新推入Reconciler层,继续A任务的渲染流程。(可恢复

2.2 生命周期层面的Fiber

React15的更新渲染流程:

React16的更新渲染流程:

改进🚚:React在render阶段将一个庞大的更新任务,拆解为若干个小的更新工作单元,每一个单元都被设置了一个不同的优先级。React根据优先的高低,实现工作单元的打断和恢复等,从而完成整个更新任务。

⚠️:正因为Fiber有如上的更新,所以需要废除componentWillXXX的生命周期。

3. Fiber渲染流程

3.1 初始化阶段

  • ReactDOM.render:同步渲染,又称为legacy模式(传统)
  • ReactDOM.createRoot:异步渲染,又称为concurrent模式(并发执行)
  1. ReactDOM.render
  • fiberRoot:真实DOM入口节点
  • rootFiber:虚拟DOM根节点

updateContainer方法的核心工作为:

第一步:请求当前Fiber节点的lane(优先级)

第二步:结合lane(优先级)创建当前Fiber节点的update对象,并将其入队列

第三步:调度当前节点(rootFiber)

performSyncWorkOnRoot是render阶段的起点,render阶段的任务就是完成 Fiber树的构建,它是整个渲染链路中最核心的一环。

  1. ReactDOM.createRoot

React底层会根据一个mode属性,决定工作流程是一气呵成(同步)还是分片执行(异步)。此处省略mode相关的源码,感兴趣的同学可以去官网查看。

3.2 render阶段

  1. React15栈调和

React15的调和过程是一个递归的过程,ReactDOM.render触发的同步模式下仍然是一个深度优先搜索的过程。

在这个过程中,beginWork将创建新的Fiber节点,completeWork则负责将Fiber节点映射为DOM节点。

  1. React16Fiber

首先会构建两棵树,一颗为workInProgress树,一颗为current树

  1. Fiber节点的创建流程
  1. Fiber树的创建流程

1)beginWork函数

第一步:循环创建新的Fiber节点

第二步:Fiber节点间创建联系

将通过child、return、sibling3个属性建立关系,其中child、return记录的是父子节点关系,sibling记录的则是兄弟节点关系。

2)completeWork函数

执行时机:当beginWork递归无法进行时,则会执行completeWork

特点:严格的自底向上执行

作用:处理Fiber节点到DOM节点的映射逻辑

核心工作内容:

  • 创建DOM节点,并将创建好的DOM节点赋值给workInProgress节点的stateNode属性
  • 通过appendAllChildren函数将DOM节点插入DOM树中(子Fiber节点对应的DOM节点挂载到父Fiber节点对应的DOM节点中)
  • 为DOM节点设置属性

render阶段主要为了寻找新旧Fiber树的不同,而commit阶段则负责实现更新。

3)副作用链effectList

副作用链可以理解为render阶段"工作成果"的一个集合,每一个Fiber节点都维护了一个独有的effectList,effectList不只记录当前需要更新的节点,还记录了后代节点信息等。

把所有需要更新的Fiber节点单独串成一串链表,方便后续有针对性地对它们进行更新。这就是所谓的"收集副作用"的过程。

effectList的重要属性:

  • firstEffect:链表的第一个Fiber节点
  • lastEffect:链表的最后一个Fiber节点

3.3 commit阶段

特点:决定的同步更新流程

  • before mutation阶段:DOM节点还没有被染到界面上去
  • mutation:负责DOM节点的渲染
  • layout:处理DOM染完毕之后的收尾逻辑,以及把fiberRoot的current指针指向worklnProgress Fiber树

4. Fiber架构下的concurrent模式

4.1 双缓冲模式

current树与worklnProgress树可以对标"双缓冲"模式下的两套缓冲数据,当current树呈现在用户眼前时,所有的更新都会由worklnProgress树来承接。workInProgress树将会在用户看不到的地方(内存里)悄悄地完成所有改变,直到current指针指向workInProgress树时,用户可以看到更新后的页面。

4.2 Scheduler

Fiber架构下的异步渲染(即Concurrent模式)的核心特征分别是"时间切片"与"优先级调度"。

  1. 时间切片
scss 复制代码
// legacy模式
function workLoopSync() {
    // Already timed out, so perform work without
    while(workInProgress!== null){
        performUnitOfwork(workInProgress);
    }
}

// concurrent模式
function workLoopConcurrent() {
    // Perform work until Scheduler asks to yield
    while(workInProgress !== null && !shouldYield()){
        performUnitOfwork(workInProgress);
    }
}

当shouldYield()调用返回为true时,则说明当前需要对主线程进行让出。此时 whille循环的判断条件整体为false,while循环将不再继续执行。

原理💡:React会根据浏览器的帧率计算时间切片的大小,并结合当前时间,计算出每一个切片的到期时间。在workLoopConcurrent函数中,每次执行都会判断当前切片是否到期,如果到期则让出主线程的使用权。

  1. 优先级调度

通过调用unstable_scheduleCallback发起调度,会结合任务的优先级信息为其执行不同的调度逻辑。

  • startTime: 任务的开始时间
  • expirationTime: expirationTime越小则任务的优先级就越高
  • timerQueue: 一个以startTime为排序依据的小顶堆。它存储的是 startTime大于当前时间的任务(待执行任务)
  • taskQueue: 一个以expirationTime为排序依据的小顶堆,它存储的是 startTime小于当前时间的任务(已过期任务)

第八节 React事件系统

1. 原生事件系统

W3C 标准约定了一个事件的传播过程要经过以下3个阶段:

  • 事件捕获阶段
  • 目标阶段
  • 事件冒泡阶段

事件委托:把多个子元素的同一类型的监听逻辑合并到父元素上,通过一个监听函数来管理的行为。将事件绑定在父元素上,利用事件冒泡原理,通过e.target判断是否为目标元素,从而决定是否触发事件。

2. React事件系统

1)统一绑定在document上

当事件在具体的DOM节点上被触发后最终都会冒泡到document上,document上所绑定的统一事件处理程序会将事件分发到具体的组件实例。

2)合成事件

React事件系统中将原生事件组合,形成合成事件。

合成事件在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与DOM原生事件相同的事件接口。

虽然合成事件并不是原生DOM事件,但它保存了原生DOM事件的引用。可以通过e.nativeEvent获取对应的原生事件。

React合成事件系统使React掌握绝对的主动权。

3. React事件系统工作流

1)事件绑定

事件的绑定是在completeWork中完成的。

completeWork内部有三个关键动作作:

  • 创建DOM节点(createlnstance)
  • 将DOM节点插入到DOM树中(appendAllChildren)
  • 为DOM节点设置属性(finalizelnitialChildren)

⚠️:由于React注册到document上的并不是某一个DOM节点对应的具体回调逻辑,而是一个统一的事件分发函数。所以即使同一事件存在多个回调函数,document也只会注册一次监听。

2)事件触发

事件触发的本质是对dispatchEvent函数的调用。

第九节 Redux原理

1. Flux框架

Flux不是一个具体的框架,而是一套由FackBook团队提出的应用架构。这套架构约束的是应用处理数据的模式。

Flux将每个应用都划分为四部分:View、Store、Action、Dispatcher

  • view视图层:表示用户界面,可以是任何形式的产物
  • Action动作:视图层发出的消息,会触发应用状态改变
  • Dispatcher派发器:负责对action进行分发
  • Store数据层:存储应用状态的仓库,同时具备修改状态的逻辑

Flux最核心的原理是严格的单向数据流,Redux是Flux思想的产物,虽然没有完全实现Flux,但是却保留了单向数据流的特点。

2. Redux

2.1 核心元素

  • Store:单一数据源,只读
  • Action:对变化的描述
  • Reducer:负责对变化进行分发和处理,并将新的数据返回给Store

2.2 工作流

获取状态:任何组件都可以以约定的方式从Store读取全局状态

修改状态:任何组件都可以通过合理的派发Action来修改全局状态

3. 工作原理

3.1 createStore

createStore方法是在使用Redux时最先调用的方法,是整个流程的入口。同时也是 Redux中最核心的API。

3.2 dispatch

dispatch动作,主要工作即"将Redux核心三要素串联起来"。

通过上锁,避免套娃式的dispatch

ini 复制代码
try{
    isDispatching = true;
    currentState = currentReducer(currentState,action);
}finally{
    isDispatching = false;
}

Redux完整流程如下:

3.3 subscribe

在store对象创建成功后,通过调用store.subscribe注册监听函数。

当dispatch action发生时,Redux会在reducer执行完毕后,将listeners数组中的监听函数逐个执行。

  • nextListener:订阅、触发、解除订阅操作的均是nextListener
  • currentListener:记录当前正在工作的listeners数组的引用,将它与可能发生改变的nextListeners区分开来,以确保监听函数在执行过程中的稳定性

4. 中间件

ini 复制代码
const store = createStore(
    reducer,
    initial_state,
    applyMiddleWare(middleWare1,middleWare2,...)
)

applyMiddleWare的作用就是向store中注入中间件(enhancer包装createStore)。

中间件是指可以增强createStore的工具,在Redux中所有的更新都是同步执行的,如果想要异步处理更新流程,则需要借助中间件。

redux-thunk就是一个异步Action解决插件。

中间件的工作流程图:

中间件的执行时机:action分发之后、reducer执行之前。

中间件的执行前提:利用applyMiddleWare对dispatch函数进行改写,使其在触发reducer之前,会先执行对redux中间件的链式调用。

第十节 React-router

1. 路由跳转

  1. 核心元素
  • BrowserRouter:路由器,根据映射关系匹配新的组件。分为BrowserRouter和HashRouter
  • Route:路由,定义组件与路径的映射关系。包括Route、Switch等
  • Link:导航,改变路径。如Link、NavLink、Redirect
  1. 路由器
  • BrowserRouter:通过H5的history API处理URL跳转
  • HashRouter:通过URL的hash属性处理路由跳转
  1. 前端路由的作用

当用户在任何路由下刷新页面,浏览器都可以根据当前URL进行资源定位,不会出现白屏问题。

  • hash模式:改变URL中#后面的部分,实现组件的切换
csharp 复制代码
// 感知hash变化
window.addEventListener('hashChange',functionn(event){
    ...
},false)
  • history模式:改变整个URL,实现组件切换
kotlin 复制代码
// 追加记录
history.pushState(data[,title][,url]);
// 修改记录
history.replaceState(data[,title][,url])
// 感知state变化
window.addEventListener('popState',functionn(event){
    ...
},false)

第十一节 高性能的React

1. 性能优化方式

  • 使用shouldComponentUpdate规避多余的更新逻辑
  • PureComponent + Immutable.js
  • React.memo 与 useMemo

2. shouldComponentUpdate

React组件会根据shouldComponentUpdate的返回值来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)。

默认值为true,即无条件的重渲染。

适用场景:

  • 父组件更新引发的子组件无条件更新
  • 组件内部的state变化引发的组件更新
kotlin 复制代码
shouldComponentUpdate(nextProps,nextState){
    // text没有改变则不更新
    if(nextProps.text === this.props.text){
        return false;
    }
    return true;
}

3. PureComponent

PureComponent内置了"在shouldComponentUpdate中对组件更新前后的props和state进行浅比较,并根据浅比较的结果决定是否需要继续更新流程"。

scala 复制代码
export const class APP extends React.PureComponent{
    ... ...
}
  • 基本数据类型:比较两次的值是否相等
  • 引用数据类型:比较两个值的引用是否相等

⚠️:如果数据没变,但是引用变化,则PureComponent还是会进行无用的重渲染;如数据变了,但是引用没变,则PureComponent不会重渲染,导致页面显示错误;

为了解决这个问题,需要借助于Immutable.js。

4. Immutable.js

Immutable.js表示不可变得值,或者持久性数据。

Immutable数据只要被创建出来,就不能被更改。我们对当前数据的任何修改动作,都会导致一个新的对象的返回,从而解决引用数据改变但是引用地址不变的问题。

5. React.memo

形式为一个高阶组件,用来对包装组件进行判断是否需要re-render。但是React.memo无法改变组件内部的变化。

arduino 复制代码
// component需要判断的组件
// areEqual比较逻辑,可选参数,不传时默认开启浅比较
export const APP = React.memo(component,areEqual);

6. useMemo

React新增的Hook,用于缓存一段逻辑或者变量,可以理解为一个更精细的React.memo。

两者区别:

  • React.memo:控制是否重渲染一个组件
  • useMemo:控制是否需要重复执行某一段逻辑
相关推荐
哑巴语天雨3 小时前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情3 小时前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起4 小时前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱4 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
高山我梦口香糖8 小时前
[react] <NavLink>自带激活属性
前端·javascript·react.js
撸码到无法自拔8 小时前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖8 小时前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js
乐闻x10 小时前
VSCode 插件开发实战(四):使用 React 实现自定义页面
ide·vscode·react.js
irisMoon0610 小时前
react项目框架了解
前端·javascript·react.js
web1508509664118 小时前
【React&前端】大屏适配解决方案&从框架结构到实现(超详细)(附代码)
前端·react.js·前端框架