前言:想要彻底了解react框架的原理及实现,学习源码是必经之路。在此学习期间,要最大程度的吸收编码思想、设计思想等。如果你也处于想要提升自己的阶段,那我们就一起交流一起学习吧~
一、什么是JSX
- JSX是一种语法糖,在编译过程中会通过babeljs转译成
React.createElement
语法 React.createElement
会返回一个React元素- React元素事实上是普通的JS对象,也就是虚拟DOM,用来描述展现在屏幕上的内容
ReactDOM
来确保浏览器中的真实DOM数据和React元素保持一致ReactDOM.render
把虚拟DOM转成真实DOM,插入到容器
二. JSX编译过程
// JSX:
<h1 className="title" style={{color:'red'}}>hello</h1>
// React17以前的编译结果:
React.createElement("h1", {
className: "title",
style: { color: 'red' }
}, "hello")
// React17以后的编译结果:
// 如果使用jsx,会自动引入'react/jsx-runtime'包
// 用来解除React17之前 import React from 'react'引发的:开发时eslint错误,运行时React undefined
// React.createElement ==> _jsx
import { jsx as _jsx } from 'react/jsx-runtime';
let element = _jsx("h1", {
className: "title",
style: { color: 'red' }
}, "hello")
// 返回的结果
{ type:'h1', props:{ className: "title", style: { color: 'red' } }, children:"hello" }
三.实现思路
1. 实现React.createElement()
1)参照React提供的createElement
方法,我们创建自己的createElement方法。
入参为:元素类型type(div/span)、配置config(key/ref)、子元素children。children可能存在多个,因此对children进行判断,当子元素有多个时,应存放进数组。
2)为方便后续进行dom-diff比较,将基本类型包装为对象。创建工具函数wrapToVdom
,入参为:elemment。此时考虑入参为string/number类型时,包装为对象再被返回。
3)包装原本createElement中返回的元素
2. 实现ReactDOM.render()
1)创建渲染函数render
,入参:vdom、container。函数内部要进行挂载
2)创建挂载函数mount
,入参:vdom、container。
3)创建createDOM
方法,入参:vdom。把虚拟DOM转为真实DOM。首先判断vdom不存在的情况,其次对根据虚拟DOM类型的不同做不同的处理:
-
REACT_TEXT:创建文本节点 createTextNode
-
普通元素:创建元素createElement。处理内部属性。属性后续都可能更新。因此需要
更新属性方法updateProps
。判断属性 -
react元素:直接挂载
-
数组:遍历数组,把每一项挂载
四. 实现JSX
1.创建项目
create-react-app react-basic
cd react-basic
yarn add cross-env
2. src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
let element1 = (
<div className="title" style={{ color: "red" }}>
<span>hello</span>world
</div>
);
console.log(JSON.stringify(element1, null, 2));
// ReactDOM.render负责渲染,把虚拟的react元素变成真实DOM渲染/插入到DOM容器内
// bable并没有把JSX编译成虚拟DOM,而是把JSX编译成了React,createElement的方法调用
// 在浏览器执行的时候才会执行React.createElement,才会生成虚拟DOM
ReactDOM.render(element1, document.getElementById("root"));
3. src/constants.js
export const REACT_TEXT = Symbol('REACT_TEXT');
export const REACT_ELEMENT = Symbol('react.element');
4. src/utils.js
import { REACT_TEXT } from "./constants";
export function wrapToVdom(element) {
return typeof element === "string" || typeof element === "number"
? { type: REACT_TEXT, props: { content: element } }
: element; }
5. src/react.js
import { wrapToVdom } from "./utils";
import { REACT_ELEMENT } from "./constants";
/**
* 创建一个虚拟DOM == React元素
* @param {*} type 元素的类型 span div p
* @param {*} config 配置对象 className style
* @param {*} children 子元素,单个-对象/多个-数组
*/
function createElement(type, config, children) {
let ref; // 可以通过ref获取引用此元素
let key; // 子元素的key唯一标识
if (config) {
delete config.__source; // 删除暂时没用的属性 source:bable编译时产生的属性
delete config.__self;
ref = config.ref; // 取出ref key
delete config.ref; // 删除config中的ref key
key = config.key;
delete config.key;
}
let props = { ...config };
if (arguments.length > 3) {
// 如果入参多余3个,说明有多个子元素,截取后以数组形式保存
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
// 可能是React元素对象,也可能是string/number/null/und
props.children = wrapToVdom(children);
}
return {
$$typeof: REACT_ELEMENT,
type,
ref,
key,
props,
};
}
const React = { createElement, };
export default React;
6. src/react-dom.js
import { REACT_TEXT } from "./constants";
/**
* 把虚拟DOM变成真实DOM插入容器
* @param {*} vdom 虚拟DOM
* @param {*} container 容器
*/
function render(vdom, container) {
mount(vdom, container);
}
export function mount(vdom, container) {
let newDOM = createDOM(vdom);
container.appendChild(newDOM);
}
/** 把虚拟DOM转为真实DOM */
export function createDOM(vdom) {
if (!vdom) return null; // null、und也是合法的dom
let { type, props } = vdom;
let dom; // 真实DOM
if (type === REACT_TEXT) {
// 如果元素为文本,创建文本节点
dom = document.createTextNode(props.content);
} else {
dom = document.createElement(type);
}
// 处理属性
if (props) {
updateProps(dom, {}, props);
if (typeof props.children == "object" && props.children.type) {
mount(props.children, dom);
} else if (Array.isArray(props.children)) {
reconcileChildren(props.children, dom);
}
}
vdom.dom = dom;
return dom;
}
/**
* 把新的属性更新到真实DOM上
* @param {*} dom 真实DOM
* @param {*} oldProps 旧的属性对象
* @param {*} newProps 新的属性对象
*/
function updateProps(dom, oldProps={}, newProps={}) {
for (let key in newProps) {
if (key === 'children') {
continue; // 子节点另外处理
} else if (key === 'style') {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
} else {
dom[key] = newProps[key];
}
}
for(let key in oldProps){
if(!newProps.hasOwnProperty(key)){
dom[key] = null;
}
}
}
function reconcileChildren(childrenVdom, parentDOM) {
for (let i = 0; i < childrenVdom.length; i++) {
let childVdom = childrenVdom[i];
mount(childVdom, parentDOM);
}
}
const ReactDOM = {
render,
};
export default ReactDOM;