前言
本文将结合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) {//返回一个虚拟domreturn {}
}const React = {createElement
}export default React;
处理key和ref
我们的key和ref都写在了config中,因此我们需要单独把key和value单独抽出来,并且把他们从config中删除
//第一步,处理key和reflet key, refif (config) {key = config.key || nullref = config.ref || nulldelete config.keydelete config.ref}
处理props和children
我们通过源码发现,他把children属性以及config中的所有元素都放进了props属性中
第二步,就是将config中的所有元素都放入到props中
let props = {...config}
第三步,就是去处理children节点,这里又有三种情况
- 没有子节点
- 有一个子节点 —— 文本节点 / 元素节点
- 有多个子节点
//第二步,处理childrenif (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和reflet key, refif (config) {key = config.key || nullref = config.ref || nulldelete config.keydelete config.ref}// 第二步,就是将config中的所有元素都放入到props中let props = {...config}//第三步,处理childrenif (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) ; //统一转变成对象}//没有儿子,不需要去处理}//返回一个虚拟domreturn { //vnodekey,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 //真实domreturn dom
}function render(vnode, container) {//将虚拟dom转变成真实domlet dom = createDOM(vnode)//将真实dom挂载到container上container.appendChild(dom)}const ReactDOM = {render
}export default ReactDOM;
处理type,生成对应的元素节点
请你回头看一下我们生成的虚拟节点的结构
- key:用于React实现diff算法的
- ref:用于获取真实Dom
- type:元素类型
- props:元素配置(例如子节点、样式)
- $$typeof:元素的唯一标识
我们在上面做了一个优化,如果是文本的话,我们自己处理成了对象的数据结构
{type:REACT_TEXT,content:element
}
//将虚拟dom转变成真实dom的方法
function createDOM(vnode) { let { type, props, content } = vnodelet 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 } = vnodelet 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.childrenif (children) {changeChildren(children, Ndom)}}return Ndom}function render(vnode, container) {//将虚拟dom转变成真实domlet dom = createDOM(vnode)//将真实dom挂载到container上container.appendChild(dom)}const ReactDOM = {render
}export default ReactDOM;
总结
自此完成我们就基本了解了React是如何实现元素渲染到视图的流程