jsx介绍
jsx 是 React.createElement(component, props, ...children) 函数的语法糖。最终会通过babel转换变成React.createElement的形式。
可以使用babel进行转化看看,如下:
react的元素渲染
react的render函数执行流程大致如下:
- 接收的vdom变成真实dom
- 把vdom的属性更新到dom上
- 将vdom的儿子变成真实dom挂载到自己的dom上,dom.appendChild
- 将自己挂载到容器root上
看看下面这段代码
import React from "react";
import ReactDOM from "react-dom/client";
let element = (
<div className="active" style={{ color: "red" }}>
<span>hello world</span>
</div>
);
ReactDOM.render(element, document.getElementById("root"));
它的执行过程如下:
- 通过babel将element变成React.createElement的形式
- React.createElement内部会返回vdom(虚拟dom,是一个js对象)
- 将这个vdom传递给render函数
- 然后执行render内部的步骤(上面讲过)
可以直接打印element看到vdom的形式
console.log(element);
也可以通过json.stringfy看的清楚点
console.log(JSON.stringify(element, null, 4));//4指定的缩进
React.createElement实现
简单实现React.createElement函数
//react.js
/**
* 创建vdom
* @param {*} type 标签类型
* @param {*} config 传入的配置,例如类名,行内样式
* @param {...any} children 孩子
* @returns
*/
function createElement(type, config, ...children) {
//如果孩子只有一个节点,那么vdom的children是对象形式,而不是数组形式,例如上面的json.strinfy的结果
if (children.length === 1) {
children = children[0];
}
let props = { ...config };
props.children = children;
return {
type,
props,
};
}
const React = { createElement };
export default React;
校验,将上面通过babel转换得到的React.createElement用自己实现的createElement实现
import React from "./react";
let element2 = React.createElement(
"div",
{
className: "active",
style: {
color: "red",
},
},
React.createElement("span", null, "hello world")
);
console.log(JSON.stringify(element2, null, 4));
结果如下:
render函数实现
简单实现render函数,render函数内部流程:
- 接收的vdom变成真实dom
- 把vdom的属性更新到dom上
- 将vdom的儿子变成真实dom挂载到自己的dom上,dom.appendChild
- 将自己挂载到容器root上
render函数的四步会变成如下函数的实现过程
首先通过createDOM函数接收到的vdom转成真实dom,然后createDOM内部通过updateProps将dom上面的属性更新到刚刚创建的dom上,接着需要通过reconcileChildren将儿子变成真实dom挂载到父dom上。代码如下:
//ReactDOM.js
//渲染流程
// * 1.接收的vdom变成真实dom
// * 2.把vdom的属性更新到dom上
// * 3.将vdom的儿子变成真实dom挂载到自己的dom上,dom.appendChild
// * 4.将自己挂载到容器root上
/**
* 将vdom渲染成真实dom并挂载容器上面
* @param {*} vdom 虚拟dom对象
* @param {*} container 挂载容器
*/
function render(vdom, container) {
const dom = createDOM(vdom);
container.appendChild(dom);
}
/**
* 虚拟dom变成真实dom
* @param {*} vdom
*/
function createDOM(vdom) {
//是数字或字符串,直接返回一个文本节点
if (typeof vdom === "string" || typeof vdom == "number") {
return document.createTextNode(vdom);
}
//1.是一个vdom对象,变成真实dom
let { type, props } = vdom;
const dom = document.createElement(type);
//2.更新vdom上面的属性到dom上
updateProps(dom, props);
//3.将vdom的儿子变成真实dom挂载到自己的dom上
if (
typeof props.children === "string" ||
typeof props.children === "number"
) {
dom.textContent = props.children;
} else if (typeof props.children === "object" && props.children.type) {
render(props.children, dom);
} else if (Array.isArray(props.children)) {
reconcileChildren(props.children, dom);
} else {
//到这里都是抛出错误,
throw new Error();
}
return dom;
}
/**
* 更新vdom上面的属性到dom上
* @param {*} dom 真实dom
* @param {*} newProps vdom上面的属性
*/
function updateProps(dom, newProps) {
for (let key in newProps) {
//遇到儿子先跳过,在后面处理
if (key === "children") continue;
if (key === "style") {
let styleObj = newProps[key];
for (let style in styleObj) {
dom.style[style] = styleObj[style];
}
} else {
dom[key] = newProps[key];
}
}
}
/**
* 将儿子变成真实dom并挂载到parentDom上面
* @param {*} childrenVdom 儿子的vdom数组
* @param {*} parentDom 儿子需要挂载的位置,父容器
*/
function reconcileChildren(childrenVdom, parentDom) {
for (let i = 0; i < childrenVdom.length; i++) {
render(childrenVdom[i], parentDom);
}
}
const ReactDOM = {
render,
};
export default ReactDOM;
校验
//index.js
//引入自己创建的render
import React from "./react";
import ReactDOM from "./react-dom";
let element = (
<div className="active" style={{ color: "red" }}>
<span>hello world</span>
</div>
);
ReactDOM.render(element, document.getElementById("root"));
结果成功渲染
添加背景颜色校验
let element = (
<div className="active" style={{ color: "red", backgroundColor: "skyblue" }}>
<span>hello world</span>
</div>
);
函数组件渲染
函数组件转成vdom对象跟普通元素有点不同,它的type值是一个函数,所以需要在上面的创建真实dom需要做进一步判断
函数组件的vdom对象如图:
function Content(props) {
return (
<div className="active">
<span style={{ color: "red" }}>name:</span>
<span>{props.name}</span>
{props.children}
</div>
);
}
let element = (
<Content name="zs">
<div>
<span style={{ color: "red" }}>age</span>:18
</div>
</Content>
);
console.log(element);
createDOM做如下修改
...
//1.是一个vdom对象,变成真实dom
let { type, props } = vdom;
let dom;
//如果是一个函数组件
if (typeof type === "function") {
return renderFunctionComponent(vdom);
} else {
dom = document.createElement(type);
}
...
renderFunctionComponent函数
/**
* 将一个函数组件的vdom转成真实dom并返回
* @param {*} vdom
*/
function renderFunctionComponent(vdom) {
let { type, props } = vdom;
let renderVdom = type(props); //函数执行后通过return返回的vdom;
return createDOM(renderVdom); //renderVdom进行渲染并返回
}
校验
function Content(props) {
return (
<div className="active">
<span style={{ color: "red" }}>name:</span>
<span>{props.name}</span>
{/* 当外部只有一个子孩子时,children不是一个数组,可以直接使用props.children */}
{props.children}
</div>
);
}
let element = (
<Content name="zs">
<div>
<span style={{ color: "red" }}>age</span>:18
</div>
</Content>
);
ReactDOM.render(element, document.getElementById("root"));
类组件渲染
类组件的vdom的type值是一个类,所以通过typeof获得的是'function',所以需要在createDOM里面进一步修改
打印查看类组件的vdom
class Count extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<div>
<span>count</span>
<span>{this.state.count}</span>
</div>
</div>
);
}
}
let element = <Count name="zs"></Count>;
console.log(element);
从上图可以看到type是一个类
createDOM修改如下
...
//如果是一个函数组件或函数组件
if (typeof type === "function") {
//判断是类组件还是函数组件
if (type.isReactComponent) {
return renderClassComponent(vdom);
} else {
return renderFunctionComponent(vdom);
}
} else {
dom = document.createElement(type);
}
...
类组件的type是一个类,这个isReactComponent是类的一个静态变量,用来区分是函数组件还是类组件。renderClassComponent实现类组件变成一个真实dom
updateProps也需要做相应的改变,因为类组件可能绑定事件,需要多加一个else if判断,将这个事件绑定到dom元素上,这里只是简单是实现一下,而react内部真正使用的是合成事件,会将事件代理到document上面。
/**
* 更新vdom上面的属性到dom上
* @param {*} dom 真实dom
* @param {*} newProps vdom上面的属性
*/
function updateProps(dom, newProps) {
for (let key in newProps) {
//遇到儿子先跳过,在后面处理
if (key === "children") continue;
if (key === "style") {
let styleObj = newProps[key];
for (let style in styleObj) {
dom.style[style] = styleObj[style];
}
} else if (key.startsWith("on")) {
//绑定一个事件
dom[key.toLocaleLowerCase()] = newProps[key];
} else {
dom[key] = newProps[key];
}
}
}
renderClassComponent函数实现
/**
* 将一个类组件转成真实dom并返回
* @param {*} vdom
*/
function renderClassComponent(vdom) {
let { type, props } = vdom;
let classInstance = new type(props);
//获取vdom
let renderVdom = classInstance.render();
//转成真实dom
let dom = createDOM(renderVdom);
//为了后面更新组件需要用到真实dom
classInstance.dom = dom;
return dom;
}
React.Component实现
//Component.js
class Component {
//用于标识组件是类组件
static isReactComponent = true;
constructor(props) {
this.props = props;
}
setState(partialState) {
let oldstate = this.state;
this.state = { ...oldstate, ...partialState };
//重新调用render方法获取新的vdom
let newVdom = this.render();
//更新dom节点
updateClassComponent(this, newVdom);
}
}
//这里直接替换掉元节点,没有使用diff,也没有异步更新
function updateClassComponent(classInstance, newVdom) {
//这里就可以拿到旧的dom
let oldDom = classInstance.dom;
let newDom = createDOM(newVdom);
oldDom.parentNode.replaceChild(newDom, oldDom);
classInstance.dom = newDom;
}
export default Component;
校验
//index.js
import React from "./react";
import ReactDOM from "./react-dom";
class Count extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
};
render() {
return (
<div>
<div>
<span>count:</span>
<span>{this.state.count}</span>
</div>
<button onClick={this.handleClick}>+</button>
<div>name:{this.props.name}</div>
</div>
);
}
}
ReactDOM.render(element, document.getElementById("root"));
当点击了“+”后,会触发handleClick函数,因为这里没有使用异步更新,所以这里结果是同步的。