-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
剖析 React 源码:先热个身 #18
Comments
<div>
<span>1</span>
<span>2</span>
</div> c => [[c, c]] 的转换结果 好像不是 <span>1</span>
<span>1</span>
<span>2</span>
<span>2</span> 吧 ,c => [[c, c]] 并没有把包裹的div去掉 <div>
<span>1</span>
<span>2</span>
</div>
<div>
<span>1</span>
<span>2</span>
</div> |
|
function App() {
return (
<Test>
<span>1</span>
<span>2</span>
</Test>
);
}
function Test (props){
return(
<div>{ React.Children.map(props.children,c => [[c, c]]) }</div>
)
} 您指的应该是这样么 |
恩 |
@对这个源码中的对象池的疑惑:@KieSun
|
想问一下大佬,react中ReactChildren的设计目的是什么,因为平时基本没有用到这个方法,可以解释一下不 |
@HerryLo 提供一些便捷的 API 让你去操作 |
@HerryLo 因为props.children可能是任何格式,直接操作会很容易出现错误,所以使用React.Children的API会更加轻松的对React.Children进行操作。推荐阅读官方的文档,里面有对API的使用的介绍React.Children |
@Eward-Wang 对象池的目的应该是为了防止内存抖动,不断创建新的对象又不复用,会不断导致gc的发生,应该从程序内存的使用角度来比较俩种方法 |
请教一下 如何在vscode中断掉调试react代码 我用jest启动debug后 断点打在ReactDOM-test的一个ReactDOM.render方法上 发现跟reconciler相关的代码无法调试(断点位置不正确)。使用官方推荐的调试策略(chrome inspect)发现是因为相关代码并不是源代码。 |
@KieSun 你好,请问 export default function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
if (__DEV__) {}
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
} |
请问一下,您画流程图的软件是啥? |
Sketch |
我觉得用一段很枯燥的文字,加上demo,加上感想这样的阅读源码才是最好的,这样更让人接受理解,不然很多人看了文章就说自己懂了原理,其实一知半解,弄出笑话。毕竟我以前这样干过! |
这是我的 React 源码解读课的第一篇文章,首先来说说为啥要写这个系列文章:
这个系列文章预计篇数会超过十篇,React 版本为 16.8.6,以下是本系列文章你必须需要注意的地方:
这篇文章内容不会很难,先给大家热个身,请大家打开 我的代码 并定位到 react 文件夹下的 src,这个文件夹也就是 React 的入口文件夹了。
文章相关资料
React.createElement
大家在写 React 代码的时候肯定写过 JSX,但是为什么一旦使用 JSX 就必须引入 React 呢?
这是因为我们的 JSX 代码会被 Babel 编译为
React.createElement
,不引入 React 的话就不能使用React.createElement
了。那么我们就先定位到 ReactElement.js 文件阅读下
createElement
函数的实现首先
createElement
函数接收三个参数,具体代表着什么相信大家可以通过上面 JSX 编译出来的东西自行理解。然后是对于
config
的一些处理:这段代码对
ref
以及key
做了个验证(对于这种代码就无须阅读内部实现,通过函数名就可以了解它想做的事情),然后遍历config
并把内建的几个属性(比如ref
和key
)剔除后丢到 props 对象中。接下里是一段对于
children
的操作首先把第二个参数之后的参数取出来,然后判断长度是否大于一。大于一的话就代表有多个
children
,这时候props.children
会是一个数组,否则的话只是一个对象。因此我们需要注意在对props.children
进行遍历的时候要注意它是否是数组,当然你也可以利用React.Children
中的 API,下文中也会对React.Children
中的 API 进行讲解。最后就是返回了一个
ReactElement
对象内部代码很简单,核心就是通过
$$typeof
来帮助我们识别这是一个ReactElement
,后面我们可以看到很多这样类似的类型。另外我们需要注意一点的是:通过 JSX写的<APP />
代表着ReactElement
,APP
代表着 React Component。以下是这一小节的流程图内容:
ReactBaseClasses
上文中讲到了
APP
代表着 React Component,那么这一小节我们就来阅读组件相关也就是 ReactBaseClasses.js 文件下的代码。其实在阅读这部分源码之前,我以为代码会很复杂,可能包含了很多组件内的逻辑,结果内部代码相当简单。这是因为 React 团队将复杂的逻辑全部丢在了 react-dom 文件夹中,你可以把 react-dom 看成是 React 和 UI 之间的胶水层,这层胶水可以兼容很多平台,比如 Web、RN、SSR 等等。
该文件包含两个基本组件,分别为
Component
及PureComponent
,我们先来阅读Component
这部分的代码。构造函数
Component
中需要注意的两点分别是refs
和updater
,前者会在下文中专门介绍,后者是组件中相当重要的一个属性,我们可以发现setState
和forceUpdate
都是调用了updater
中的方法,但是updater
是 react-dom 中的内容,我们会在之后的文章中学习到这部分的内容。另外
ReactNoopUpdateQueue
也有一个单独的文件,但是内部的代码看不看都无所谓,因为都是用于报警告的。接下来我们来阅读
PureComponent
中的代码,其实这部分的代码基本与Component
一致PureComponent
继承自Component
,继承方法使用了很典型的寄生组合式。另外这两部分代码你可以发现每个组件都有一个
isXXXX
属性用来标志自身属于什么组件。以上就是这部分的代码,接下来的一小节我们将会学习到
refs
的一部分内容。Refs
refs 其实有好几种方式可以创建:
ref={el => this.el = el}
React.createRef
这一小节我们来学习
React.createRef
相关的内容,其余的两种方式不在这篇文章的讨论范围之内,请先定位到 ReactCreateRef.js 文件。内部实现很简单,如果我们想使用
ref
,只需要取出其中的current
对象即可。另外对于函数组件来说,是不能使用
ref
的,如果你不知道原因的话可以直接阅读 文档。当然在之前也是有取巧的方式的,就是通过
props
的方式传递ref
,但是现在我们有了新的方式forwardRef
去解决这个问题。具体代码见 forwardRef.js 文件,同样内部代码还是很简单
这部分代码最重要的就是我们可以在参数中获得
ref
了,因此我们如果想在函数组件中使用ref
的话就可以把代码写成这样:ReactChildren
这一小节会是这篇文章中最复杂的一部分,可能需要自己写个 Demo 并且 Debug 一下才能真正理解源码为什么要这样实现。
首先大家需要定位到 ReactChildren.js 文件,这部分代码中我只会介绍关于
mapChildren
函数相关的内容,因为这部分代码基本就贯穿了整个文件了。如果你没有使用过这个 API,可以先自行阅读 文档。
对于
mapChildren
这个函数来说,通常会使用在组合组件设计模式上。如果你不清楚什么是组合组件的话,可以看下 Ant-design,它内部大量使用了这种设计模式,比如说Radio.Group
、Radio.Button
,另外这里也有篇 文档 介绍了这种设计模式。我们先来看下这个函数的一些神奇用法
对于上述代码,
map
也就是mapChildren
函数来说返回值是[c, c, c, c]
。不管你第二个参数的函数返回值是几维嵌套数组,map
函数都能帮你摊平到一维数组,并且每次遍历后返回的数组中的元素个数代表了同一个节点需要复制几次。如果文字描述有点难懂的话,就来看代码吧:
对于上述代码来说,通过
c => [[c, c]]
转换以后就变成了接下里我们进入正题,来看看
mapChildren
内部到底是如何实现的。这段代码有意思的部分是引入了对象重用池的概念,分别对应
getPooledTraverseContext
和releaseTraverseContext
中的代码。当然这个概念的用处其实很简单,就是维护一个大小固定的对象重用池,每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空然后丢回池子。维护这个池子的用意就是提高性能,毕竟频繁创建销毁一个有很多属性的对象会消耗性能。接下来我们来学习
traverseAllChildrenImpl
中的代码,这部分的代码需要分为两块来讲这部分的代码相对来说简单点,主体就是在判断
children
的类型是什么。如果是可以渲染的节点的话,就直接调用callback
,另外你还可以发现在判断的过程中,代码中有使用到$$typeof
去判断的流程。这里的callback
指的是mapSingleChildIntoContext
函数,这部分的内容会在下文中说到。这部分的代码首先会判断
children
是否为数组。如果为数组的话,就遍历数组并把其中的每个元素都递归调用traverseAllChildrenImpl
,也就是说必须是单个可渲染节点才可以执行上半部分代码中的callback
。如果不是数组的话,就看看
children
是否可以支持迭代,原理就是通过obj[Symbol.iterator]
的方式去取迭代器,返回值如果是个函数的话就代表支持迭代,然后逻辑就和之前的一样了。讲完了
traverseAllChildrenImpl
函数,我们最后再来阅读下mapSingleChildIntoContext
函数中的实现。bookKeeping
就是我们从对象池子里取出来的东西,然后调用func
并且传入节点(此时这个节点肯定是单个节点),此时的func
代表着React.mapChildren
中的第二个参数。接下来就是判断返回值类型的过程:如果是数组的话,还是回归之前的代码逻辑,注意这里传入的
func
是c => c
,因为要保证最终结果是被摊平的;如果不是数组的话,判断返回值是否是一个有效的 Element,验证通过的话就 clone 一份并且替换掉key
,最后把返回值放入result
中,result
其实也就是mapChildren
的返回值。至此,
mapChildren
函数相关的内容已经解析完毕,还不怎么清楚的同学可以通过以下的流程图再复习一遍。其余内容
前面几小节的内容已经把 react 文件夹下大部分有意思的代码都讲完了,其他就剩余了一些边边角角的内容。比如
memo
、context
、hooks
、lazy
,这部分代码有兴趣的可以直接自行阅读,反正内容都还是很简单的,难的部分都在 react-dom 文件夹中。其他文章列表
最后
阅读源码是一个很枯燥的过程,但是收益也是巨大的。如果你在阅读的过程中有任何的问题,都欢迎你在评论区与我交流,当然你也可以在仓库中提 Issus。
另外写这系列是个很耗时的工程,需要维护代码注释,还得把文章写得尽量让读者看懂,最后还得配上画图,如果你觉得文章看着还行,就请不要吝啬你的点赞。
下一篇文章就会是 Fiber 相关的内容,并且会分成几篇文章来讲解。
最后,觉得内容有帮助可以关注下我的公众号 「前端真好玩」咯,会有很多好东西等着你。
The text was updated successfully, but these errors were encountered: