React Mount-- renderRootSync
学习源码不易,和大家一起共勉!
前置知识:理解JSX
原文链接:react.iamkasong.com/preparation…
jsx
是ECMAScript
的语法拓展,我们可以用jsx语法
来更加灵活,自如的表达UI组件
,jsx
最终都会被编译器转译成为ECMAScript
。
图1:Route组件
图二:Provider组件,业务组件
看到上图大家一定非常熟悉,这就是我们平时写的react
组件,所以我对jsx
的另一个理解就是组件化,无论是Route
路由组件标签,Routes
路由组件标签,Context.Provider
React上下文组件标签,还是业务组件标签
,jsx都可以使之嵌套,或者自由组合,还可以被编译器转译成结构化的vdom
,并且在@babel/plugin-transform-react-jsx中jsx组件的值会被当作type
使用。这样一来,我们就可以针对每一种组件标签,都写一套挂载逻辑和更新逻辑就可以实现jsx组件
化。
前置知识:如何调试 React 源码
原文链接:react.iamkasong.com/preparation…
首先git clone
拉取React
源代码 ->yarn
安装依赖 ->yarn build
构建 react / react-dom / scheduler
的cjs
包。yarn link
创建这个三个包的软链接,npx create react app
创建新项目。将新项目的react react-dom scheduler
(yarn link react/react-dom)
到build
好的cjs
包 ,npm start
启动项目,debugger
或者打断点,即可以开始调试cjs
文件。
在我们添加软链接的包(link)中调试Hello React
图三:在打包好的cjs包当中console.log()
运行已经添加了link软链的新项目,控制台打印出Hello React
图四:控制台打印出`Hello React`
React Mount
renderRootSync
及其涵盖的子调用就是React 初次Mount
的核心过程。
图五:React初次Mount的函数调用栈
首先一上来`mount`的核心操作就是创建`FiberRootNode`节点,设置`current`属性,为初次挂载打好基础,因为初次挂载`其他fiber`节点都没有`current属性`。 > current属性这里先要有印象,下文还会提到。那么,创建好FiberRootNode
节点的结果是这样的:
图六:创建好`FiberRootNode`节点的结果
图七:创建好`FiberRootNode`节点的结果
然后:开始核心beginwork
,下面是bedginwork
及其核心操作的调用栈:
beginwork
开始
图八: bedginwork及其核心操作的调用栈
解释:beginwork
拿到当前的Fiber
节点,会去判断是什么类型的组件标签,是元素标签?(updateHostComponent),函数组件(updateFunctionComponent)? 类组件(updateClassComponent)?还是memo
组件,context
组件,对应好后去执行不同的update
逻辑,之后拿到fiber.pendingProps.chidlren
,也就是当前fiber
节点的子虚拟dom
,带着子虚拟dom
去执行reconcileChildren
,reconcileChildren
主要是通过判断不同子虚拟dom的type
,去创建子节点fiber
,如果是children
是数组vdom还会构建出这一层fiber节点
的child 和 sibling 以及return
的关系,最后返回这一层的child
节点,让child
作为下一个beginwork
的节点。
图九:判断是什么类型的组件标签
图十:reconcileChildren主要是通过判断不同子虚拟dom的`type`,去创建子节点`fiber`
beginwork
的结果与总结
实例代码
图十一:beginwork的结果
图十二:beginwork的结果
**总结**:`beginwork`在`mount`阶段,最重要的作用就是带着当前的`fiber`节点和子虚拟dom执行`reconcileChildren`,每次`reconcile`都会利用`子虚拟dom`来构建好这一层的`fiber链表`(包括`fiber节点之间的child, sibling, return关系`)。completeWork
开始
当深度优先遍历到一个没有child
的fiber节点时(这里是img),开始completework
进行回溯,completework
主要做了三部分工作:第一部分是:当前的fiber
节点调用createInstance
来创建真实dom,创建好真实dom之后,挂载到fiber.stateNode属性上, 第二部分是:调用appendAllChildren()去按child -> sibling的顺序去append这一层子节点的dom,第三部分是:更新该fiber节点真实dom上的属性,事件,样式,ref,比如:上述的实例中,img
没有child
,不会append
,直接返回,div.app
有child
,会依次从div.app
父fiber的child到sibling
将子节点的真实dom去append到父节点上。
(注意
completework
是回溯,回溯到父节点的时候,子节点已经将子节点自身真实dom
挂载到fiber
节点上去了,父节点可以直接使用)所以回溯到div.app
, div.appappendAllChildren
()之后,我们打出Instance
,也就是div.app
的真实dom我们会发现,当回溯到div.app
父节点的时候,所有的子节点都已经被append
进去了)。
图十三:打印出Instance
我们的子dom节点虽然已经建立好了联系,但是还没有渲染在web页面上,我们都知道在react当中,dom最终渲染在页面上是在commit mutation
阶段,在commit
阶段通过effectTag
属性 追踪fiber
副作用来对dom节点进行增删改移,不一样的是,初次节点的挂载逻辑就只在根节点的第一个child(<App/> --> function App)
上去打effectTag
,因为其他节点,初次挂载没有current属性
(上文有提到),所以当根fiber节点
和非根节点
带着他的childrenvdom
去reconceileChildren
的时候,走了不一样的reconcile
调用,所以只有根fiber节点的child子fiber节点
才打上了effectTag
。而其他节点没有,不过,这样也完全够用,因为初次挂载,每个节点都需要placement
,所以commit mutation
阶段的时候,直接将函数组件返回的第一个dom节点(div.app -> 函数fiber节点.child.stateNode属性) 插入到根节点(div#root -> FiberRootNode.containerInfo)就mount成功了。
图十四:根fiber节点和非根节点带着他的childrenvdom去reconceileChildren的时候,走了不一样的reconcile调用