渲染处理
在React中,是将编写的Jsx语法编译为虚拟DOM对象,然后将构建的虚拟DOM对象渲染为真实dom,最后将真实DOM渲染到页面中的过程。
第一次渲染页面是直接从虚拟DOM转化编译为真实DOM,后面每当状态发生改变时,React会生成新的虚拟DOM并与旧的虚拟DOM进行比较,
通过比较新旧虚拟DOM的差异,找出需要更新的部分,并将这些部分更新到真实DOM上。
下面看这段代码
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
<h1>react Jsx渲染</h1>
<div>Jsx编译成虚拟DOM,然后将构建的虚拟DOM对象渲染为真实dom,最后将真实DOM渲染到页面中。</div>
</>
);
上面写的是Jsx代码的格式,React会做些什么呢?
首先React基于 babel-preset-react-app
把JSX编译为 React.createElement(...)
这种格式!!
只要是元素节点,必然会基于createElement
进行处理。
然后执行React.createElement
函数对象返回虚拟DOM。
在控制台输出结果:
console.log(
React.createElement(
React.Fragment,
null,
React.createElement("h1", {className: "title"}, "react Jsx\u6E32\u67D3"),
React.createElement("div", {className: "content"}, "Jsx\u7F16\u8BD1\u6210\u865A\u62DFDOM\uFF0C\u7136\u540E\u5C06\u6784\u5EFA\u7684\u865A\u62DFDOM\u5BF9\u8C61\u6E32\u67D3\u4E3A\u771F\u5B9Edom\uFF0C\u6700\u540E\u5C06\u771F\u5B9EDOM\u6E32\u67D3\u5230\u9875\u9762\u4E2D\u3002")
)
)
输出
可以得到这样的一个对象,里面包含了节点类型,属性、子节点等信息。
createElement
函数做了什么?看下面这段代码:
//创建虚拟DOM 返回一个虚拟节点对象
function createElement(ele,props,...children){
const vdom = {
$$typeof: Symbol('react.element'),
type:null,//标签名/组件
props:{}// 存储了元素的相关属性 && 子节点信息
}
let len = children.length
vdom.type = ele
if(props!==null){
vdom.props = {
...props
}
if(len === 1){
vdom.props.children = children[0]
}
if(len > 1){
vdom.props.children = children
}
}
return vdom
}
ele
:元素标签名/组件名
props
:元素的属性集合,如果没有设置过任何属性,则此值是null)
children
:第三个及以后的参数,都是当前元素的子节点。如果没有子节点则没有这个属性,属性值可能是一个值,也可能是一个数组
以上是利用createElement
构建的一个简单的虚拟DOM。
最后就是利用render函数
把构建的虚拟DOM渲染为真实DOM。
首先需要一个迭代器,可以获取对象所有的私有属性。代码如下:
function each(obj, callback){
if(obj === null || typeof obj !== 'object') throw new TypeError('obj is not a object');
if(typeof callback !== "function") throw new TypeError('callback is not a function');
let keys = Reflect.ownKeys(obj);
keys.forEach(key => {
let value = obj[key];
// 每一次迭代,都把回调函数执行
callback(value, key);
});
}
然后模拟render
函数,代码如下:
function render(vdom, container) {
let { type, props } = vdom;
// 如果type是string,那一定是标签名称,不是组件
if (typeof type === "string") {
// 创建dom标签
let ele = document.createElement(type);
// 枚举虚拟DOM对象的所有props属性,为标签设置相关的属性 & 子节点
each(props, (value, key) => {
// 如果key===className,那就是标签的class样式名称
if (key === 'className') {
ele.className = value;
return;
}
// 如果key===style,就是给标签设置的行内样式对象
if (key === 'style') {
each(value, (val, attr) => {
ele.style[attr] = val;
});
return;
}
// 如果key===children,那说明标签存在子节点
if (key === 'children') {
let children = value;
if (!Array.isArray(children)) children = [children];
children.forEach(child => {
// 子节点如果是文本节点,直接在标签内插入即可
if (/^(string|number)$/.test(typeof child)) {
ele.appendChild(document.createTextNode(child));
return;
}
// 如果子节点又是一个虚拟DOM,就递归处理
render(child, ele);
});
return;
}
// 其他属性设置
ele.setAttribute(key, value);
});
// 把新增的标签,增加到指定容器中
container.appendChild(ele);
} else {
// 如果是组件名称就会执行createElement函数创建虚拟DOM,然后又会执行render函数渲染
//...
}
};
最后调用上面的createElement
和render
函数,生成真实DOM,渲染到浏览器
const vdom = createElement(
"div",
{className:"box"},
createElement("h1",{className:"title"},"标题"),
createElement("div",{className:"content"},"内容"),
)
console.log(vdom)//虚拟DOM对象
render(vdom, document.getElementById('root'))
浏览器显示效果:
总结
- 基于
babel-preset-react-app
将调用的组件转化换为React.createElement()
格式 - 执行
React.createElement()
创建虚拟DOM - 把虚拟DOM中
props
作为实参传递给createElement
函数,执行该函数,得到返回结果,然后基于render
函数渲染为真实DOM,插入到body里面的root
容器中。
一幅图说明一切