// 一个li节点自带的属性就有307个
const liDom = document.createElement("li");
let num = 0;
for (let key in liDom) {
num += 1;
console.log(num); // 307
我们生成了一个最基本的li
节点,并通过遍历依次访问节点的属性,经过统计发现li
单属性就307
个,而这仅仅是一个节点。
在前面我们也提到过,不管是jq
封装,还是react vue
的模板语法,它的前提一定是研发自己提前知道了哪部分内容未来是可变的,所以我们才要动态封装,才需要使用{}
进行包裹,那既然如此,我们就对比未来可能会变的部分不是更好吗?
而回到上文我们对于虚拟结构的抽象,对于react
而言,props
是可变的,child
是可变的,state
也是可变的,而这些属性恰好都在虚拟dom
中均有呈现。
所以到这里,我们解释了虚拟dom
的第一个优势,站在对比更新的角度,虚拟dom
能聚焦于需要对比什么,相对原生dom
它提供更高效的对比可行性。
肆 ❀ 贰 更佳的兼容性
我们在上文提到,react与babel
将jsx
转成了js
对象(虚拟dom),之后又通过render
生成dom
,那为啥还要转成js
而不是直接生成dom
呢,因为在这个中间react
还需要做diff
对比,兼容处理,以及跨平台的考虑,我们先说兼容处理。
准确来说,虚拟dom
只是react
中的一部分,要真正体现虚拟dom
的价值,肯定得结合react
中的其它设计来一起讲,其中一点就是结合合成事件所体现的强大的兼容性。
我们在介绍jq
时强调了它在操作dom
的便捷,以及各类api
兼容性上的贡献,而react
中使用了虚拟dom
也做了大量的兼容。
打个比方,原生的input
有change
事件,普通的div
总没有onchange
事件吧?不管你有没有留意,其实dom
和事件在底层已经做了强关联,不同的dom
能触发的事件,浏览器在一开始就已经定义好了,而且你根本改不了。
但是虚拟dom
就不同了,虚拟dom
一方面模仿了原生dom
的行为,其次在事件方面也做了合成事件与原生事件的映射关系,比如:
onClick: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
react
暴露给我们的合成事件,其实在底层会关联到多个原生事件,通过这种做法抹平了不同浏览器之间的api
差异,也带来了更强大的事件系统。
若对于合成事件若感兴趣,可以阅读博主 八千字长文深入了解react合成事件底层原理,原生事件中阻止冒泡是否会阻塞合成事件?一文。
肆 ❀ 叁 渲染优化
我们知道react
遵循UI = Render(state)
,只要state
发生了改变,那么render
就会重新触发,以达到更新ui
层的效果。而更改state
依赖了setState
,大家都知道setState
对于state
更新的行为其实是异步的,假设我们在一次事件中更改了多次state
,你会发现页面也仅会渲染一次。
而假定我们是直接操作dom
,那还有哪门子的异步和渲染等待,当你append
完一个子节点,页面早渲染完了。所以虚拟dom
的对比提前,以及setState
的异步处理,本质上也是在像尽可能少的操作dom
靠近。
若对于setState
想有更深入的了解,可以阅读博主这两篇文章:
react中的setState是同步还是异步?react为什么要将其设计成异步?
react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?
肆 ❀ 肆 跨平台能力
同理,之所以加入虚拟dom
这个中间层,除了解决部分性能问题,加强兼容性之外,还有个目的是将dom
的更新抽离成一个公共层,别忘了react
除了做页面引用外,react
还支持使用React Native
做原生app
。所以针对同一套虚拟dom
体系,react
只是在最终将体现在了不同的平台上而已。
伍 ❀ 虚拟DOM比原生快吗?
那么问题来了,聊了这么久的虚拟dom
,虚拟dom
性能真的比操作原生dom
要更快吗?很遗憾的说,并不是,或者说不应该这样粗暴的去对比。
我们在前面虽然对比了虚拟dom
属性以及原生dom
的属性量级,但事实上我们并不会对原生dom
属性进行递归对比,而是直接操作dom
。而且站在react
角度,即便经历了diff
算法以及一系列的优化,react
到头来还是要操作原生dom
,只是对于研发来讲不用关注这一步罢了。
所以我们可以想象一下,现在要替换p
标签的内容,用原生就是直接修改innerHTML
属性,对于react
而言它需要先生成虚拟dom
,然后新旧diff
找出变化的部分,最后才修改原生dom
,单论这个例子,一定是原生快。
但我们既然说虚拟dom
,就一定得结合react
的使命来解释,虚拟dom
的核心目的是模拟了原生dom
大部分特性,让研发高效无痛写html
的同时,还达到了单点刷新而不是整个替换(前面表单替换的例子),最重要的,它也将研发从繁琐的dom
操作中解放了出来。
总结来说,单论修改一个dom
节点的性能,不管react
还是vue
亦或是angular
,一定是原生最快,但虚拟dom
有原生dom
比不了的价值,起码react
这些框架能让研发更专注业务以及数据处理,而不是陷入繁琐的dom
增删改查中。
陆 ❀ 虚拟DOM的实现原理
文章开头的五个问题到这里已经解释了三个,还剩两个问题均与源码有一定关系,虽然略显枯燥但我会精简给大家阐述这个过程,另外,为了让知识量不会显得格外庞大,本文将不会阐述diff
算法与fiber
部分,这两个知识点我会另起文章单独介绍,敬请期待。
除此之外,接下来两个问题的源码,我将均以react17.0.2
源码为准,所以大家也不用担心版本差异,会不会有理解了用不上的问题,而且目前用react 18
的公司也不会很多。
我们先解释虚拟dom
的创建过程,要聊这个那必然逃不开React.createElement
方法,github源码,具体代码如下(我删除了dev
环境特有的逻辑):
* 创建并返回给定类型的新ReactElement。
* See https://reactjs.org/docs/react-api.html#createelement
function createElement(type, config, children) {
let propName;
// 创建一个全新的props对象
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// 有传递自定义属性进来吗?有的话就尝试获取ref与key
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
if (hasValidKey(config)) {
key = '' + config.key;
// 保存self和source
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 剩下的属性都添加到一个新的props属性中。注意是config自身的属性
for (propName in config) {
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
props[propName] = config[propName];
// 处理子元素,默认参数第二个之后都是子元素
const childrenLength = arguments.length - 2;
// 如果子元素只有一个,直接赋值
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// 如果是多个,转成数组再赋予给props
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
props.children = childArray;
// 处理默认props,不一定有,有才会遍历赋值
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
// 默认值只处理值不是undefined的属性
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
// 调用真正的React元素创建方法
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
代码看着好像有点多,但其实一共就只做了两件事:
根据createElement
所接收参数config
做数据加工与赋值。
加工完数据后调用真正的虚拟dom
创建API ReactElement
。
而数据加工部分可分为三步,大家可以对应上面代码理解,其实注释写的也很清晰了:
第一步,判断config
有没有传,不为null
就做处理,步骤分为
判断ref
、key
,__self
、__source
这些是否存在或者有效,满足条件就分别赋值给前面新建的变量。
遍历config
,并将config
自身的属性依次赋值给前面新建props
。
第二步,处理子元素。默认从第三个参数开始都是子元素。
如果子元素只有一个,直接赋值给props.children
。
如果子元素有多个,转成数组后再赋值给props.children
。
第三步,处理默认属性defaultProps
,一个纯粹的标签也可以理解成一个最最最基础的组件,而组件支持 defaultProps
,所以这一步判断有没有defaultProps
,如果有同样遍历,并将值不为undefined
的部分都拷贝到props
对象上。
至此,第一大步全部做完,紧接着调用ReactElement
,我们接着看这一块的源码,同样我删掉dev
部分的逻辑,然后你会发现就这么一点代码,github源码:
const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
// 这个标签允许我们将其标识为唯一的React Element
$$typeof: REACT_ELEMENT_TYPE,
// 元素的内置属性
type: type,
key: key,
ref: ref,
props: props,
// 记录负责创建此元素的组件。
_owner: owner,
return element;
这个方法啥也没干,单纯接受我们在上个方法加工后的数据,并将其组装成了一个element
对象,也就是我们前文所说的虚拟dom
。
不过针对这个虚拟dom
,我们可以把$$typeof: REACT_ELEMENT_TYPE
拧出来单独讲讲。我们可以看看它的具体实现:
// The Symbol used to tag the ReactElement-like types.
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
大家在查看虚拟dom
时应该都有发现它的$$typeof
定义为Symbol(react.element)
,而Symbol
一大特性就是标识唯一性,即便两个看着一模一样的Symbol
,它们也不会相等。而react
之所以这样做,本质也是为了防止xss
攻击,防止外部伪造虚拟dom
结构。
其次,如果大家有在开发中留意,虚拟dom
的不允许修改,哪怕你为这个对象新增属性也不可以,这是因为在ReactElement
方法省略的dev
代码中,react
使用Object.freeze
冻结了虚拟dom
使其无法修改。但实际上我们确实有为虚拟dom
添加属性的场景,解决这个问题时我们可以借用顶层React.cloneElement()
方法,它会以你传递的虚拟dom
为模板克隆并返回一个新的虚拟dom
对象,同时这个过程中你可以为其添加新的config
,具体用法可见 React.cloneElement。
其次,如果当前环境不支持Symbol
时,REACT_ELEMENT_TYPE
的值为0xeac7
。
var REACT_ELEMENT_TYPE = 0xeac7;
为什么是0xeac7
呢?官方答复是,因为它看起来像React
....好了,那么到这里,关于如何生成虚拟dom
的源码分析结束。
柒 ❀ react中虚拟dom是如何转变成真实dom的
终于,我们来到了本文的最后一个问题,要想搞清这个问题,我们的关注点自然是ReactDOM.render
方法了,这个部分比较麻烦,大家跟着我的思路走就行。(有兴趣可以直接把react
脚手架项目跑起来,写一个最基本的组件,然后去react-dom.development.js
文件断点也可以)。
// 我为了方便断点,定义了一个class组件P
class P extends Component {
state = {
name: 1,
handleClick = () => {};
render() {
return <span onClick={this.handleClick}>111</span>;
ReactDOM.render(<P />, document.getElementById("root"));
首先我们来到render
方法,代码如下:
function render(element, container, callback) {
// 我删除了对于container是否合法的效验逻辑
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
render
做的事情其实很简单,验证container
是否合法,如果不是一个有效的dom
就会抛错,核心逻辑看样子都在legacyRenderSubtreeIntoContainer
中,根据命名可以推测是将组件子树都渲染到容器元素中。
// 同样,我删除了部分对主逻辑理解没啥影响的代码
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
var root = container._reactRootContainer;
var fiberRoot;
// 有fiber的root节点吗?没有就新建
if (!root) {
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
fiberRoot = root._internalRoot;
unbatchedUpdates(function () {
// 核心关注这里
updateContainer(children, fiberRoot, parentComponent, callback);
} else {
fiberRoot = root._internalRoot;
updateContainer(children, fiberRoot, parentComponent, callback);
return getPublicRootInstance(fiberRoot);
因为react 16
引入了fiber
的概念,所以后续其实很多代码就是在创建fiber
节点,legacyRenderSubtreeIntoContainer
一样,它一开始判断有没有root
节点(一个fiber对象),很显然我们初次渲染走了新建逻辑,但不管是不是新建,最终都会调用updateContainer
方法。但此方法没有太多我们需要关注的逻辑,一直往下走,我们会遇到一个很重要的beginWork
(开始干正事)方法,代码如下:
function beginWork(current, workInProgress, renderLanes) {
// 删除部分无影响的代码
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
// 模糊定义的组件
case IndeterminateComponent:
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
// 函数组件
case FunctionComponent:
var _Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
// class组件
case ClassComponent:
var _Component2 = workInProgress.type;
var _unresolvedProps = workInProgress.pendingProps;
var _resolvedProps = workInProgress.elementType === _Component2 ? _unresolvedProps : resolveDefaultProps(_Component2, _unresolvedProps);
return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
beginWork
方法做了很重要的一件事,那就是根据你render
接收的组件类型,来执行不同的组件更新的方法,毕竟我们可能给render
传递一个普通标签,也可能是函数组件或者Class
组件,亦或是hooks
的memo
组件等等。
比如我此时定义的P
是class
组件,于是走了ClassComponent
路线,紧接着调用updateClassComponent
更新组件。
function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {
// 删除了添加context部分的逻辑
// 获取组件实例
var instance = workInProgress.stateNode;
var shouldUpdate;
// 如果没有实例,那就得创建实例
if (instance === null) {
if (current !== null) {
current.alternate = null;
workInProgress.alternate = null;
workInProgress.flags |= Placement;
// 全体目光向我看齐,看我看我,这里new Class创建组件实例
constructClassInstance(workInProgress, Component, nextProps);
// 挂载组件实例
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes);
} else {
shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
// Class组件的收尾工作
var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes);
在看这段代码前,我们自己也可以提前想象下这个过程,比如Class
组件你一定是得new
才能得到一个实例,只有拿到实例后才能调用其render
方法,拿到其虚拟dom
结构,之后再根据结构创建真实dom
,添加属性,最后加入到页面。
所以在updateClassComponent
中,首先会对组件做context
相关的处理,这部分代码我删掉了,其余,判断当前组件是否有实例,如果有就去更新实例,如果没有那就创建实例,所以我们聚焦到constructClassInstance
与mountClassInstance、finishClassComponent
三个方法,看命名就能猜到,前者一定是创造实例,后者是应该是挂载实例前的一些处理,先看第一个方法:
function constructClassInstance(workInProgress, ctor, props) {
// 删除了对组件context进一步加工的逻辑
// ....
// 看我看我,我宣布个事,这里创建了组件实例
// 验证了前面的推测,这里new了我们的组件,并且传递了当前组件的props以及前面代码加工的context
var instance = new ctor(props, context);
var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
adoptClassInstance(workInProgress, instance);
// 删除了对于组件生命周期钩子函数的处理,比如很多即将被废弃的钩子,在这里都会被添加 UNSAFE_ 前缀
//.....
return instance;
constructClassInstance
正如我们推测的一样,这里通过new ctor(props, context)
创建了组件实例,除此之外,react
后续版本已将部分声明周期钩子标记为不安全,对于钩子命名的加工也在此方法中。
紧接着,我们得到了一个组件实例,接着看mountClassInstance
方法:
function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
// 此方法主要是对constructClassInstance创建的实例进行数据组装,为其赋予props,state等一系列属性
var instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;
initializeUpdateQueue(workInProgress);
// 删除了部分特殊情况下,对于instance的特殊处理逻辑
虽然命名是挂载,但其实离真正的挂载还远得很,本方法其实是为constructClassInstance
创建的组件实例做数据加工,为其赋予props state
等一系列属性。
在上文代码中,其实还有个finishClassComponent
方法,此方法在组件自身都准备完善后调用,我们期待已久的render
方法处理就在里面:
function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
var instance = workInProgress.stateNode;
ReactCurrentOwner$1.current = workInProgress;
var nextChildren;
if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
// ...
} else {
setIsRendering(true);
// 关注点在这,通过调用组件实例的render方法,得到内部的元素
nextChildren = instance.render();
setIsRendering(false);
workInProgress.memoizedState = instance.state;
return workInProgress.child;
在此方法内部,我们通过获取之前创建的组件实例,然后调用了它的render
方法,于是成功执行了我们组件P
的render
方法:
render() {
return <span onClick={this.handleClick}>111</span>;
需要注意的是,render
返回的其实是一个jsx
的模板语法,在真正return
之前,react
还会再次调用生成虚拟dom
的逻辑也就是ReactElement
方法,将span
这一段转变成虚拟dom
。
而对于react
而言,很明显虚拟dom
的span
也可能理解成一个最最最基础的组件,所以它会重走beginWork
这条路线,只是到了组件分类时,这一次会走HostComponent
路线,然后触发updateHostComponent
方法,我们直接跳过相同的流程,之后就会走到completeWork
方法。
到这里,我们可以理解例子P
组件虚拟dom
都准备完毕,现在要做的是对于虚拟
dom这种最基础的组件做转成真实dom
的操作,见如下代码:
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps;
// 根据tag类型做不同的处理
switch (workInProgress.tag) {
// 标签类的基础组件走这条路
case HostComponent:
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// ...
} else {
// ...
} else {
// 关注点1:创建虚拟dom的实例
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
// 关注点2:初始化实例的子元素
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
可以猜到,虽然同样还是调用createInstance
生成实例,但目前咱们的组件是个虚拟dom
对象啊,一个普通的span
标签,所以接下来一定会创建最基本的span
节点,代码如下:
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
// 根据span创建节点,调用createElement方法
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
precacheFiberNode(internalInstanceHandle, domElement);
// 将虚拟dom span的属性添加到span节点上
updateFiberProps(domElement, props);
return domElement;
// createElement具体实现
function createElement(type, props, rootContainerElement, parentNamespace) {
var isCustomComponentTag;
var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement);
var domElement;
var namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE$1) {
if (type === 'script') {
var div = ownerDocument.createElement('div');
div.innerHTML = '<script><' + '/script>';
var firstChild = div.firstChild;
domElement = div.removeChild(firstChild);
} else if (typeof props.is === 'string') {
domElement = ownerDocument.createElement(type, {
is: props.is
} else {
// 在这里,真实dom span节点创建完毕
domElement = ownerDocument.createElement(type);
if (type === 'select') {
var node = domElement;
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
node.size = props.size;
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
return domElement;
在createElement
方法中,react
会根据你的标签类型来决定怎么创建dom
,比如如果你是script
,那就创建一个div
用于包裹一个script
标签。而我们的span
很显然就是通过ownerDocument.createElement(type)
创建,如下图:
创建完成后,此时的span
节点还是一个啥都没有的空span
,所以通过updateFiberProps
将还未加工的span
的子节点以及其它属性强行赋予给span
,在之后会进一步加工,之后返回我们的span
:
然后来到finalizeInitialChildren
方法,这里开始对创建的span
节点的子元素进一步加工,其实就是文本111
,
function finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
// 实际触发的其实是这个
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
// 跳过对于部分,接着看 setInitialDOMProperties
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
var props;
switch (tag) {
// ...
default:
props = rawProps;
// 验证props合法性
assertValidProps(tag, props);
// 正式设置props
setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);
又是一系列的跳转,为dom
设置属性的逻辑现在又聚焦在了setInitialDOMProperties
中,我们直接看代码:
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
for (var propKey in nextProps) {
// 遍历所有属性,只要这个属性不是原型属性,那就开始正式处理
if (!nextProps.hasOwnProperty(propKey)) {
continue;
var nextProp = nextProps[propKey];
// 如果属性是样式,那就通过setValueForStyles为dom设置样式
if (propKey === STYLE) {
if (nextProp) {
Object.freeze(nextProp);
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
var canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
// 设置文本属性
setTextContent(domElement, nextProp);
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if ( typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
这段代码看着有点长,其实做的事情非常的清晰,遍历span
目前的props
,如果props
的key
是style
,那就通过setValueForStyles
为当前真实dom
一一设置样式,如果key
是children
,很明显我们虚拟dom
的111
是放在children
属性中的,外加上如果这个children
类型还是string
,那就通过setTextContent
为dom
添加文本信息。
这里给大家展示为真实dom
设置style
以及设置innerHTML
的源码:
// 为真实dom添加样式的逻辑
function setValueForStyles(node, styles) {
// 获取真是dom的style对象,后面就遍历styles对象,依次覆盖
var style = node.style;
for (var styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
continue;
var isCustomProperty = styleName.indexOf('--') === 0;
if (!isCustomProperty) {
warnValidStyle$1(styleName, styles[styleName]);
// 获取样式的值
var styleValue = dangerousStyleValue(styleName, styles[styleName], isCustomProperty);
if (styleName === 'float') {
styleName = 'cssFloat';
// 最终覆盖node节点原本的值
if (isCustomProperty) {
style.setProperty(styleName, styleValue);
} else {
style[styleName] = styleValue;
// 为真实dom添加innerHTML的逻辑
var setTextContent = function (node, text) {
if (text) {
var firstChild = node.firstChild;
if (firstChild && firstChild === node.lastChild && firstChild.nodeType === TEXT_NODE) {
firstChild.nodeValue = text;
return;
// 为真实dom设置文本信息
node.textContent = text;
那么到这里,其实我们的组件P
已经准备完毕,包括真实dom
也都创建好了,就等插入到页面了,那这些dom
什么时候插入到页面的呢?后面我又跟了下调用栈,根据我页面啥时候绘制的111
一步步断点缩小范围,最终定位到了insertOrAppendPlacementNodeIntoContainer
方法,直译过来就是将节点插入或者追加到容器节点中:
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
var tag = node.tag;
var isHost = tag === HostComponent || tag === HostText;
if (isHost || enableFundamentalAPI ) {
var stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
// 在容器节点前插入
insertInContainerBefore(parent, stateNode, before);
} else {
// 在容器节点后追加
appendChildToContainer(parent, stateNode);
} else if (tag === HostPortal) ; else {
var child = node.child;
// 只要子节点不为null,继续递归调用
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
var sibling = child.sibling;
// 只要兄弟节点不为null,继续递归调用
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
在insertOrAppendPlacementNodeIntoContainer
中,react
会根据当前节点是否有子节点,或者兄弟节点进行递归调用,然后分别根据insertInContainerBefore
与appendChildToContainer
做最终的节点插入页面操作,这里我们看看appendChildToContainer
的实现:
function appendChildToContainer(container, child) {
var parentNode;
if (container.nodeType === COMMENT_NODE) {
parentNode = container.parentNode;
parentNode.insertBefore(child, container);
} else {
parentNode = container;
// 将子节点插入到父节点中
parentNode.appendChild(child);
var reactRootContainer = container._reactRootContainer;
if ((reactRootContainer === null || reactRootContainer === undefined) && parentNode.onclick === null) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(parentNode);
由于我们定义的组件非常简单,P
组件只有一个span
标签,所以这里的parentNode
其实就是容器根节点,当执行完parentNode.appendChild(child)
,可以看到页面就出现了111
了。
至此,组件的虚拟dom
生成,真实dom
的创建,加工以及渲染全部执行完毕。
可能大家对于这个过程还是比较迷糊,我大致画个图描述下这个过程:
而react
是怎么知道谁是谁的子节点,谁是谁的父节点,这个就需要了解fiber
对象了,其实我们在创建完真实dom
后,它还是会被加工成一个fiber
节点,而此节点中通过child
可以访问到自己的子节点,通过sibling
获取自己的兄弟节点,最后通过return
属性获取自己的父节点,通过这些属性为构建dom
树提供了支撑,当然fiber
我会另开一篇文章来解释,这里不急。
前文,我们验证了Class
组件是通过new
得到组件实例,然后开展后续操作,那对于函数组件,是不是直接调用拿到子组件呢?这里我简单跟了下源码,发现了如下代码:
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
// ....
var children = Component(props, secondArg);
可以发现确实如此,拿到子节点,然后后续还是跟之前一样,将虚拟dom
转变成真实dom
,以及后续的一系列操作。
不过有点意外的是,我以为我定义的函数组件在判断组件类型时,会走case FunctionComponent
分支路线,结果它走的case IndeterminateComponent
,也就是模糊定义的组件,不过影响不大,还是符合我们的推测。
好了,到这里,我已经写了一万字,关于虚拟dom
如何转变成真实dom
也介绍完毕了。
捌 ❀ 我是如何阅读源码的
在文章结束前,我顺带分享下我是如何阅读react
源码的,本来在写这篇文章前,我也想着要不查查资料,看看大家都是怎么写的,结果部分高赞的文章基本发布时间都在19
年,那时候的react
版本基本都是15
,连fiber
的概念都没有,无奈之下我只能自己来尝试读源码并解决我自己提出的问题。如果将源码阅读理解成一次探险,我是这样做的。
捌 ❀ 壹 确定阅读前的目标
react
的源码比较多,一个react
一个react-dom
加起来代码量都几万行了,所以在读之前,一定要搞清楚自己的目标,这样你也能少受不重要逻辑的干扰,比如我在阅读之前初步定下的目标是:
虚拟dom
是怎么生成的?
函数组件和class
组件渲染有什么不同?
为啥我之前尝试直接修改虚拟dom
,添加属性没成功(对应后面typeof Symbol的解释)
虚拟dom
是怎么转变成真实dom
的?
啥时候才把真实dom
插入到页面?
清晰了目标,那就可以找到起点开始看了,我要看渲染,那自然看render
,但接下来就麻烦了,如果你跟着render
一步步往下走,那估计你看不了五分钟,应该就没耐心看了,因为这里面存在大量你根本看不懂,或者对你帮助不大的代码,那么我是怎么做的呢?
捌 ❀ 贰 以点成线
我要看虚拟dom
转变真实dom
,react
到头来还是要操作真实dom
,那它就一定得通过原生的createElement
来创建dom
节点,所以我直接在源码中搜createElement
,然后看看这些命名出现的上下文,根据语境大致推断是否是自己想要的,不确定也可以打个断点。
哎,然后我就发现我成功找到function createElement
方法,而且它还真是我想要的方法,但是呢,此时逻辑距离render
可谓是十万八千里,这中间究竟发生了什么?这时候就可以根据执行栈进行梳理:
比如上图就是我定位到给真实dom
添加属性的方法,然后我根据调用栈命名,大致知道它在干嘛,同时排除那些没意义的函数的干扰,从终点反向走回起点,看看这一路react
是怎么处理的。
同理,我在找最后react
将真实dom
插入到页面的逻辑时,我发现我跟不下去了,因为断点乱跳,于是我就看页面渲染111
的时机,然后初略断点,如果这个断点还没走到111
已经渲染了,说明这个操作在之前,通过这种方式不断缩小范围范围,最终定位到了insertOrAppendPlacementNodeIntoContainer
方法,也解开了我前面的疑惑。
捌 ❀ 叁 以线成面
通过以点连线的方式,你能非常快的理清一小段一小段的逻辑,而这些逻辑的交叉,阅读前的目标就逐渐清晰了。比如我在梳理了Class
组件后,我就在想,函数组件又是怎么渲染的?于是非常快的定位到了函数组件渲染子节点的逻辑。
我们可以把源码理解成夜晚的星空,小时候总是喜欢选几个点练成线,再用线连成图案,什么北极星织女星,不就是这样画出来的吗,而现在只是将这种做法投射到了源码阅读中罢了。
玖 ❀ 总
写到这已经一万一千字,差不多一篇论文的长度了。而这篇文章,从查资料,读源码到写作结束,也差不多用了我一周的零碎时间。一开始只是想写写概念,写着写着对自己要求越来越高,于是一篇文章写得停不下来了,不过好在终于写到了尾声,我也松了口气了(下一篇fiber
感觉也很难受的样子)。
通过本文,我们介绍了虚拟dom
的概念,了解了究竟什么是虚拟dom
。结合文章开头框架发展史,我们也解释了虚拟dom
存在的价值以及它所具备的优势,而且框架之间也不应该盲目的去对比。在文章后半段,我们介绍了React.createElement
与ReactDOM.render
的源码,理解了虚拟dom
的创建过程,以及react
是如何将虚拟dom
转变成真实dom
的,如果有时间,我也推荐大家自行断点,根据我的提示来加深理解这个过程,它并不难,只是需要足够的耐心。
希望本文能为有缘的你提供一些帮助,那么本文到这里正式结束。