网上那些八股文,都是别人对react理解后整理的描述,你去背没有任何意义!
只有自己写一遍,收获的才是自己的!用自己的理解总结出来的,才是你真的掌握了!
本文将带你一步步手写实现React,希望可以帮到你。
一、React.createElement
前面说过jsx编译后会转换为React.createElement或者jsx/jsxs格式的递归调用函数 其实在此之前还有一个ast转化过程(babel-preset-react-app做的),这里省掉ast,直接从函数调用开始实现
1 前言
我们写的jsx,其实就是转换后的React.createElement,所以我们可以直接写React.createElement,效果也是一样的!
jsx
/*
* @Description : 手写react-dom 前言
* @Author : zhangyuru
* @FilePath : react-dom.js
*/
import React from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
// 我们写的jsx 等同于下面的 React.createElement
// let element = (
// <div className="title" style={{ color: "#fff", background: "#000" }}>
// <span>hello</span>world
// </div>
// );
// 等同于上面的jsx,效果是一样的
let element = React.createElement(
"div",
{
className: "title",
style: {
color: "#fff",
background: "#000",
},
},
React.createElement("span", null, "hello"),
"world"
);
console.log(JSON.stringify(element, null, 2));
root.render(element);
渲染流程图
2 准备工作
1) 功能拆分
为了后面的更多手写实现的扩展,所以需要拆分功能,分别写在不同的文件中
你需要创建以下几个文件
constants.js
--- 存放公共常量utils.js
--- 存放工具函数react.js
--- react功能的核心react-dom.js
--- react-dom功能的核心
2) constants.js
jsx
/*
* @Description : 手写React的常量存放
* @Author : zhangyuru
* @FilePath : contants.js
*/
/* 表示这是一个文本类型的元素 在源码里没有这样一个类型 */
export const REACT_TEXT = Symbol("REACT_TEXT");
3) utils.js
编写第一个工具函数:wrapToVdom,将传入的属性转为vdom对象
jsx
/*
* @Description : 手写React的工具函数
* @Author : zhangyuru
* @FilePath : utils.js
*/
import { REACT_TEXT } from "./contants";
/**
* @description : 将传入的数据转为vdom对象
* @param { } element
* @return { } vdom对象
*/
export function wrapToVdom(element) {
if (typeof element === "string" || typeof element === "number") {
// 虚拟DOM.props.content就是此元素的内容
return { type: REACT_TEXT, props: { content: element } };
} else {
return element;
}
}
3.createElement
react.js中
jsx
/*
* @Description : 手写react
* @Author : zhangyuru
* @FilePath : react.js
*/
import { wrapToVdom } from "./utils";
/**
* @description : 实现createElement方法
* @param { } type 元素类型
* @param { } config 元素属性
* @param { } children 子元素
* @return { } vdom
*/
function createElement(type, config, children) {
// 将接受的第二个参数转为props
let props = { ...(config || {}) };
// 对children的处理
if (arguments.length > 3) {
// 如果参数个数大于3个,说明不止一个子节点,截取子节点并用wrapToVdom处理
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
// 只有一个子节点的情况下,继续用wrapToVdom处理
if (typeof children !== "undefined") props.children = wrapToVdom(children);
}
// 返回值后面会继续扩展
return { type, props };
}
const React = {
createElement,
};
export default React;
二、ReactDOM.render
1.创建render函数
render函数接收两个参数:vdom,container
react-dom.js中
jsx
/*
* @Description : 手写react-dom
* @Author : zhangyuru
* @FilePath : react-dom.js
*/
import { REACT_TEXT } from "./contants";
/**
* @description : render方法,创建真实dom,插入到指定容器中
* @param { } vdom
* @param { } container
* @return { } void
*/
function render(vdom, container) {
let newVdom = createDom(vdom); // 调用此方法创建真实dom
container.appendChild(newVdom); // 将真实dom插入到容器中
}
2.createDom
接收vdom,返回真实dom
jsx
import { REACT_TEXT } from "./contants";
/**
* @description : 根据vdom的描述 创建真实dom
* @param { } vdom
* @return { } 真实dom元素
*/
function createDom(vdom) {
let { type, props } = vdom;
let 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) {
render(props.children, dom); // 儿子是对象 【只有一个儿子】递归调render继续创建
}
if (Array.isArray(props?.children)) {
reconcileChidren(props.children, dom); // 儿子是数组 需要遍历再递归创建
}
}
vdom.dom = vdom; // 后面如果设置了ref 就把这个dom给ref
return dom;
}
3.updateProps
根据vdom的描述,挂载/更新元素的属性
jsx
/**
* @description : 根据vdom的描述,挂载/更新元素的属性
* @param { } dom
* @param { } oldProps 暂时没用,后面会用作diff
* @param { } newProps 新的props
* @return { } void
*/
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === "children") {
continue; // 暂时跳过,后面会单独处理子节点
}
if (key === "style") {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
} else {
dom[key] = newProps[key];
}
}
}
4.reconcileChidren
循环创建/更新子节点
jsx
/**
* @description : 循环创建/更新子节点
* @param { } children
* @param { } parentDom
* @return { } void
*/
function reconcileChidren(children, parentDom) {
for (let i = 0; i < children.length; i++) {
let childVdom = children[i];
render(childVdom, parentDom);
}
}
三、使用自己的API
1.切换引用
jsx
/*
* @Description : 手写react + react-dom
* @Author : zhangyuru
* @FilePath : index.js
*/
// import React from "react";
// import ReactDOM from "react-dom/client";
// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";
// 模拟vdom创建
let element = React.createElement(
"div",
{
className: "title",
style: {
color: "#fff",
background: "#000",
height: "200px",
},
},
React.createElement("span", null, "hello"),
"world"
);
// 实现渲染
ReactDOM.render(element, document.getElementById("root"));