前言
本文将结合React的设计思想来实现元素的渲染,即通过JSX语法
的方式是如何创建为真实dom渲染到页面上,本文基本不涉及React的源码,但与React的实现思路是一致的,所以非常适合小白学习,建议跟着步骤敲代码,如有错误,请批评指正!
建议:
- 如果你不清楚JSX是一个什么东西或者不了解React的话,建议先到React官方文档跟着文档做小游戏的方式大致的了解JSX
- 如果你也想学习Vue的源码,也可以看下这篇博客,它与Vue的实现思路也是一致的,都是将虚拟DOM转变成真实DOM
- 不要太纠结每个方法是如何实现的,如果过于纠结就会陷入到无限递归循环的地狱中,看React源码也是这样的
官方文档
不妨先创建一个React项目试试:
npx create-react-app my-app
实现思路
这里我们仅探讨元素渲染的实现原理
React通过Babel将JSX语法的文件转译成React.createElement函数,调用React.createElement函数将JSX转变成虚拟Dom(也就是一个Vnode对象),再通过ReactDOM.render函数将虚DOM变成真实DOM挂载到页面上
- 实现React.createElement函数
- 实现Render函数
- 完成渲染展示到页面上
初始化项目
当你通过上面的方式创建出一个React项目,不妨先删除多余的文件,把他变成最简单的一个jsx文件
在这里,我仅仅保留一个文件
import React from 'react';
import ReactDOM from 'react-dom/client';
let element = <h1>Hello, world</h1>;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
element
);
如果你成功打印出来一个Hello, world,那么第一步就成功了
React.createElement
Babel的转译涉及AST语法树的知识,可以去看我之前的博客,这里不再赘述,我们这里直接讲Babel将jsx语法的文件转变成React.createElement函数调用并生成虚拟DOM的实现步骤。
虚拟Dom的数据结构
这里我们先查看React.createElement生成虚拟Dom的数据结构,这里有利于我们如果手写方法创建虚拟Dom。
我们直接打印虚拟Dom元素
import React from 'react';
import ReactDOM from 'react-dom/client';
let element = <h1>Hello, world</h1>;
console.log(element);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
element
);
可以看到,他的本质就是一个对象,Babel转译成createElement函数,调用之后返回了一个对象,这个对象就是虚拟Dom,里面有几个关键的值
也就是变成这个函数的调用
React.createElement("h1",{className:"title",style:{color:'red'}},"hello")
这个函数接受三个参数,
- 一个是元素的类型
- 第二个是元素的配置
- 第三个是元素的内容(可能不止是文本,也可能是一个元素节点)
关键键值
- key:用于React实现diff算法的
- ref:用于获取真实Dom
- type:元素类型
- props:元素配置(例如子节点、样式)
- $$typeof:元素的唯一标识
具体实现
前面说这个方法,接受三个参数
- 一个是元素的类型
- 第二个是元素的配置
- 第三个是元素的内容(可能不止是文本,也可能是一个元素节点)
import React from 'react';
import ReactDOM from 'react-dom';
let element2 = React.createElement("h1", {
className: "title",
style: {
color: 'red'
}
}, 'hello world','hi');
console.log(element2);
ReactDOM.render(
element2,
document.getElementById('root')
);
注意点1:你现在尝试在’hello world’后面再追加一个文本’hi’,你会发现当子节点有多个
的时候,他的props中的children属性会从一个字符串类型变成数组类型
,这一点很重要!
注意点2:如果你不是一个文本,而是一个元素对象,则是一个对象,如果是多个元素对象,则变成一个数组,里面是元素对象
import React from 'react';
import ReactDOM from 'react-dom';
let element2 = React.createElement("h1", {
className: "title",
style: {
color: 'red'
}
}, React.createElement("span", null, "hello"));
console.log(element2);
ReactDOM.render(
element2,
document.getElementById('root')
);
初始化函数
我们新建一个react.js文件,暴露这一个React对象,里面有一个 createElement函数,我们就是要实现使用这个函数返回一个虚拟dom
//接受三个参数,元素的类型、元素的配置、元素的节点
function createElement(type,config,children) {
//返回一个虚拟dom
return {
}
}
const React = {
createElement
}
export default React;
处理key和ref
我们的key和ref都写在了config中,因此我们需要单独把key和value单独抽出来,并且把他们从config中删除
//第一步,处理key和ref
let key, ref
if (config) {
key = config.key || null
ref = config.ref || null
delete config.key
delete config.ref
}
处理props和children
我们通过源码发现,他把children属性以及config中的所有元素都放进了props属性中
第二步,就是将config中的所有元素都放入到props中
let props = {...config}
第三步,就是去处理children节点,这里又有三种情况
- 没有子节点
- 有一个子节点 —— 文本节点 / 元素节点
- 有多个子节点
//第二步,处理children
if (props) {
//有多个儿子
if (arguments.length > 3) {
//多个儿子,就把他们变成一个数组
props.children = Array.prototype.slice.call(arguments, 2)
//有一个儿子 (1)文本 (2)元素
}else if(arguments.length === 3){
props.children = children;
}
//没有儿子,不需要去处理
}
``
处理 $$typeof
这个key是React用于标识元素的,我们创建一个stant.js
文件,用于暴露所有的标识类型
//用于标识元素
export const REACT_ELEMENT = Symbol('react.element')
export const REACT_TEXT = Symbol('react.text')
优化
在处理children节点的时候,当我们只有一个子节点并且是一个文本的时候,他是一个字符串类型的,我们统一处理成对象类型有利于后序做更新操作,通过toObject方法
import { REACT_TEXT } from "./stants";
export function toObject(element) {
return typeof element === 'string' || typeof element === 'number' ? {type:REACT_TEXT,content:element} : element
}
整体代码
react.js
//实现以下:
// let element2 = React.createElement("h1", {
// className: "title",
// style: {
// color: 'red'
// }
// }, React.createElement("span", null, "hello"));
import { REACT_ELEMENT } from "./stants"
import { toObject } from "./utils"
function createElement(type,config,children) {
if (config == null) {
config = {}
}
//第一步,处理key和ref
let key, ref
if (config) {
key = config.key || null
ref = config.ref || null
delete config.key
delete config.ref
}
// 第二步,就是将config中的所有元素都放入到props中
let props = {...config}
//第三步,处理children
if (props) {
//有多个儿子
if (arguments.length > 3) {
//多个儿子,就把他们变成一个数组
props.children = Array.prototype.slice.call(arguments, 2).map(toObject)
//有一个儿子 (1)文本 (2)元素
}else if(arguments.length === 3){
props.children = toObject(children) ; //统一转变成对象
}
//没有儿子,不需要去处理
}
//返回一个虚拟dom
return { //vnode
key,
ref,
$$typeof:REACT_ELEMENT,
props,
type: type,
}
}
const React = {
createElement
}
export default React;
在index.js中引入我们自己的react文件来试试吧,到这里我们就实现了 React.createElement函数,生成了虚拟Dom
React.render函数
这个函数是将虚拟dom转变成真实dom的关键函数,这里我们接受两个参数,一个是虚拟dom,第二个是挂载节点,也就是实现这个函数
ReactDOM.render(
element2,
document.getElementById('root')
);
初始化函数
//将虚拟dom转变成真实dom的方法
function createDOM(vnode) {
let dom //真实dom
return dom
}
function render(vnode, container) {
//将虚拟dom转变成真实dom
let dom = createDOM(vnode)
//将真实dom挂载到container上
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM;
处理type,生成对应的元素节点
请你回头看一下我们生成的虚拟节点的结构
- key:用于React实现diff算法的
- ref:用于获取真实Dom
- type:元素类型
- props:元素配置(例如子节点、样式)
- $$typeof:元素的唯一标识
我们在上面做了一个优化,如果是文本的话,我们自己处理成了对象的数据结构文章来源:https://www.toymoban.com/news/detail-683131.html
{
type:REACT_TEXT,
content:element
}
//将虚拟dom转变成真实dom的方法
function createDOM(vnode) {
let { type, props, content } = vnode
let Ndom;
//1、判断type是什么类型的,是文本还是元素并生成对应的节点
if (type === REACT_TEXT) { //如果是一个文本类型的
Ndom = document.createTextNode(content) //注意:我们在前面已经把所有的文件节点处理为一个对象类型的了
} else {
Ndom = document.createElement(type) //div
}
//2、处理属性 {children style:{color:red,fontsize:16px} className="title" }
if (props) {
console.log("props",props)
//为了后续处理更新操作
updateProps(Ndom, {}, props)
}
//3、处理子节点
return Ndom
}
处理属性
//初始化和更新props的方法
function updateProps(dom, oldProps, newProps) {
//初始化
if (newProps) {
//遍历新的属性对象
for (let key in newProps) {
if (key === 'children') {
continue
} else if (key === 'style') { //如果是style的话就一个个追加进去
let styleObj = newProps[key]
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
} else { //例如className就直接放上去即可
dom[key] = newProps[key]
}
}
}
//更新操作,如果有旧节点
if (oldProps) {
//旧的属性在新的属性中没有,则删除
for (let key in oldProps) {
if(!newProps[key]){
dom[key] = null
}
}
}
//2、处理属性 {children style:{color:red,fontsize:16px} className="title" }
if (props) {
//为了后续处理更新操作
updateProps(dom, {}, props)
}
处理子节点
//处理子节点
//接收两个参数,一个是子节点,另一个是挂载节点
function changeChildren(children, dom) {
//有一个儿子的情况 对象
if (typeof children == 'object'&& children.type ) {
render(children, dom) //递归调用
//有多个儿子的情况 数组
} else if (Array.isArray(children)) {
//循环处理
children.forEach(child =>
render(child, dom)
)
}
}
整体代码
import { REACT_TEXT } from "./stants"
//初始化和更新props的方法
function updateProps(dom, oldProps, newProps) {
//初始化
if (newProps) {
//遍历新的属性对象
for (let key in newProps) {
if (key === 'children') {
continue
} else if (key === 'style') { //如果是style的话就一个个追加进去
let styleObj = newProps[key]
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
} else { //例如className就直接放上去即可
dom[key] = newProps[key]
}
}
}
//更新操作,如果有旧节点
if (oldProps) {
//旧的属性在新的属性中没有,则删除
for (let key in oldProps) {
if (!newProps[key]) {
dom[key] = null
}
}
}
}
//处理子节点
//接收两个参数,一个是子节点,另一个是挂载节点
function changeChildren(children, dom) {
//有一个儿子的情况 对象
if (typeof children == 'object'&& children.type ) {
render(children, dom) //递归调用
//有多个儿子的情况 数组
} else if (Array.isArray(children)) {
//循环处理
children.forEach(child =>
render(child, dom)
)
}
}
//将虚拟dom转变成真实dom的方法
function createDOM(vnode) {
let { type, props,content } = vnode
let Ndom; //新的dom节点
//1、判断type是什么类型的,是文本还是元素并生成对应的节点
if (type === REACT_TEXT) { //如果是一个文本类型的
Ndom = document.createTextNode(content) //注意:我们在前面已经把所有的文件节点处理为一个对象类型的了
} else {
Ndom = document.createElement(type) //div
}
//2、处理属性 {children style:{color:red,fontsize:16px} className="title" }
if (props) {
//为了后续处理更新操作
updateProps(Ndom, {}, props)
//3、处理子节点
let children = props.children
if (children) {
changeChildren(children, Ndom)
}
}
return Ndom
}
function render(vnode, container) {
//将虚拟dom转变成真实dom
let dom = createDOM(vnode)
//将真实dom挂载到container上
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM;
总结
自此完成我们就基本了解了React是如何实现元素渲染到视图的流程文章来源地址https://www.toymoban.com/news/detail-683131.html
到了这里,关于【React源码实现】元素渲染的实现原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!