React/视图更新那些事

845 阅读20分钟

WX20221102-124007@2x.png

React不同版本更新原理有部分差异,本文将从3个版本节点(分别为React15以及之前、React16、React18)讲述React的视图更新原理

在了解视图更新原理之前,首先有两个前备知识需要了解,React的事件合成机制以及事务机制,了解这两个机制会有助于我们更好地理解视图更新原理

事件合成

PS:由于不同版本的视图更新原理有部分差异,我是从React15开始看的源码,React16和React18是后看的,所以这个前备知识也是看的React15的源码。虽然不同版本的事件合成略有差别,但是整体思路是不变的。

React中的事件使用的并不是原生事件,而是React利用原生事件以及事件委托,自己实现了一套事件机制,包括事件注册、事件存储、事件分发、事件执行等。

虽然React自己实现了一套事件机制,但是这个事件机制也是依赖了原生事件,并不是独立创造了一套新的事件系统。

React合成事件会以事件委托方式将所有事件绑定在document(React16以及之前)或者root/挂载的容器上(Reac16之后)上,并通过多个原生事件合成一个自己的合成事件,在组件卸载阶段会自动销毁绑定的事件。

可以看一下下面的代码,通过下面代码,大概了解一下事件合成的流程。

F147AF77-45A7-454D-90A0-728EDFB0E2ED.png

事件注册

我们在parent和childern的div标签上面绑定分别绑定了一个点击事件,但React并不会直接把点击事件直接绑定在这两个div标签上,而是通过原生事件的事件委托,把这个点击事件绑定到了document或者root(React16之后)上,这个过程,我们称为事件注册,在这一阶段,依然是依赖了原生事件,而且React的事件是由多个或一个原生事件组合而成的,如onClick事件就是原生事件的clik事件,onChange事件是由blurchangeclickinputkeydownkeyupselectionchange8个原生事件合成,在某个元素上绑定onChange事件,就是在document中绑定了8个原生事件,所以说react的事件合成不是自己独立创造了一套新的事件系统,而是也需要依赖原生事件。至于多个事件可能会再一次动作中触发的,React是怎么处理这多个事件的等更深入的原理这里不会再更深入去讲。

这一过程是发生在组件挂在的过程中,通过listenTo绑定的,本篇文章主要内容并不是主要介绍合成事件,只是一个前备知识的了解,所以不会去关注具体的实现和细节,只关注整体流程和思想,想要仔细研究的可以去看源码,我会把对应的源码文件放在下面。

源码地址: ReactDOMComponent.js   500行

事件存储

上面说到在挂在组件的过程中,会发生事件注册,但其实在把事件挂载到document上之后,这个过程还会做一件事情,就是把事件存储起来,以便事件触发的时候可以找到相应的方法去执行

React存储事件是通过putListener方法把事件存储在listenerBank之中。

源码地址:EventPluginHub.js      135行

事件分发

当我们点击元素触发点击事件时,document监听到事件触发,调用dispatchEvent分发事件,这个方法主要干了四件事情

第一件事情放到后面说

第二件事情:收集事件

React会从触发事件的DOM节点开始获取其所有的父元素,并把该节点和其所有父元素放在数组中,然后依次遍历对每个元素对事件进行收集处理,我们从这里可以看出来React自己实现了一套冒泡机制,所以我们用原生的阻止冒泡事件event.stopPropagation()不起作用。

源码地址:ReactEventListener.js   58行      核心方法:handleTopLevelImpl

事件执行

第三件事情:生成合成事件对象

根据不同的时事件类型将原始事件包装对应的成事件合成对象,并模拟捕获和冒泡,补充合成事件对象的_dispatchInstances_dispatchListeners

源码地址:ReactEventListener.js   74行      核心方法:_handleTopLevel

第四件事情:执行事件

把事件组合成一个队列eventQueue,然后依次执行,依据事件类型判断是冒泡阶段还是捕获阶段执行,捕获阶段执行带Capture的事件,冒泡阶段执行不带的,如onClick是冒泡阶段,onClickPature是捕获阶段。

源码地址:ReactEventEmitterMixin.js 38行     核心方法:runEventQueueInBatch

为什么使用合成事件

  1. 提供合成事件对象(就像原生事件提供了事件对象一样,里面是时间相关的属性,而且包括了原生事件对象nativeEvent),自行实现了一套事件捕获到事件冒泡的逻辑, 抹平各个浏览器之前的兼容性问题,更好的跨平台。
  2. 事件只在 document/root 上绑定,并且每种事件只绑定一次,减少内存开销。
  3. 使用对象池来管理合成事件对象的创建和销毁,可以减少垃圾回收次数,防止内存抖动。(16之后删除)
  4. 方便事件统一管理

事务机制

React源码中,很多地方都是通过事务机制来调用需要执行的方法的,下面我们来看下什么是事务机制

下图是React源码上用来解释事务机制的图

A20581DA-B505-4BB1-BD05-B706FFD2FC29.png

这个图本身看起来还是比较简单的,虽然Reac的事务源码可能并不是这么简单,但是原理是非常简单的。其本质就是将我们传入的任何方法(目标方法)进行包装,分别在我们的方法之前和之后执行一些别的方法。

其原理是传入若干个wrapper,每个wrapper可以认为是有着initialite方法和close方法的对象,当我们调用事务的perform方法后,依次调用每个wrapper的initialite方法,接着执行我们自己传入的方法,最后再一次执行每个wrapper的close方法,大致流程如下

WX20221031-193558@2x.png

伪代码如下所示,可以自己运行下,打印下信息,就会更加了解

function myTransaction(...wrappers) {
    return function(myFunction) {
       wrappers.forEach(wrapper => wrapper.initialite())
       myFunction()
       wrappers.forEach(wrapper => wrapper.close())
    }
}
var wrapper1 = {
    initialite() {
        console.log('我是wrapper1的initialite')
    },
    close() {
        console.log('我是wrapper1的close')
    }
}
var wrapper2 = {
    initialite() {
        console.log('我是wrapper2的initialite')
    },
    close() {
        console.log('我是wrapper2的close')
    }
}
function myFunction() {
    console.log('这是我要自己执行的方法')
}
var perform = myTransaction(wrapper1, wrapper2)
perform(myFunction)
// 打印结果
// 我是wrapper1的initialite
// 我是wrapper2的initialite
// 这是我要自己执行的方法
// 我是wrapper1的close
// 我是wrapper2的close
复制代码

React15

在react15中,组件分为函数组件和类组件,但是函数没有状态和实例,因此只能在类组件中改变数据,更新视图。同时,react认为数据是不可变的,所以更改数据的某一个属性也不会触发视图的更新,所以在react15中视图更新只有setState方法。搞懂视图更新原理就需要去看setState做了什么。(这里不讨论forceUpdate

useState

以如下代码为例,我们看下useState到底做了什么

class App extends React.Component<any, any> {
constructor(props: any) {
    super(props);
    this.state = {
        parentCount: 0,
        childrenCount: 5,
    };
}
parent = () => {
    console.log('我是父元素的点击')
    this.setState({
        parentCount: this.state.parentCount + 1
    });
};

children = () => {
    console.log('我是子元素的点击')
    this.setState({
        childrenCount: this.state.childrenCount + 1
    });
}
render() {
    return (
        <div>
            <div>父元素计数{ this.state.parentCount }</div>
            <div>子元素计数{ this.state.childrenCount }</div>
            <div className='parent' onClick={this.parent}>
                <div className='children' onClick={this.children}>点我</div>
            </div>
        </div>
     );
   }
}
export default App;
复制代码

还记得合成事件的相关知识么,当我们点击children按钮时,会触发document的dispatchEvent,我们在合成事件的时候说过它做了四件事情,但是第一件事情当时我们并没有说是做了什么,其实第一件事情就是调用了ReactUpdates.batchedUpdates开启了批量更新。

react为什么要开启批量更新呢?是因为react调用setState的时候,如果不开启批量更新,每次执行setState的时候都会递归更新组件,哪怕我们更改同一个数据2次,我们只需要最后一次更改的值就行了,但是仍然,更新2次,造成性能上极大的浪费,所以react会在dispatchEvent中首先开启批量更新。


dispatchEvent: function(topLevelType, nativeEvent) {
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
        topLevelType,
        nativeEvent
    );
    try {
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); // ReactUpdates.batchedUpdates开启批量更新

    } finally {
        TopLevelCallbackBookKeeping.release(bookKeeping);
    }
},
复制代码

我们首先看一下react的批量更新原理

其实react15的批量更新原理非常简单,就是通过batchingStrategyisBatchingUpdates这个字段控制的,如果为true,就是开启批量更新,如果是false就未开启批量更新。

我们说过在dispatchEvent中,做的第一件事就是开启批量更新,我们看一下具体是怎么做的。

dispatchEvent中调用了ReactUpdates.batchedUpdates方法,我们看下这个方法

function batchedUpdates(callback, a, b, c, d, e) {
    ensureInjected(); // 注入batchingStrategy对象
    return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
源码:ReactUpdates.js  105行
复制代码

我们可以看到,batchedUpdates中调用了batchingStrategy.batchedUpdates这个方法,但是batchingStrategy这个对象不是像平常一样通过导入引进的,而是通过ensureInjected这种方式注入的,我们要找一下注入对象的源码,这种通过注入的方式引用的源码不太好找,我们就直接给出源码位置在ReactDefaultBatchingStrategy.js这个文件中,我们看下这里面做了什么。(ps: react15使用的是自定义模块系统,叫做Haste,也使用require,但是和commonjs是有区别的,react15中,所有的模块名是唯一的,所以只需要名字来引用模块)


var ReactDefaultBatchingStrategy = {
    // 批量更新标示
    isBatchingUpdates: false,
    batchedUpdates: function(callback, a, b, c, d, e) {
        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
        // 将批量更新标示改为true,开启批量更新
        ReactDefaultBatchingStrategy.isBatchingUpdates = true;
        if (alreadyBatchingUpdates) {
            return callback(a, b, c, d, e);
        } else {
            return transaction.perform(callback, null, a, b, c, d, e);
        }
    },
};
源码:ReactDefaultBatchingStrategy.js  45行
复制代码

从源码中我们可以看到批量更新开关isBatchingUpdates,同时还有个batchedUpdates方法,这个方法会把批量更新开关打开,而ReactUpdates.batchedUpdates调用了batchingStrategy.batchedUpdates,这个方法也就是上面源码中的batchedUpdates。到此,我们就搞懂了React合成事件中是怎么触发批量更新的。

接着走到了transaction.perform,大家还记得这个么,就是我们上面提到的通过事务调用我们的方法。这个事务是由两个wrapper构成的,分别是

var FLUSH_BATCHED_UPDATES = {
    initialize: emptyFunction,
    close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
复制代码
var RESET_BATCHED_UPDATES = {
    initialize: emptyFunction,
    close: function() {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false;
    },
};
复制代码

我们可以看到这两个wrapper的initialize都是个空的函数,所以直接走到我们传入的handleTopLevelImpl方法,这个方法就是我们在合成事件中提到的事件分发的核心方法,之后就会走一遍合成事件的整个过程,最后触发我们定义的事件的方法。我们的例子是定义了个onClick事件方法,最终就会走到下面这个方法

children = () => {
    console.log('我是子元素的点击')
    this.setState({
        childrenCount: this.state.childrenCount + 1
    });
}
复制代码

这个方法我们只调用了setState这一个方法,接下来终于到我们的setState原理了。


ReactComponent.prototype.setState = function(partialState, callback) {
    this.updater.enqueueSetState(this, partialState);
    if (callback) {
        this.updater.enqueueCallback(this, callback, 'setState');
    }
};
源码:ReactBaseClasses.js  60行
复制代码

setState调用了updater,这个updater我们称为更新器,视图更新就是通过这个更新器进行更新的。每个react组件在实例话的时候也都会生成一个自己的更新器,也就是说每个组件都有一个更新器。

更新器做了两个事情

第一件事:将对应组件及状态分别放入队列中


enqueueSetState: function(publicInstance, partialState) {
    var queue =
        internalInstance._pendingStateQueue ||
        (internalInstance._pendingStateQueue = []);
    // 将把状态放入队列中
    queue.push(partialState);
    // 把组件实例放入待更新组件队列中
    enqueueUpdate(internalInstance);
},
源码:ReactUpdateQueue.js  232行
复制代码

我们看下enqueueUpdate具体代码

function enqueueUpdate(internalInstance) {
    ReactUpdates.enqueueUpdate(internalInstance);
}
源码:ReactUpdateQueue.js  22行


function enqueueUpdate(component) {
    // 注入batchingStrategy对象
    ensureInjected();
    if (!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return;
    }
    // 把组件放进待更新队列中
    dirtyComponents.push(component);
    if (component._updateBatchNumber == null) {
        component._updateBatchNumber = updateBatchNumber + 1;
    }
}
源码:ReactUpdates.js  213行
复制代码

把组件放入更新队列的时候,如果已经开启批量更新了,就把组件直接放进对列中,等待批量更新。如果没有开启的就直接更新。

到这里,setState执行完毕。但是这时候并没有更新视图呀?是的,setState只是把状态和组件放进相应的队列中,真正的更新是发生在事务的close阶段,我们在上面提到过两个wrapper,这时候就走到了第一个wrapper的close方法,也就是ReactUpdates.flushBatchedUpdates,这才是真正更新视图的地方,我么可以看到会循环dirtyComponents获取每一个组件进行更新。


var flushBatchedUpdates = function() {
    while (dirtyComponents.length || asapEnqueued) {
        // 我们在setState的时候把组件实例放进了这个队列中,这里循环获取并更新
        if (dirtyComponents.length) {
            var transaction = ReactUpdatesFlushTransaction.getPooled();
            transaction.perform(runBatchedUpdates, null, transaction);
            ReactUpdatesFlushTransaction.release(transaction);
        }
        if (asapEnqueued) {
            asapEnqueued = false;
            var queue = asapCallbackQueue;
            asapCallbackQueue = CallbackQueue.getPooled();
            queue.notifyAll();
            CallbackQueue.release(queue);
        }
    }
};
源码:ReactUpdates.js 121行
复制代码

在更新之前会处理一下状态,我们在setState的时候把状态都放到了一个队列里面,在更新视图前会把这个状态处理一下

_processPendingState: function(props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;
    if (!queue) {
        return inst.state;
    }
    if (replace && queue.length === 1) {
        return queue[0]
    }
    var nextState = Object.assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
        var partial = queue[i];
            Object.assign(
            nextState,
            typeof partial === 'function'
                ? partial.call(inst, nextState, props, context)
                : partial,
    );
}
return nextState;
},
源码:ReactCompositeComponent.js   896行
复制代码

我们存在队列里面的状态都会被合并,这里的合并只会合并一层,生成最终的一个状态对象。

接着就会从当前组件递归更新,走dom-diff,最终更新视图。

更新完视图,就会把待更新组件队列置为空

dirtyComponents.length = 0;
// 这里也是通过事务实现的,就不细说,大家可以自己去看
// 源码:ReactUpdates.js 37行
复制代码

接着就会走事务的最后一个close方法,就是把批量更新状态关闭

ReactDefaultBatchingStrategy.isBatchingUpdates = false;
复制代码

到此,整个react15的视图更新就完毕了。

我么总结下整个流程,其实很简单

  1. 开启批量更新
  2. 走合成事件过程,最终会调用到我们自己绑定事件的方法上
  3. 将要更改的组件状态放入当前组件的状态数组中
  4. 将当前组件实例放入待更新队列中
  5. 循环组件队列,获取每个待更新组件
  6. 合并组件中放入的所有状态,得到一个最终的状态
  7. 每个组件递归更新所有子组件(这里会有dom-diff操作,然后在更新)
  8. 把待更新队列置空
  9. 把批量更新开关关闭

WX20221102-111748@2x.png

问题

1. useState视图更新到底是同步的还是异步的

useState的批量更新很简单,就是依赖isBatchingUpdates ,如果执行useState的时候这个字段是true,那么就是异步批量更新的,但是如果字段是false,就是同步更新,比如


var children = () => {
    console.log('我是子元素的点击');
    // 这里是异步更新,因为在执行这个方法的时候,事件合成里面会把isBatchingUpdates置为true
    this.setState({
        childrenCount: this.state.childrenCount + 1
    });
    setTimeout(() => {
        // 这里是同步更新,因为在执行这个方法的时候,已经是下一个事件循环了,isBatchingUpdates为false了
        this.setState({
            childrenCount: this.state.childrenCount + 1
        });
    })
}
复制代码

react也提供了强制异步更新的操作,ReactDOM.unstable_batchedUpdates

setTimeout(() => {
    // 这里也变成批量更新了
    ReactDOM.unstable_batchedUpdates(() => {    
        this.setState({
            childrenCount: this.state.childrenCount + 1
        })
        this.setState({
            childrenCount: this.state.childrenCount + 2
        })
    });
}, 1000);
复制代码

ReactDOM.unstable_batchedUpdates原理很简单,这个方法其实就是ReactUpdates.batchedUpdates,调用ReactUpdates.batchedUpdates,就会把批量更新状态置为true

var ReactDOM = {
    unstable_batchedUpdates: ReactUpdates.batchedUpdates,
};
源码:ReactDOM.js  37行
复制代码

2. react的更新颗粒度

react不像vue,不会进行数据劫持,所以react不知道哪些组件用到了对应的状态,因此在使用useState改变状态之后,会从当前组件开始,递归更新所有子组件且不能中断,哪怕子组件状态没有改变也会更新,react的更新颗粒度也不会想vue这么细,具体到使用到的组件,这就会导致性能上的问题。react提供了shouldComponentUpdate钩子,如果这个钩子返回false,就不会更新组件。

3. useState改变状态的时候,哪怕数据没有变,也会更新视图么

会的,因为react的思想就是数据不可变的,只要你使用useState,那么就会更新视图,我们可以使用可以使用PureComponent解决,PureComponent原理也很简单,就是通过对前后状态进行浅比较实现shouldComponentUpdate钩子返回不同的值。

React16

React16有两个重大的改变,第一个是加入了fiber的概念,第二个是引入了hooks

hooks的加入让函数式组件也具有了状态(状态存在对应的fiber上),useState, useReducer等也可以更新视图了

useState

useState分为两个阶段

第一个阶段是mount阶段,第二个阶段是update阶段

当我们刚进入页面,react初始化的时候,也就是第一个阶段的时候,这时每次调用useState都会调用mountState

function mountState(initialState) {
    // 生成一个hook节点  
    const hook = mountWorkInProgressHook();
    if (typeof initialState === 'function') {
        initialState = initialState();
    }
    // 将初始状态挂在hook节点中  
    hook.memoizedState = hook.baseState = initialState;
    // 每个hook节点创建一个队列
    const queue = (hook.queue = {
        last: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,
        lastRenderedState: (initialState: any),
    });
    const dispatch =  (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
    ));

    // 返回  状态值和更新的方法  
    return [hook.memoizedState, dispatch];
}
源码:ReactFiberHooks 741行  React: 16.8.6
复制代码

初始化阶段mountState会创建hook节点,并将传进来的初始值赋值给hook,最后会把这个值作为数组的第一个值返回出去,我们也可以从这里看到我们实际使用的时候为什么能在数组的第一个值中获取到对应的状态

接下来看下返回的第二个参数,dispatch,这个很好理解但也很重要,就是利用bind方法预置了两个参数,分别为当前组件对应的fiber(currentlyRenderingFiber)和对应的队列,至于为什么这么做,是因为我们在调用dispatch的时候,react就可以知道是哪个组件的fiber需要更新,而我们在使用时候只用传入要改变的状态值就好了

我们再看下hook节点生成的过程

function mountWorkInProgressHook() {
    const hook: Hook = {
        memoizedState: null,
        baseState: null,
        queue: null,
        baseUpdate: null,
        next: null,
    };
    if (workInProgressHook === null) {
        //因为是第一次创建,所以直接把workInProgressHook指向新建的hook节点
        firstWorkInProgressHook = workInProgressHook = hook;
    } else {
        // 如果不是第一次创建,那么就会直接在上个hook节点上的next属性上挂当前创建的hook节点,这样形成了链表结构,通过next链接
        workInProgressHook = workInProgressHook.next = hook;
    }
    return workInProgressHook;
}
复制代码

这里比较重要的是形成了链表结构,下一个hook节点挂在上一个hook节点的next属性上,举个例子

function myReact() { 
    const [count, setCount] = useState(0)
    const [count1, setCount1] = useState(1)  
    function myClick() {
        setCount((count) => { return count + 1; }) 
    } 

    function myClick1() {
        setCount((count) => { return count + 1; })
    }
    return ( 
        <div>
            <div onClick={myClick}> count is: {count} </div> 
            <div onClick={myClick1}> count1 is: {count1} </div>  
        </div>  
    ) 
}
复制代码

我们用了两个hooks,都是useState,那么在页面一进来执行这个函数的时候,走到这两个useState的时候都会走mountState,只不过第一个hook节点的next属性上挂了第二个的hook节点,最终的数据结构会是

{
    "memoizedState": 0,
    "baseState": null,
    "queue": null,
    "baseUpdate": null,
    "next": { 
         "memoizedState": 10,
         "baseState": null,
         "queue": null,
         "baseUpdate": null,
     }
}
PS: 里面的数据不是真的,只是为了展示结构
复制代码

之后就会交给react调度渲染页面。这里不再深入相关知识了。

那么接下来当我们点击按钮,执行了setCount之后会发生什么呢,我们来一起看下disPatch方法

function dispatchAction(fiber, queue, action) {
    // 创建更新
    const update = {
        expirationTime,
        action,
        eagerReducer: null,
        eagerState: null,
        next: null,
    };
    // 获取更新队列之前的更新
    const last = queue.last;
    // 这里比较乱,其实就是最新的更新指向前一个的更新,最后一个更新又会指向第一个更新,形成一个环
    if (last === null) {
        // 第一次更新,
        update.next = update;
    } else {
        const first = last.next;
        if (first !== null) {
            // 将新生成的update的next指向第一个update
            update.next = first;
        }
        last.next = update;
    }
    queue.last = update;
    scheduleWork(fiber, expirationTime);
    }
}
复制代码

disPatchAction中的方法很多,但是我们只关心最主要的,就是以上几行,首先会创建一个更新,然后获取更新队列里之前的更新,形成一个环,类似下面这种 WX20221101-212140@2x.png

我们看下最终的数据结构(数据是假的,只是看下结构),我们可以看到形成了个环

88905437-47F0-4580-8919-81FB7C95D2E7.png

之后就会执行scheduleWork,进行React调度,重新走React调度流程,再次进入useState方法,由于这次不是初始化,因此会走UpdateState,这里的判断逻辑如下

ReactCurrentDispatcher.current =
    nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
源码:ReactFiberHooks.js  369行
复制代码

所以这次的useState走的是UpdateState

D053E3DD-417A-4F50-B927-7953D2A0E1CC.png

我们接着看updateState,会发现updateState其实是调用了updateReducerupdateState就是个特殊的updateReducer

function updateState(initialState) {
    return updateReducer(basicStateReducer, initialState);
}
源码:ReactFiberHooks.js 766行

basicStateReducer就是个内置的方法,就是执行我么传入的action

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

源码:ReactFiberHooks.js 574行
复制代码

我们看updateReducer具体做了什么

function updateReducer(reducer, initialArg, init,) {
    // 继续创建hook节点  
    const hook = updateWorkInProgressHook();
    // 获取更新队列  
    const queue = hook.queue;
    queue.lastRenderedReducer = reducer;
    // re-render逻辑
    if (numberOfReRenders > 0) {
        // This is a re-render. Apply the new render phase updates to the previous

        // work-in-progress hook.
        const dispatch = queue.dispatch;
        if (renderPhaseUpdates !== null) {
            const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
            if (firstRenderPhaseUpdate !== undefined) {
                renderPhaseUpdates.delete(queue);
                let newState = hook.memoizedState;
                let update = firstRenderPhaseUpdate;
                do {
                    const action = update.action;
                    newState = reducer(newState, action);
                    update = update.next;
                } while (update !== null);
                if (!is(newState, hook.memoizedState)) {
                    markWorkInProgressReceivedUpdate();
                }
                hook.memoizedState = newState;
                if (hook.baseUpdate === queue.last) {
                    hook.baseState = newState;
                }
                queue.lastRenderedState = newState;
                return [newState, dispatch];
           }
        }
        return [hook.memoizedState, dispatch];
    }
    // 非re-render
    const last = queue.last;
    const baseUpdate = hook.baseUpdate;
    const baseState = hook.baseState;
    let first;
    if (baseUpdate !== null) {
        if (last !== null) {
            last.next = null;
        }
        first = baseUpdate.next;
        } else {
            first = last !== null ? last.next : null;
        }
        if (first !== null) {
            let newState = baseState;
            let newBaseState = null;
            let newBaseUpdate = null;
            let prevUpdate = baseUpdate;
            let update = first;
            let didSkip = false;
            do {
                const updateExpirationTime = update.expirationTime;
                if (updateExpirationTime < renderExpirationTime) {
                    if (!didSkip) {
                        didSkip = true;
                        newBaseUpdate = prevUpdate;
                        newBaseState = newState;
                    }
                    if (updateExpirationTime > remainingExpirationTime) {
                        remainingExpirationTime = updateExpirationTime;
                    }
                } else {
                    if (update.eagerReducer === reducer) {
                        newState = update.eagerState;
                    } else {
                        const action = update.action;
                        newState = reducer(newState, action);
                    }
                }
                prevUpdate = update;
                update = update.next;
            } while (update !== null && update !== first);
            if (!didSkip) {
                newBaseUpdate = prevUpdate;
                newBaseState = newState;
            }
            if (!is(newState, hook.memoizedState)) {
                markWorkInProgressReceivedUpdate();
            }
            hook.memoizedState = newState;
            hook.baseUpdate = newBaseUpdate;
            hook.baseState = newBaseState;
            queue.lastRenderedState = newState;
        }
        const dispatch = queue.dispatch;
        return [hook.memoizedState, dispatch];
}
复制代码

更新也是会像初始化阶段一样,生成hook节点挂在链表后面,只不过这时候的hook属性都是有值的

const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    queue: currentHook.queue,
    baseUpdate: currentHook.baseUpdate,
    next: null,
};

复制代码

接着会获取更新队列,循环执行reducer,得到最最终的state,其中reducer就是basicStateReducer,然后把这个state和存在更新队列里的disPatchAction返回回去,然后会像mount过程一样交给react调度渲染页面。

上面代码很长,是因为区分了是不是re-render,并有解除update队列环的问题,我们在这里就不细说了。

fiber

Fiber的加入使得视图更新操作发生了变化,而Fiber又是16新增的核心算法,整个架构发生了变化,我们这里只是大概说一下Fiber干了什么,具体的原理逻辑这里不会过多概述。

Fiber可以理解为把组件的树形结构变为了链表结构。

Fiber可以理解为执行单元,能够把工作任务分割成更小的单元。

Fiber也可以理解为流程让出机制,当有更高优任务时,可以暂停这次任务,把控制权交出去,等高优任务完成后继续开始这次任务。

Fiber对视图更新的影响

Fiber的加入之后,react从当前组件递归更新,变成了从root开始遍历更新。不是说从当前组件更新就会导致一些性能问题么(例如数据没变也更新了),现在为什么反而从root开始更新,不是更浪费性能么,那是因为fiber的加入,使得组件树形结构变成了链表结构,可以打上标记告知哪些组件需要更新,哪些不需要,这样就可以跳过某些组件,也可以在又其他高优先级的任务时,暂停更新,处理更高优的任务,不会阻塞主线程,等高优任务完成后,再回来继续更新组件等优先级低的事情。

react15架构

WX20221101-142407@2x.png

react15中,由Reconciler计算出变化的组件,然后交给stack RendererRenderer更新视图到页面上,这里面有两个问题,一是这里组件是树结构,更新的时候是会递归更新,二是一旦开始递归更新,无法打断执行,如果这里卡死的话,那么用户什么操作也执行不了。

react16架构

WX20221101-142805@2x.png

React16增加了调度器的概念,并且协调器变成fiber协调器。调度器可以计算优先级任务,看看有没有更高优的任务,没有的话,交给fiber协调器,协调器可以计算出组件变化,然后给变化的组件打上标记,交给渲染器,渲染器根据标记更新视图到页面上。而且虚线框的过程可以因为其他高优任务或时间不够等问题随时中断与恢复(因为这是发生在内存中的,用户感受不到,可以随意打断)。协调器也能够只对变化了的组件打标记,使得渲染器更新变化了的视图。

React18

React18之中,批量更新原理发生了变化,之前是通过ReactDefaultBatchingStrategy.isBatchingUpdates这个开关控制,造成的问题就是异步代码会导致批量更新失效(提供了unstable_batchedUpdates可以来强制批量更新),React18 里面,则不再用这个属性值当作开关,而是通过优先级来批量更新,相同优先级的任务进行合并,一次更新,解决了React18之前中异步代码导致批量更新失效问题,成为并发模式。通过下面方法使用并发模式

// ReactDOM.createRoot使用并发模式
ReactDOM.createRoot(document.getElementById('#root')).render(<App />) 

作者:用户970908572949
链接:juejin.cn/post/716127…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。