「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」
写完前一天的文章后,我意识到我的文章对 react 代码的描述非常的散乱,造成这结果的原因有很多方面,一方面是由于我在 react 方面还处在学习ing的阶段,在很多代码仍未理解透彻的前提上就开始更文了,太过匆忙;另一方面则是我的语言组织能力和结构化思维确实不怎么样,很难表达清楚我对代码的理解。为了提高后面 react 系列文章的质量,本文将专门梳理一下 react 源码学习的策略,制定后面更文的规划。
学习 react 源码的前提
首先需要学习源码前阅读过 react 的官网文档,对其有基本的了解。React 文档提供了中文支持,就暂时不要为难自己看非母语的文档了。注意我们需要看的是“文档页”而不是“教程页”,官网的教程是以一个井字棋游戏作为示例展示用法,不如文档页描述得全面。
对 react 的用法熟悉后就可以尝试去看源码了,react 仓库由 facebook 团队维护,如果访问github 仓库的速度不理想,可以使用 gitee 的为 react 同步的镜像仓库,每日会与在 github 上的原仓库同步一次。
源码阅读顺序
阅读顺序可以使用自顶向下法——阅读入口函数相关的代码,通过不断跳转到函数定义的方式去理解其他部分的代码,理解整个框架的代码执行链路。
react 被分成了很多个模块,放在了仓库的package文件夹下,初学者如我一开始往往不知道如何从何看起。事实上, 在package目录下的每个一级文件夹的名字就是这个模块的包名。回想一下我们为了使用 react,一步步导入了哪些包,就能知道应该从哪看起了。
顺序梳理思路
我们使用 react 往往要在一开始就编写将根组件挂载到真实 DOM 节点上的代码, 如下所示:
var React = require('react');
var ReactDOM = require('react-dom');
function MyComponent() {
return <div>Hello World</div>;
}
ReactDOM.render(<MyComponent />, node);
ReactDOM.render 可以说是整个 react 应用的入口,那么我们第一步可以先找到 'react-dom'模块。从这里开始,分析执行链路(关注参数的传递和函数跳转,暂时不要理解具体的逻辑):
- 找到 render 函数的定义
- 分析用户编写的组件函数在 render 函数中被传递到了哪些地方
packages/react-dom/index.js 暴露 render 方法
packages/react-dom/src/client/ReactDOMLegacy.js 定义render 方法
render 的第一个参数为 element
element 传到了 legacyRenderSubtreeIntoContainer,参数名为 children
children 传入了 updateContainer,这来自另一个模块 react-reconciler,路径为 packages/react-reconciler/src/ReactFiberReconciler.js
children 传入 updateContainer 对应的参数名为 element
element 赋值给了 update.payload
update 对象传入了 enqueueUpdate,update被分配给了sharedQueue.interleaved
sharedQueue 来自enqueueUpdate函数的另一个入参 fiber
fiber对应 legacyRenderSubtreeIntoContainer 中的 container._reactRootContainer,初始化container._reactRootContainer 调了 legacyCreateRootFromDOMContainer 函数
legacyCreateRootFromDOMContainer 调了createContainer函数,这来自react-reconciler/src/ReactFiberReconciler
createContainer 调了 createFiberRoot
createFiberRoot 构建 root 调了 createHostRootFiber
createHostRootFiber 调了 createFiber,返回 new FiberNode,这是一个满足Fiber接口的对象
- 分析
sharedQueue的链路
sharedQueue 被传入了 pushInterleavedQueue,该函数存在闭包变量 interleavedQueues
同样引用了 interleavedQueues 的函数还有 enqueueInterleavedUpdates
enqueueInterleavedUpdates 在 packages\react-reconciler\src\ReactFiberWorkLoop.new.js 的prepareFreshStack函数被调用
反推被调用的链路:prepareFreshStack <- performConcurrentWorkOnRoot <- ensureRootIsScheduled <- scheduleUpdateOnFiber <- updateDehydratedSuspenseComponent <- updateSuspenseComponent <- attemptEarlyBailoutIfNoScheduledUpdate <- beginWork
beginWork函数中当 workInProgress.tag为IndeterminateComponent时,执行mountIndeterminateComponent函数
mountIndeterminateComponent函数会执行 renderWithHooks
renderWithHooks会执行用户的组件函数,包括里面的hook函数
- 找出 2 和 3 的交集
回到enqueueUpdate所在的updateContainer函数,里面会执行scheduleUpdateOnFiber,后者会调用 ensureRootIsScheduled,进入3的链路
源码学习 tips
- 跳过
__DEV__相关代码。仅在开发环境下有效的代码往往不是框架的核心逻辑,但又在整体代码中占了一定的篇幅,为了减少心智负担,保留看核心代码的精力,把这部分代码折叠起来或注释掉,不要尝试理解它; - 给关键部分添加母语注释。 关键部分指的是你分析组件函数从render函数中传入到结果被渲染的整个链路经过的每个函数,用注释标注一下这些关键节点。
- 先规划好函数的阅读顺序再阅读具体实现细节。就像上文梳理源码阅读顺序一样,自顶向下找到依次被执行的函数,先记录好函数名和顺序,后面再安排时间,规定第几天看哪一部分。
- 将框架运行起来。可选,如果能将框架运行起来,在需要的位置打
console.log观察对代码理解是很有帮助的。但由于网络环境的原因,我安装依赖出现了问题,所以这一步我没能跑起来。以免在折腾环境这件事上花费时间过多,我就暂时不考虑运行调试了 - 跳过已过时代码。查看 react 代码仓库,可以看到有些文件名字相似只有后缀不同,一个是
.old.js为后缀,另一个是.new.js为后缀,显然前者是历史遗留代码,将逐步被后者替代,后者才是未来,我们目前没必要关注已过时的代码,所以跳过对这些旧代码的阅读吧。