‘壹’ React 源码探源 1 Mount
React 在上一周(2021年6月8日) 出了 18 beta ,作为一个资深 React 玩家,如果未能通读过 React 源码,出门都不好意思跟人打招呼。本系列是笔者通读 React 17 源码的一个尝试。
本系列并不是 React 官方作者的行为,有些结论是处于笔者自己的理解,如有不当或者遗漏请各位老师和同学指正。
本系列尽量在完整的主题上切分主题。然而因为 React 源码的复杂度,这几无可能。如果未能做到完整的切分,本文应尽量做到各个文章之间的无关性。
从头自己来读 React 的源码是一件很有挑战的事情,而且有可能误入歧途。本文并不是从头开始自己来,主要参考了 Youtube 的 React Source Code Walkthrough 。 需要的同学请自行带梯子。
本文所有的源码都参考于 react repo , 使用的版本是 17.0.3 , Commit 是
如要探寻 React 源码,需要本地编译启动 React 源码,请参考 React 官方文档
一般来讲,开发环境都是完备的,笔者只是安装了 JDK 。
在开始之前,需要保证
首先我们打开 fixtures/packaging/babel-standalone/dev.html ,可以看到页面的渲染结果:
一般来讲,React 将整个 Mount 过程分解为 Render 和 Commit 过程,Render 的过程主要在 workLoopSync 完成,Commit 过程主要在 commitRoot 中完成。
实际上观察调用栈可以发现,在 Render 之前, React 还有调用了 ,这个过程目前发现的主要工作是给 ReactDOM 挂载点初始化事件监听,就是在 listenToAllSupportedEvents 中完成的工作。
本阶段主要是创建 fiber tree,以及 fiber tree 对应的 DOM Tree,每个 fiber 对应于一个 work,对应一个 React 节点。
渲染的 fiber tree 可以在 ReactDOM 的 DOM 的根结点查看到:
本系列后续会通过更多的例子来探究 Fiber 树的结构,现在可以先说一下现在笔者发现的一些结论。
Fiber 树中使用 child 表述子节点,通过 return 表述父节点,通过 sibling 表述兄弟节点。通过 stateNode 表述关联的 DOM 节点。
Fiber 树实际上有两个,相互之间通过 alternate 连接。在 commit 之前,fiber 树根的 current 下面还没有子节点,当整体渲染结束以后,fiber 树根的 current 会指向当前的 current.alternate .
Commit 阶段会将现在的 fiber tree 渲染到 DOM 树中,并且执行组件中可能的 effect。本系列会用更多的例子来探究 Commit 的过程。
‘贰’ create-react-app build 打包隐藏源码
在使用 create-react-app 时,打包生产环境 npm run build ,浏览器打开后仍然是可以看到源码的。
在这里以新建一个默认项目为例:
项目根目录新建 .env.proction 文件,内容如下:
然后重新打包,浏览器打开后就看不到源码啦。
为了探究原理,执行 eject 后,可以看到webpack配置中有这么一段
这里的 process.env.GENERATE_SOURCEMAP 控制着是否捎带源码。所以我们可以配置环境变量 GENERATE_SOURCEMAP=false 即可。
当执行 build 时,将按顺序优先寻找 .env.proction.local , .env.proction , .env.local , .env 文件来配置环境变量,所以就有了上面的操作。
更多关于环境变量的信息可查看 Adding Custom Environment Variables 。
‘叁’ 【原创】react-源码解析 - forward-ref&context(4)
通常我们会通过ref去获取Dom节点的实例,或者ClassComponent的实例,但是,如果我么们的组件是一个function类型的component,由于functionComponent是没有实例的所以我们在使用的时候也相应的取不到改组件的this,当然ref也一样。这时react为我们提供了一个forwardRef方法:
通过这种方式创建的函数类型的组件,使我们能够在函数中继续使用ref属性,当然我们在实际的应用的中也不会傻到去取函数类型组件的ref,因为我们知道它是没有实例的。但是,当我们在使用其他库提供的组件的时候,我们可能并不知道这个这个组件的类型,这时如果能够合理的使用这个方法将会为我们省去不必要的麻烦,同时这里也有HOC的思想在里面。接收一个组件,返回对原组件进行包装的新的组件。接下来我们去看看方法的源码: forwardRef 源码很简单,改方法返回了一个Oject具有render属性,同时$$typeof为"react.forward_ref"的Symbol值。
这里可能存在对于type属性的概念混淆。我们一定不能认为使用forward创建出的组件的$$typeof属性为:'react.forward_ref'。我们使用forwardRef创建的组建的额时候,实际是将上面例子中的TargetCom作为参数传入到CreateElement方法中的,实际返回的element中的$$typeof还是REACT_ELEMENT_TYPE, 也就是说这里我们将TargetCom{创建出的对象--具有render和$$typeof属性}传入,其中CreateElement的type属性为forward方法返回的那个对象,也就是说在type对象里面有个叫做$$typeof的属性这个属性的键值为:'react.forward_ref',
在后安的渲染过程中有很多判断,其中有一些就是更具$$typeof展开的,这里我们一定要搞清楚凡是通过CreateElement创建的组件的$$typeof属性都为: 'REACT_ELEMENT_TYPE'。
这里我们还是按照惯例对api进行一下简单的说明,我们知道在react中是通过props属性来实现组件间通信的,这种通信方式存在的问题在于,虽然父子组件之间通信很方便但是当我们的组件嵌套层级很深,这时候如果使用props传参就不太现实了,首先中间层的组件不一定是你自己写的其次中间层组件声明的props对于这些组件本身没有任何意义,这个时候我们就需要使用context方法帮助我们实现多级组件之间的通信。我们在顶层组件中提供了context对象之后,所有的后代组件都可以访问这个对象。以此达到跨越多层组件传递参数的功能。在react当前版本中有两种实现context的方式:
(1)ParentComponent.childContextTypes{} == {不推荐,下个大版本会废弃}
(2)const { Provider, Consumer } = React.createContext('default');
在使用childContextTypes时候我们需要在父级组件中声明一个getChildContext的方法,该方法返回一个对象,这个对象就是我们需要传给后代组件的context对象。当我们在使用第一种方法的时候我们需要在组件上声明context对象中属性的类型,有些类似于react的PropTypes类型检测。同时需要在使用到context的后代组件中声明contextTypes类似于下面这种写法:
如果不这样声明的话,在后代组价中是取不到context对象的。这里我们需要注意的是我们在子组件中使用context的时候,需要哪个属性就必须去contextTypes中声明,因为改组件的上级组件不止一个上级组件中的context也不止一个。而createContext方法的使用就简化了很多,首先我们看到改方法返回两个对象Provider, Consumer分别为context的提供方和订阅方:
在上层组件中声明之后,在想用到context的后代组件中国使用Consumer包括起来就可以访问到之前声明的context: ReactContext
从源码中我们可以看到CreateContext方法创建了一个对象改对象有一个_currenValue属性记录context的变化,这个对象Provider属性中声明context,然后使改对象的Consumer属性指向对象本身,我们在使用Consumer的时候就直接从context的currenValue上去取值。以上就是react中的Createcontext方法的实现原理,当然实际过程并没有这么简单,至于具体的实现我们接着往下看。同时这里我们也需要注意该对象下的$$typeof属性并不是用来替换ReactElement中的$$typeof, 与我们之前将到的forwardRef中声明的$$typeof一样都只是我们传入CreateElement方法中type属性上的内容。
了解更多: react-source-code
‘肆’ 深入理解React16之:(一).Fiber架构
React16虽然出了一阵子了。刚出来的时候,粗略看了一遍更新文档。以为没什么大的改动,也听说项目从react15-16的升级过度可以很平滑,再加上项目改版上线一直比较频繁,所以一直还用的15.6的版本。
偶然在知乎看到@程墨Morgan大神的live,便抱着好奇心和学习的心态报名了,受益良多。
我理解的Fiber架构:
在我之前的一篇文章有简单介绍, 阅读react源码--记录:1.1 问题记录
下面从一个具体实例理解一下,再加上我画了图,应该很好理解啦~(图画的有点渣)
假如有A,B,C,D组件,层级结构为:
我们知道组件的生命周期为:
挂载阶段:
那么在挂载阶段,A,B,C,D的生命周期渲染顺序是如何的呢?
以render()函数为分界线。从顶层组件开始,一直往下,直至最底层子组件。然后再往上。
组件update阶段同理。
————————
前面是react16以前的组建渲染方式。这就存在一个问题,
好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,然后他再上岸。在这期间, 岸上发生的任何事,都不能对他进行干扰,如果有更重要的事情需要他去做(如用户操作),也必须得等他上岸
看一下fiber架构 组建的渲染顺序
潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。
加入fiber的react将组件更新分为两个时期
这两个时期以render为分界,
phase1的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。此过程,React 在 workingProgressTree (并不是真实的virtualDomTree)上复用 current 上的 Fiber 数据结构来一步地(通过requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中。
phase2的生命周期是不可被打断的,React 将其所有的变更一次性更新到DOM上。
这里最重要的是phase1这是时期所做的事。因此我们需要具体了解phase1的机制。
这样的话,就和react16版本之前有很大区别了,因为可能会被执行多次,那么我们最好就得保证phase1的生命周期 每一次执行的结果都是一样的 ,否则就会有问题,因此, 最好都是纯函数。
对了,程墨大神还提到一个问题,饥饿问题,即如果高优先级的任务一直存在,那么低优先级的任务则永远无法进行,组件永远无法继续渲染。这个问题facebook目前好像还没解决,但以后会解决~
所以,facebook在react16增加fiber结构,其实并不是为了减少组件的渲染时间,事实上也并不会减少,最重要的是现在可以使得一些更高优先级的任务,如用户的操作能够优先执行,提高用户的体验,至少用户不会感觉到卡顿~
‘伍’ React 源码(三)使用本地依赖库
在 React 应用中依赖基本上是通过 yarn 或者 npm 进行安装的,但是在看源码的过程中,有的时候想要去调试,或者说打印一些数据,如果可以在本地的 React 应用里面依赖本地的 React 仓库,那么就可以进行上述的操作了。
在 React 官方文档中的开发流程 里面介绍了如何使用本地依赖库。
在启动本地 React 项目的时候出现了以下报错
在将 react-jsx-dev-runtime.development.js 文件复制到 build/node_moles/react/cjs 目录下即可。