likes
comments
collection
share

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

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

内容讲到啥程度?

我最近在读框架源码,读的过程中发现render对于这个框架来说真的太重要了,因为它把能干的事都干了,所以我准备将这个方法拆成几篇文章来讲解,尽量做到每个方法都不漏。

这篇文章就讲到创建一个更新任务就可以了,至于后面的操作就着重放到下篇去讲解了。

什么叫做更新任务?其实就是一个对象。大家肯定都知道一个知识点:ReactDOM.rendersetState等可以触发React的更新操作。那其实这个知识点就是在问:哪些操作可以创建更新对象

你将从本文收获什么?

在正式进入学习之前,我先告诉大家阅读本文后你将学到哪些知识

  • React应用的启动模式有哪些?它是用来干啥的?
  • React在初始渲染组件时,更新对象是如何维护的?

环境准备

首先新建一个html文件,然后引入react@17、react-dom@17、babel的cdn链接。

文件内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <script type="text/babel">
        class App extends React.Component {
            constructor(props){
                super(props)
                this.state = {}
            }
            render (){
                return <div className='app-class'>这是class组件</div>
            }
        }
        ReactDOM.render(<App />, document.getElementById('root'));
    </script>
</body>
</html>

然后打开你的浏览器访问一下这个文件。随后右键检查打开开发者面板,在这个面板里选择Sources面板。随后点击更多按钮(如下图):

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

然后选择open file选项

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

最后在搜索框里输入react.development.jsreact-dom.development.js 文件,找到文件后,我们就可以开始调试之旅了。

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

当然你也可以选择其他的调试方法:

  • React官方文档,源码贡献那一章节。
  • 在上一步的基础上实现代码热更新,代码文件自动定位等等(推荐:冴羽zxg_神说要有光

源码调试与分析

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

从上张图我们发现,ReactDOM.render啥也没干,直接进入到了legacyRenderSubtreeIntoContainer 这个方法里。

function render(element, container, callback){
    /***
        element:<App />
        container: id为root的dom节点
    */
    return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}

我们接着往下看legacyRenderSubtreeIntoContainer 方法。

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

因为container表示的是root的dom节点,所以在没有进入到if (!root)判断的时候,root一定是undefined,因为它是从dom节点里取了一个不存在的属性嘛。

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
    /***
        parentComponent: null,
        children: <App />,
        container: id为root的dom节点,
        forceHydrate: false,
        callback: null
    */
    var root = container._reactRootContainer;
    var fiberRoot;
    if (!root) {
      // Initial mount
      root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
      fiberRoot = root._internalRoot;

      unbatchedUpdates(function () {
        updateContainer(children, fiberRoot, parentComponent, callback);
      });
    }
    return getPublicRootInstance(fiberRoot);
}

当执行完 root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate); 这句代码的时候,我们发现root变量的值是下面这样:

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

图里可能看不清,root值大致如下:

let root: ReactDOMBlockingRoot = {
    _internalRoot: { 
        tag: 0,
        containerInfo: div#root,
        context: null,
        current: {
            tag: 3,
            lanes: 0,
            mode: 0,
            updateQueue: {
                baseState: null,
                effects: null,
                firstBaseUpdate: null,
                lastBaseUpdate: null,
                ...
            },
            ...
        },
        ...
    }
}

这里就出现了第一个知识点:React应用的启动方式有哪些

React应用的启动方式

在React@17里,React细分了3种启动模式,分别如下:

  • Legacy模式。由ReactDOM.render触发,这个方法在顶层函数里创建了一个root变量,这个root变量的类型是ReactDOMBlockingRoot
  • Blocking 模式。由ReactDOM.createBlockingRoot.render触发,同样也是创建了一个变量,这个变量的类型是fiberRoot
  • Concurrent 模式。由ReactDOM.createRoot.render触发,也是创建了一个变量,变量的类型是HostRootFiber

这也很好的解释了为什么刚才的root变量的类型是ReactDOMBlockingRoot。因为我们的启动应用的方式是ReactDOM.render

那这3种启动模式有什么区别呢?

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

参考:React17React18

继续阅读

我们继续往下看,应该执行下面这个函数了(updateContainer):

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
    /***
        parentComponent: null,
        children: <App />,
        container: id为root的dom节点,
        forceHydrate: false,
        callback: null
    */
    // 代码省略...
    if (!root) {
      // Initial mount(代码省略) ...
      unbatchedUpdates(function () {
        updateContainer(children, fiberRoot, parentComponent, callback);
      });
    }
    return getPublicRootInstance(fiberRoot);
}

unbatchedUpdates 其实就是个壳,直接执行里面的updateContainer函数。

从源码的角度告诉你:ReactDOM.render是如何渲染class组件的(上)

updateContainer方法如下:

function updateContainer(element, container, parentComponent, callback) {
    var current$1 = container.current;
    // 获取更新的时间戳
    var eventTime = requestEventTime();
    // 请求更新的优先级
    var lane = requestUpdateLane(current$1);
    var context = getContextForSubtree(parentComponent);
    if (container.context === null) {
      container.context = context;
    }
    // 创建更新任务
    var update = createUpdate(eventTime, lane);
    update.payload = {
      element: element
    };
    // 维护更新对象
    enqueueUpdate(current$1, update);
    
    // 之后的篇章再讲解...
}

获取产生更新的时间戳

function requestEventTime() {
    // 省略其他代码...
    currentEventTime = now();
    return currentEventTime;
}

这个时间戳会被下文update对象里的一个eventTime属性保管。

获取本次更新的lane属性

function requestUpdateLane(fiber) {
    // 省略其他代码...
    return SyncLane;
}

这个requestUpdateLane 方法 和 上面的 requestEventTime 方法都返回了一个值并且赋值给了update对象。

至于这2个方法是干什么用的,这里先不用管。我们只知道它俩会给update对象赋值即可(因为这两个方法的作用需要根据大量情况的跟踪才能看明白,而不是通过一次render跟踪就能看明白的,后面会有专门的文章来讲解)。

产生更新对象

function createUpdate(eventTime, lane) {
    var update = {
      eventTime: eventTime,
      lane: lane,
      tag: UpdateState,
      payload: null,
      callback: null,
      next: null
    };
    return update;
 }

维护更新对象

function enqueueUpdate(fiber, update) {

    /***
        fiber: {
            ...,
            mode: 0,
            lanes: 0,
            updateQueue: {
                baseState: null,
                effects: null,
                firstBaseUpdate: null,
                lastBaseUpdate: null,
                shared: {
                    pending: null
                }
            }
        }
        
        update: {
            eventTime: 23483.5,
            lane: 1,
            next: null,
            tag: 0,
            payload: {
                element: <App />
            }
        }
    */

    var updateQueue = fiber.updateQueue;
    /***
        updateQueue: {
            baseState: null,
            effects: null,
            firstBaseUpdate: null,
            lastBaseUpdate: null,
            shared: {
                pending: null
            }
        }
    */
    
    var sharedQueue = updateQueue.shared;
    /***
        sharedQueue: {
            pending: null
        }
    */
    
    var pending = sharedQueue.pending; // null

    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
      // update对象里的next属性指向了自己,自己形成了环形链表。
    } else {
      update.next = pending.next;
      pending.next = update;
    }

    sharedQueue.pending = update; // fiber里的属性也指向了这个环形链表。
  }

上面的代码其实一句话就能概括:fiber里有一个属性指向了update对象,并且这个update对象是一个环形链表,由于我们的APP组件太过简单,所以导致这个update对象只有一个节点

本文总结

截止到目前为止,我们知道React在初始化的阶段里 先是确定启动方式 -> 然后创建update对象 -> 最后是让fiber与update对象关联这样的一个流程。

那么下一篇我们继续来看看React在后面都干了什么。

本文到这里其实也结束啦,如果上述过程中出现了错误,欢迎各位大佬指正。如果对您有帮助,欢迎给个点赞+关注,那么再见啦~~