# React虚拟dom以及如何将虚拟Dom转换成真实dom
# 虚拟 Dom 实现原理
# JSX 和 createElement
我们在实现一个React
组件时可以选择两种编码方式,第一种是使用JSX
编写:
class Hello extends Component {
render() {
return <div>Hello mrgaogang</div>;
}
}
第二种是直接使用React.createElement
编写:
class Hello extends Component {
render() {
return React.createElement("div", null, `Hello mrgaogang`);
}
}
实际上,上面两种写法是等价的,JSX
只是为 React.createElement(component, props, ...children)
方法提供的语法糖。也就是说所有的JSX
代码最后都会转换成React.createElement(...)
,Babel
帮助我们完成了这个转换的过程。
如下面的JSX
function TestComponent() {
return <div className="aa" style={{ backgroundColor: "white" }} />;
}
class Test2 extends React.Component {
render() {
return <div className="test2" onClick={() => {}} />;
}
}
function App() {
return (
<div className="App">
<TestComponent />
<Test2 />
Hello mrgaogang
<ul>
<li>苹果</li>
<li>橘子</li>
</ul>
</div>
);
}
将会被Babel
转换为: babel 在线查看 (opens new window)
function TestComponent() {
return React.createElement("div", {
className: "aa",
style: {
backgroundColor: "white",
},
});
}
class Test2 extends React.Component {
render() {
return React.createElement("div", {
className: "test2",
onClick: () => {},
});
}
}
function App() {
return React.createElement(
"div",
{
className: "App",
},
React.createElement(TestComponent, null),
React.createElement(Test2, null),
"Hello mrgaogang",
React.createElement(
"ul",
null,
React.createElement("li", null, "\u82F9\u679C"),
React.createElement("li", null, "\u6A58\u5B50")
)
);
}
注意,babel
在编译时会判断JSX
中组件的首字母,当首字母为小写时,其被认定为原生DOM
标签,createElement
的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement
的第一个变量被编译为对象;
另外,由于JSX
提前要被Babel
编译,所以JSX
是不能在运行时动态选择类型的,比如下面的代码:
function Story(props) {
// Wrong! JSX type can't be an expression.
return <components[props.storyType] story={props.story} />;
}
需要变成下面的写法:
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
所以,使用JSX
你需要安装Babel
插件babel-plugin-transform-react-jsx
{
"plugins": [
["transform-react-jsx", {
"pragma": "React.createElement"
}]
]
}
# 创建虚拟 DOM
下面我们来看看虚拟DOM
的真实模样,将下面的JSX
代码在控制台打印出来:
function TestComponent() {
return <div className="aa" style={{ backgroundColor: "white" }} />;
}
class Test2 extends React.Component {
render() {
return <div className="test2" onClick={() => {}} />;
}
}
function App() {
return (
<div className="App">
<TestComponent />
<Test2 />
Hello mrgaogang
<ul>
<li>苹果</li>
<li>橘子</li>
</ul>
</div>
);
}
那么React
是如何将我们的代码转换成这个结构的呢,下面我们来看看createElement
函数的具体实现(文中的源码经过精简)。
createElement
函数内部做的操作很简单,将props
和子元素进行处理后返回一个ReactElement
对象,下面我们来逐一分析:
(1).处理 props:
- 1.将特殊属性
ref
、key
从config
中取出并赋值 - 2.将特殊属性
self
、source
从config
中取出并赋值 - 3.将除特殊属性的其他属性取出并赋值给
props
后面的文章会详细介绍这些特殊属性的作用。
(2).获取子元素
- 1.获取子元素的个数 —— 第二个参数后面的所有参数
- 2.若只有一个子元素,赋值给
props.children
- 3.若有多个子元素,将子元素填充为一个数组赋值给
props.children
(3).处理默认 props
- 将组件的静态属性
defaultProps
定义的默认props
进行赋值
ReactElement
ReactElement
将传入的几个属性进行组合,并返回。
type
:元素的类型,可以是原生 html 类型(字符串),或者自定义组件(函数或class
)key
:组件的唯一标识,用于Diff
算法,下面会详细介绍ref
:用于访问原生dom
节点props
:传入组件的props
owner
:当前正在构建的Component
所属的Component
$$typeof
:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE
:
var REACT_ELEMENT_TYPE =
(typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) ||
0xeac7;
可见,$$typeof
是一个Symbol
类型的变量,这个变量可以防止XSS
。
如果你的服务器有一个漏洞,允许用户存储任意JSON
对象, 而客户端代码需要一个字符串,这可能会成为一个问题:
// JSON
let expectedTextButGotJSON = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: "/* put your exploit here */",
},
},
};
let message = { text: expectedTextButGotJSON };
<p>{message.text}</p>;
JSON
中不能存储Symbol
类型的变量。
ReactElement.isValidElement
函数用来判断一个React
组件是否是有效的,下面是它的具体实现。
ReactElement.isValidElement = function (object) {
return typeof object === 'object' && object !== null && object.?typeof === REACT_ELEMENT_TYPE;
};
可见React
渲染时会把没有?typeof
标识,以及规则校验不通过的组件过滤掉。
当你的环境不支持Symbol
时,?typeof
被赋值为0xeac7
,至于为什么,React
开发者给出了答案:
0xeac7
看起来有点像React
。
self
、source
只有在非生产环境才会被加入对象中。
self
指定当前位于哪个组件实例。_source
指定调试代码来自的文件(fileName
)和代码行数(lineNumber
)
# 虚拟 DOM 转换为真实 DOM
通常我们启动一个项目大致方法如下:
ReactDOM.render(<App />, document.getElementById("root"));
那么 ReactDOM 是如何将 vdom 转换成真实的 dom 呢?这里简单实现了一下ReactDOM.render
;真实的情况ReactDOM做了更多的事情。
function render(node, container) {
// 节点可能为文本节点
if (typeof node === "string" || typeof node === "number") {
return container.appendChild(document.createTextNode(node));
}
let props = node.props || {};
let type = node.type;
// 如果是class类型的组件,需要new实例
if (type && type.prototype && type.prototype.isReactComponent) {
const returnElement = new type(props).render();
type = returnElement.type;
props = returnElement.props;
} else if (typeof type === "function") {
// 如果为函数式组件需要调用
const returnElement = type(props);
type = returnElement.type;
props = returnElement.props;
}
const resultDom = document.createElement(type);
for (const key in props) {
const value = props[key];
if (key === "className") {
// 处理class
resultDom.className = value;
} else if (key === "style") {
// 处理style
resultDom.style.cssText = value;
} else if (key === "children") {
if (typeof value === "string" || typeof value === "number") {
// children可能为文本或者数组类型
// 如果内容是文字、数字之类的,把文字、数字放到标签里面
resultDom.appendChild(document.createTextNode(value));
} else {
if (Array.isArray(value)) {
//如果有多个chilren则循环插入
value.forEach((ele) => {
render(ele, resultDom);
});
} else {
//之后一个children的时候直接render
render(value, resultDom);
}
}
} else {
// 设置其余的属性
resultDom.setAttribute(key, value);
}
}
// 添加到父节点上
container.appendChild(resultDom);
}
export default { render };