react render环节分析

render微观细节

start

这里从Component开始分析,因为我们一般使用 react时候最基础的用法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// jsx file 
class HelloMessage extends React.Component {
render() {
return (
      <div>
Hello Wolrd
</div>
);
}
}
// main.js
ReactDOM.render(
<HelloMessage name="Taylor" />,
mountNode
)

所以这里暂时以这个为目标,看看react最终是怎样实现这个jsx到js再到html上显示出HelloWorld的

当代码开始处理时候首先时通过 babel的预处理,这段代码关键的地方被转译后是这样

1
2
3
4
ReactDOM.render(
React.createElement(HelloMessage, { name: Taylor }, ""),
mountNode
)

不过值得一提是createElement实质上并不会直接处理

如果HelloMessage里面还有更多元素,那么第三个参数依旧用React.createElement来遍历子节点创建元素直到没有更多子元素了——这就是为什么 jsx里面render的元素必须用一个元素包裹起来的原因。

举例来说,就像下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class HelloMessage extends React.Component { 
render() {
return (
      <div>
<span>
<span>Helloword</span>
</span>
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
mountNode
)
// 被转换为:
ReactDOM.render(
React.createElement(
HelloMessage,
{ name: Taylor },
React.createElement(
"div",
null,
React.createElement(
"span",
null,
React.createElement(
"span",
null,
"Helloword"
)
)
)
), mountNode
)

这里暂时跳过React.createElement执行过程,先了解一下React.render是如何将react这些渲染到HTML上的。

在此之前,我们先在ReactDomComponent-test.js line 42打一个断点,然后在webstorm里点调试,接下来前往src/renderers/dom/ReactDOM.js目录将光标定位到568行。点击 图标,程序将会运行到这一行并断点,这样我们可以看到createElement函数最终返回的结构。它是这样的

Render的流程

1
2
3
4
5
6
7
ReactMount.render
-> ReactMount._renderSubtreeIntoContainer
-> ReactMount._renderNewRootComponent
-> ReactUpdates.batchedUpdates
-> ReactUpdates.batchedMountComponentIntoNode
-> mountComponentIntoNode(ReactMount.js中)
-> ReactMount._mountImageIntoNode

ReactMount._renderSubtreeIntoContainer

_renderSubtreeIntoContainer做了哪些事情呢?

  • 参数校验:
    • setState异步回调是否合法
    • 目标渲染的元素是否是createElement返回的合法结构
    • 是否传入了根节点容器
  • 使用TopLevelWrapper包装目标渲染元素 && 设定Context预备传入
  • 是否是更新环节 当前例子这里设置的null 如果更新环节需要保留一些状态传入
  • 将ReactElement、根节点容器、重用标记(shouldReuseMarkup)、Context传入ReactMount._renderNewRootComponent进行渲染

ReactMount._renderNewRootComponent

它做了这些:

  • 参数校验
    • ReactCurrentOwner校验
    • 容器合法性校验
  • 记录scroll值
  • 生成最终会被挂载上去的ReactNode(框架内私有表现形式,componentInstance)
  • 调用ReactUpdates.batchedUpdates进行更新

ReactUpdates.batchedUpdates

它做了这些:

  • 确认事务模型就绪 && batchingStrategy就绪
  • 调用batchingStrategy.batchedUpdates进行下一步
  • 根据isBatchingUpdates进行调度处理
    • 如果为true 那么仅执行传入的callback
    • 如果为false 使用transaction.perform对callback进行调用(这里有前置和收尾处理 这是事务的特点)
  • 不管如何callback会被执行(这里callback是batchedMountComponentIntoNode,定义在ReactMount.js中)

batchedMountComponentIntoNode

它做了:

  • 获取ReactReconcileTransaction事务实例
  • 使用该事务实例调用mountComponentIntoNode来进行下一步更新

mountComponentIntoNode

这个函数的作用是挂载组件并将其渲染到DOM

它做了:

  • 调用ReactReconciler.mountComponent生成markup标记内部含有{children: markup数组, node:HTMLElement}。此时mountComponent实质是ReactDomComponent.mountComponent。可以参考ReactDomComponent篇看看这个函数做了什么。
  • 调用ReactMount._mountImageIntoNode渲染markup(markup.node是HTML元素可以直接插入DOM)

ReactMount._mountImageIntoNode

这个函数是最终的函数 它没有返回值,只是将相关处理好的结果插入DOM中

在这个例子时里面因为没有旧的元素而是全部新生成所以进入了

useCreateElement分支,这个分支中将会删除所有的

container子元素,并执行DOMLazyTree.insertTreeBefore

1
2
3
4
5
6
if (transaction.useCreateElement) {
while (container.lastChild) {
container.removeChild(container.lastChild);
}
DOMLazyTree.insertTreeBefore(container, markup, null);
}

而DOMLazyTree.insertTreeBefore这个函数,这个函数里面有一个比较核心的函数insertTreeChildren,这个函数会调用insertTreeChildren,insertTreeChildren又会根据传入的 tree的 node长度递归调用insertTreeChildren,总之这两个函数会互相调用。直到所有的children都被放入其对应的node,并渲染到对应容器里面。

1
2
3
4
5
if (children.length) {
for (var i = 0; i < children.length; i++) {
insertTreeBefore(node, children[i], null);
}
}

最终,它的执行结果是 insertTreeBefore最外层获取了一个markup, 这个markup的node最终将所有的children里面的node渲染到这个markup的node节点 也就是我们最最终想要渲染的HTMLELement。获取之后,就被插入到了container里面。

最终,整个渲染就到此结束了。

TODO

  • ReactReconciler.mountComponent过程是怎样的
  • 事件的处理是怎样的
  • 生命周期的实现