【React】JSX底层处理机制
React 在内部会将 JSX 转换为 JavaScript 代码,通过
React.createElement
转换成
virtualDOM
,最后通过
render
渲染成真实DOM。
1、JSX语法转virtualDOM
虚拟DOM对象:框架自己内部构建的一套对象体系(对象的相关成员都是React内部规定的),基于这些属性描述出,我们所构建视图中的,DOM节点的相关特征
(1)基于babel-preset-react-app
把JSX编译为React.createElement(…)这种格式,只要是元素节点,必然会基于createElement
处理,可以通过 https://babeljs.io/repl 输入JSX代码,可以实时看到编译之后的结果
React.createElement(ele,props,...children)
- ele:元素标签名(或者组件)
- props:元素属性集合(对象),如果没有设置过任何的属性,则此值是null
- children:第三个及以后的参数,都是当前元素的子节点
//JSX
<>
<div className='container'>
<h2 className="title" style={styObj}>hello world</h2>
<div className="box">
<span>{x}</span>
<span>{y}</span>
</div>
</div>
</>
React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
className: "container"
},
React.createElement(
"h2",
{
className: "title",
style: styObj
},
"hello world"
),
React.createElement(
"div",
{
className: "box"
},
React.createElement("span", null, x),
React.createElement("span", null, y)
)
)
);
(2)再把createElement方法执行,创建出virtualDOM「也有称之为:JSX元素、JSX对象、ReactChild对象…」
virtualDOM = {
$$typeof: Symbol(react.element),
ref: null,
key: null,
type: 标签名「或组件」,
// 存储了元素的相关属性 && 子节点信息
props: {
元素的相关属性,
children:子节点信息「没有子节点则没有这个属性、属性值可能是一个值、也可能是一个数组」
}
}
这里我们打印下createElement
执行之后得到的virtualDOM!
let jsxObj = React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
className: "container"
},
React.createElement(
"h2",
{
className: "title",
style: styObj
},
"hello world"
),
React.createElement(
"div",
{
className: "box"
},
React.createElement("span", null, x),
React.createElement("span", null, y)
)
)
);
console.log(jsxObj,'jsxObj')
2、构建的虚拟DOM渲染为真实DOM
真实DOM:浏览器页面中,最好渲染出来的可以被看到的DOM元素。
渲染为真实DOM是基于ReactDOM中的render方法处理
//react16版本
ReactDOM.render(<>.....</>,document.getElementId('root))
const root = ReactDOM.createRoot(document.getElementId('root))
root.render(<>xxxxxxxx</>)
补充说明:
第一次渲染页面是直接从virtualDOM->真实DOM;
后期视图更新的时候,需要经过一个DOM-DIFF
的对比,计算出补丁包PATCH(两次视图差异的部分),把PATCH补丁包进行渲染
3、手写createElement
createElement
:创建virtualDOM对象。首先virtualDOM对象的结构如下:
let virtualDOM = {
$$typeof:Symbol("react.element"),
key:null,
ref:null,
type:null,
props:{}//标签上属性以及子节点的属性
}
在src目录下创建jsxHandle.js文件,导出一个名为createElement
的函数。children
的类型是一个数组(由于使用了 …children 语法)。它可能会包含字符串、数字、React 元素或者是嵌套的数组。
使用createElement
,如下:
React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
className: "container"
},
React.createElement(
"h2",
{
className: "title",
style: styObj
},
"hello world"
),
React.createElement(
"div",
{
className: "box"
},
React.createElement("span", null, x),
React.createElement("span", null, y)
)
)
);
手写createElement
,如下:
- 声明virtualDOM需要的属性
- 设置virtualDOM的
type
为ele
- 设置virtualDOM的
props
为props
- 设置virtualDOM的
children
为传过来的children
(需要特殊处理)
export function createElement(ele, props, ...children) {
let virtualDOM = {
$$typeof: Symbol('react.element'),
key: null,
ref: null,
type: null,
props: {}
};
let len = children.length;
virtualDOM.type = ele;
if(props!==null){
virtualDOM.props = {
...props
}
};
console.log(len,'len',children)
if(len==1) virtualDOM.children = children[0];
if(len >1) virtualDOM.children = children;
return virtualDOM
};
打印出 console.log(len,'len',children)
,可见children
类型为Array
解释下上面的一些细节,使用下面JSX代码进行说明:
<></>
下只有div一个子元素,所以在虚拟DOM第一层的props
的children
的length
为1
<>
<div className='container'>
<h2 className="title" style={styObj}>hello world</h2>
<div className="box">
<span>{x}</span>
<span>{y}</span>
</div>
</div>
</>
对于下面的JSX,编译之后的virtualDOM如下,可以看到其children为10
,而不是一个数组。
<span>{y}</span>
4、手写 render
render 把 virtualDOM 变为真实DOM,继续在jsxHandle.js
文件中,导出一个render
函数,接收两个参数(对于16版本),以下为16版本,跟18版本大差不差。
//React18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
//React 16
render(虚拟DOM, document.getElementById('root'));
-
从virtualDOM 获取
type, props
-
如果type是
string
,则是html标签,创建标签(document.createElement(type)
) -
处理
props
- 设置标签上的
className、style
等属性,通过setAttribute
进行 - 有
children
属性,说明有子节点,其子节点可分为文本节点、virtualDOM节点- 文本节点:创建文件节点,
appendChild
到当前元素 - virtualDOM节点:继续递归执行
render
- 文本节点:创建文件节点,
- 设置标签上的
-
最终将元素
appendChild
到container
export function render(virtualDOM, container) {
let { type, props } = virtualDOM;
if (typeof type === "string") {
// 存储的是标签名:动态创建这样一个标签
let ele = document.createElement(type);
// 为标签设置相关的属性 & 子节点
each(props, (value, key) => {
console.log(props,value, key)
// className的处理:value存储的是样式类名
if (key === 'className') {
ele.className = value;
return;
}
// style的处理:value存储的是样式对象
if (key === 'style') {
each(value, (val, attr) => {
ele.style[attr] = val;
});
return;
}
// 子节点的处理:value存储的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;
}
// 子节点又是一个virtualDOM:递归处理
render(child, ele);
});
return;
}
ele.setAttribute(key, value);
});
// 把新增的标签,增加到指定容器中
container.appendChild(ele);
}
};
其中每次循环的props
的值如下:
递归props
设置标签上的属性,并处理子节点。所以这里需要一个循环的函数each
。
- 判断
props
是否为对象 - 判断回调的
callback
是否为函数
const each = 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);
});
};
each
循环中,props,value,key
如下:
基于传统的for/in
循环,会存在一些弊端「性能较差(既可以迭代私有的,也可以迭代公有的);只能迭代“可枚举、非Symbol类型的”属性…」
解决思路:获取对象所有的私有属性「私有的、不论是否可枚举、不论类型」
Object.getOwnPropertyNames(arr)
-> 获取对象非Symbol类型的私有属性「无关是否可枚举」Object.getOwnPropertySymbols(arr)
-> 获取Symbol类型的私有属性
获取所有的私有属性:
let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr));
基于ES6中的Reflect.ownKeys
代替上述操作「弊端:不兼容IE」:用于获取对象的所有自有属性的键,包括普通的字符串类型属性
和 Symbol 类型属性
let keys = Reflect.ownKeys(arr);
源码
可以私信我
原文地址:https://blog.csdn.net/qq_38951259/article/details/145149659
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!