小前端读源码 - React16.7.0(二)

2022-09-26 10:37:19 浏览数 (1)

上一篇文章说到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一一渲染的函数了,最终将组件对象返回的一系列操作。

  1. legacyCreateRootFromDOMContainer
  2. unbatchedUpdates
  3. render / legacy_renderSubtreeIntoContainer

legacyCreateRootFromDOMContainer

如果当前传入的container并不是一个已经初始化的容器,那么将会执行legacyCreateRootFromDOMContainer这个函数,那么从源码也看到了,在执行完legacyCreateRootFromDOMContainer之后其实就会将返回的值赋值到container中的_reactRootContainer了。如果之后还对同一个container进行render的话,就会判断到存在_reactRootContainer这一个对象,那么就会进入这个判断中了。那么legacyCreateRootFromDOMContainer到底帮我们做了什么事情呢?我们先看看它返回了什么东西回来。

好吧,我们并不知道里面的是什么,那么只能看看legacyCreateRootFromDOMContainer函数里面执行了什么东西了。

首先react需要判断你是不是服务器渲染,其实早在ReactDOM对象内到legacyCreateRootFromDOMContainer之间有很多关于服务器渲染的判断,但是我们现在目标是先搞懂react在浏览器渲染的流程和逻辑,所以我们会先跳过一些服务器渲染的流程和逻辑。那么legacyCreateRootFromDOMContainer一开始就会通过传入的forceHydrateshouldHydrateDueToLegacyHeuristic去判断是不是服务器渲染,那么结果当然是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中。执行的函数顺序如下:

  1. createContainer
  2. 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当中的。

0 人点赞