上一篇文章说到React代码经过编译后,会将JSX的语法都经过react.createElement函数转换成一个对象传入的ReactDOM.render中。本章将会接着阅读ReactDOM.render中是如何将元素生成虚拟DOM以及如何渲染到页面中的。
Lam:小前端读源码 - React16.7.0(一)
接着上一章说到的,去看看ReactDOM里面到底有什么。从源码当中我们发现ReactDOM提供了一些属性和方法。其中的作用自行查文档了。
- createPortal
- findDOMNode
- hydrate
- render
- unstable_renderSubtreeIntoContainer
- unmountComponentAtNode
- unstable_createPortal
- unstable_batchedUpdates
- unstable_interactiveUpdates
- flushSync
- unstable_createRoot -> React 17版本将会废除
- unstable_flushControlled
- __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
我们经常使用的一般都是ReactDOM.render这个API,将我们的组件渲染到页面中,我们就一起看看render里面到底做了什么事情吧!
首先render接受3个参数,element、container、callback。从React16版本开始,element就是我们经过react.createElement后返回的对象。container就是我们需要渲染到的元素。render将不会return会组件对象了,改为在callback中返回。在render中会将数据传入一个叫做legacyRenderSubtreeIntoContainer的方法中。
legacyRenderSubtreeIntoContainer
首先legacyRenderSubtreeIntoContainer会检查传入的container的类型,如果传入的类型不符合规定将会报错。那么可以传入什么类型呢?
代码语言:javascript复制function isValidContainer(node) {
return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE || node.nodeType === COMMENT_NODE && node.nodeValue === ' react-mount-point-unstable '));
}
react会检测传入的container的nodeType是需要等于1、9、11或者8,如果nodeType等于8的情况下,还需要nodeValue必须等于" react-mount-point-unstable "。
并且如果controller是body的话,也会出现waring提示!
代码语言:javascript复制!(container.nodeType !== ELEMENT_NODE || !container.tagName || container.tagName.toUpperCase() !== 'BODY') ? warningWithoutStack$1(false, 'render(): Rendering components directly into document.body is ' 'discouraged, since its children are often manipulated by third-party ' 'scripts and browser extensions. This may lead to subtle ' 'reconciliation issues. Try rendering into a container element created ' 'for your app.') : void 0;
接着react会判断你当前传入的contrainer是不是已经是一个react的根组件,会通过判断传入的contrainer中是否存在_reactRootContainer这个对象进行判断,react会在渲染的同时将_reactRootContainer注入到contrainer对象中。
并且会根据是否存在_reactRootContainer进行不同的渲染方式,我们先看当前的contrainer是没有渲染过任何reactElement的情况下如何执行的。
接着在legacyRenderSubtreeIntoContainer函数中会执行几个比较重要的函数将传入的container和children以及children中的children一一渲染的函数了,最终将组件对象返回的一系列操作。
- legacyCreateRootFromDOMContainer
- unbatchedUpdates
- render / legacy_renderSubtreeIntoContainer
legacyCreateRootFromDOMContainer
如果当前传入的container并不是一个已经初始化的容器,那么将会执行legacyCreateRootFromDOMContainer这个函数,那么从源码也看到了,在执行完legacyCreateRootFromDOMContainer之后其实就会将返回的值赋值到container中的_reactRootContainer了。如果之后还对同一个container进行render的话,就会判断到存在_reactRootContainer这一个对象,那么就会进入这个判断中了。那么legacyCreateRootFromDOMContainer到底帮我们做了什么事情呢?我们先看看它返回了什么东西回来。
好吧,我们并不知道里面的是什么,那么只能看看legacyCreateRootFromDOMContainer函数里面执行了什么东西了。
首先react需要判断你是不是服务器渲染,其实早在ReactDOM对象内到legacyCreateRootFromDOMContainer之间有很多关于服务器渲染的判断,但是我们现在目标是先搞懂react在浏览器渲染的流程和逻辑,所以我们会先跳过一些服务器渲染的流程和逻辑。那么legacyCreateRootFromDOMContainer一开始就会通过传入的forceHydrate和shouldHydrateDueToLegacyHeuristic去判断是不是服务器渲染,那么结果当然是false啦。那么就会进入到一个清楚container内容的判断中。
清楚内容的逻辑是先获取到container的lastChild,然后判断lastChild是否为一个元素,并且这个元素不能带有data-reactroot这个属性,否则报错。然后删除掉这个子元素,这是一个循环直到container的lastChild为null才会停止。
有时候我们需要在react.js和业务js加载前出现一些占位图或者loading图片这一些提高首屏的方式,那么就无可避免的在contrainer里面写入一些默认的html标签去实现占位样式了。
代码语言:javascript复制<div id="root">
<p>占位</p>
<p>占位</p>
<p>占位</p>
...
</div>
这样就会导致在legacyCreateRootFromDOMContainer中需要删除container内的子元素要循环多次,所以一个优化的点就是把里面同级的内容包在一个元素中,那么只需要循环一次就可以了。
代码语言:javascript复制<div id="root">
<div>
<p>占位</p>
<p>占位</p>
<p>占位</p>
...
</div>
</div>
这里其实也只是判断是否为服务器渲染。
最后就将参数传入ReactRoot并实例化ReactRoot后返回。
ReactRoot
从源码看到ReactRoot这个构造函数就是通过一系列的函数初始化了一堆属性(应该是属于状态之类的变量)。然后赋值到this._internalRoot中。执行的函数顺序如下:
- createContainer
- createFiberRoot
因为react在16.2就已经修改为了Fiber架构,所以这里createFiberRoot只是其中一个创建Fiber一种方式而已。
暂时我们先用到的是createHostRootFiber这个函数。所有的fiber都是FiberNode的实例。
最终输出的就是一开始我们看见的那个对象。
当然ReactRoot的原型上有以下4个API:
- render
- unmount
- legacy_renderSubtreeIntoContainer
- createBatch
我们常用的估计也就render和unmount这两个了。而legacy_renderSubtreeIntoContainer和createBatch这两个API在文档中其实也没有说明。
到这位置其实就是整个container的_reactRootContainer初始化过程了,那么我们就回到legacyRenderSubtreeIntoContainer这个函数中继续往下看渲染过程了。
legacyRenderSubtreeIntoContainer会对我们ReactDOM.render传入的第三个参数(回到函数)进行一个包装。最终返回的是this._internalRoot.current.child.stateNode。
接着就是一个批处理的判断,但是还没有发现这个批处理是什么情况会使用,我们先忽略它。
到这里为止,其实都是创建关键的root根对象。接下来就是root.render将要渲染到根对象中的App的ReactElement对象进行一些操作了。
root.render
需要关注的是在render函数内有2个地方是需要注意的:
- ReactWork
- updateContainer
ReactWork是什么东西呢?
其实ReactWork是一个很简单的东西,它有两个值_callbacks和_didCommit。通过执行then函数传入callback,如果判断到当前的_didCommit为false的情况下,就将callback添加到_callbacks数组内。然后通过执行_onCommit去改变_didCommit的值,之后循环执行_callbacks中的callback。
updateContainer
render函数之后会执行updateContainer函数,传入children,root和work实例化后的_onCommit函数。因为这个render其实是root根对象上的render,所以children就是App(当然也可以是其他,视乎你执行ReactDOM.render时传入的第一个参数是什么)。
在updateContainer中会通过requestCurrentTime和computeExpirationForFiber得出currentTime和expirationTime这个两个时间之后传入到updateContainerAtExpirationTime中,之后再传入到scheduleRootUpdate中。
scheduleRootUpdate会将expirationTime传入一个createUpdate函数中创建一个update对象。并且将element赋值到update.payload中(element就是App的ReactElement),并且将callback赋值到update.callback中。
接着会执行enqueueUpdate函数,这个函数其实大概的意思就是将新建的update对象和当前的FiberNode对象传入,然后为current$$1这个对象添加了updateQueue对象,里面保存着相关的一些任务。以下是返回结果:
之后就执行scheduleWork函数。曾经断点开过这个函数执行完之后,页面就会渲染出dom节点了并且回调函数也执行了。无比兴奋。
其实到源码看到这里发现很多问题,例如react很喜欢用全局变量,而且里面发现其实为了之后的异步渲染做了不少准备的,很多的判断代码。开始有点怀疑是不是应该读16.7.0版本的代码,但是已经开始了,那就继续吧。
总结
整个流程是比较复杂,中间很多对象之间的引用,又实例一些对象,如果单看上面的流程比较懵逼的话,没有关系,我在这里梳理一下整个流程,最终传入scheduleWork前的参数是怎么生成出来的,原来的container和children去哪里了呢?我们通过一个流程图去说明整个流程是怎样的。
下一篇继续说如何渲染到真实DOM当中的。