React的虚拟DOM转化为真实DOM
在上一篇React的虚拟DOM生成中,我们成功的将jsx转换的函数转变为了虚拟DOM,我们现在需要做的就是将其转化为真实DOM并渲染在界面上。
首先我们来过一下转化的过程
jsx
<h1 className='title' style={{ color: "red" }}>hello<span>word</span></h1>
babel翻译后
React.createElement("h1", {
className: "title",
style: {
color: "red"
}
}, "hello", React.createElement("span", null, "word"))
React.createElement转化后
{
"props": {
"className": "title",
"style": {
"color": "red"
},
"children": [
{
"props": {
"content": "hello"
}
},
{
"props": {
"children": [
{
"props": {
"content": "word"
}
}
]
},
"type": "span"
}
]
},
"type": "h1"
}
然后我们看看react中是怎么渲染到页面上的
import React from "react
import ReactDOM from "react-dom"
const element = (<h1 className='title' style={{ color: "red" }}>hello<span>word</span></h1>)
ReactDOM.render(
element,
document.getElementById('root')
);
分析:
- 通过ReactDOM这对象的render方法进行渲染
- 第一个参数为渲染对象
- 第二个参数为渲染的容器节点
处理:
- 创建一个
react-dom.js文件 - 导出
ReactDOM对象 ReactDOM中带有属性render,并提供render方法
实现ReactDOM.render方法
/**
* 虚拟dom转真实dom
* @param {*} vdom 虚拟dom
* @param {*} container 容器
*/
function render(vdom, container) {
mount(vdom, container)
}
const ReactDOM = {
render
}
export default ReactDOM;
为了代码的复用性和可读性,我们添加一个mount方法,意为挂载,专注一个事情,将元素添加到容器中
/**
* 挂载方法
* @param {*} vdom 虚拟dom
* @param {*} container 挂载容器
*/
function mount(vdom, container) {
let newDOM = createDOM(vdom);
if (newDOM) {
container.appendChild(newDOM);
}
}
我们的挂载方法依然只是简单的几行代码
- 创建一个DOM元素,我们又添加了一个
createDOM方法,专门创建元素使用 - 如果DOM存在就添加到容器中
function createDOM(vdom) {
if (!vdom) return null;
let { key, props, ref, type } = vdom;
let dom; // 真实dom
// 如果是文本类型,创建一个文本节点
if (type === REACT_TEXT) {
dom = document.createTextNode(props.content);
} else {
dom = document.createElement(type);
}
if (props) {
updateProps(dom, {}, props);
if(props.children){
let children = props.children;
if(Array.isArray(children)){
reconcileChildren(children, dom)
}
}
}
return dom;
}
-
判断是否存在虚拟DOM,不存在就返回null
-
判断DOM节点是否为文本节点,如果是文本节点,就创建文本节点
-
如果不是文本节点,那么就创建一个type的节点【type保存着节点的标签名】
-
判断props是否存在,如果存在,那么就进行修改props,这里的话,我们还是创建了一个【updateProps方法】专门处理该事情
- 参数一: 当前DOM,后面属性是不是要添加到当前DOM上
- 参数二:空对象,这个表示一个旧属性,后面diff的时候需要,我们提前预留下,因为第一次没有,所以是空对象
- 参数三:props,这个就表示当前的一个新的属性对象
-
判断一下是否有children属性,如果children属性存在,并且是个数组的话,那么就使用
reconcileChildren方法对子节点进行一个挂载- 这里处理children属性是因为children也是属于DOM的,并非属性的,所以放在当前方法处理更为合适,当然放在props属性中处理也不是不行
-
返回DOM元素
我们先看那个updateProps方法
/**
* 更新props,后面可能进行diff,所以带有旧属性
* @param {*} dom 真实dom
* @param {*} oldProps 旧属性
* @param {*} newProps 新属性
*/
function updateProps(dom, oldProps, newProps) {
for (const key in newProps) {
// 当前值
const data = newProps[key];
// 子属性忽略
if (key === "children") { continue; }
// 样式类型单独处理
else if (key === "style") {
for (const attr in data) {
dom.style[attr] = data[attr];
}
}
// 其他属性直接赋值
else {
dom[key] = data;
}
}
}
- 循环取出所有的属性值与key
- 判断
key是否为children,因为我们在createDOM方法中处理了该属性,所以跳过 - 判断
key是否为style,style这个是一个对象,并且我们需要特殊对待他,所以单独处理一下 - 其他情况下直接赋值,如果还有什么需要特殊处理的可以在这里进行判断
最后我们再看看reconcileChildren方法
/**
* 递归处理子元素
* @param {*} childrenVdom 子元素
* @param {*} parentDOM 父元素
*/
function reconcileChildren(childrenVdom, parentDOM){
childrenVdom.forEach(children => mount(children, parentDOM));
}
我们直接挂载就好了,这样形成递归,渲染所有的子节点
最后我们可以看看效果了
import React from './package/react';
import ReactDOM from './package/react-dom';
const element = React.createElement("h1", {
className: "title",
style: {
color: "red"
}
}, "hello", React.createElement("span", null, "word"))
ReactDOM.render(
element,
document.getElementById('root')
);