React学习-渲染的奥秘
相信深入学习过 React 或 Vue 的友友们或多或少都知道所谓框架的渲染,其实就是 Render 函数的作用,本文章就是针对该函数怎么实现的展开说明。
引入 jxs 背后的秘密
为什么这里要先说明jsx呢?
- 现在的 React 很多写法都是跟 jsx 一起的,然后再利用 babel 插件将 jsx 编译成浏览器懂得的js代码。
还不清楚jsx是什么的朋友可以接着往下看,有说明的。
创建一个 React 项目
终端输入
create-react-app react_demo
当你打开你创建的项目的时候,其实里面的很多文件都是没啥用的,你大可将 src 目录下的全部文件删除到只剩下 index.js 文件,这个文件才是整个项目的入口文件。
说明 index.js 中的细节,并拆解jsx
此时的 index.js 文件
// src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(domcument.getElementById("root"));
root.render(<h1>Hello World</h1>)
想必大家发现了,为什么在 render 函数里面可以传一个html式的实参呢?其实这就是 jsx 语言了。 里面的 实参 其实会等价于:
root.render(<h1>Hello World</h1>);
// 等价于
const element = React.createElement("h1",{},"Hello World");
root.render(element); // 等下我们会重写 render 函数
// 也就是说
<h1>Hello World<h1>
// 会被解析成
const Vnode = {
type:"h1",
props:{},
children
}
// 这也是大家耳熟能详的虚拟dom
上面引入了 React.createElement 方法,这是 react 内部实现的一个实现的一个办法,现在我们就来实现一下这个办法。
实现 React.createElement
确认函数的参数
在 src 目录下创建 react.js 文件。
// src/index.js
const element = React.createElement("h1",{},"Hello World");
由上述代码可以知道 createElement 函数传进去了三个实参,所以:
// src/react.js
function createElement(type,config = {}, children){
...
}
函数中的细节
// src/react.js
function ReactElement(type,props){
const element = {type,props}
return element;
}
function createElement(type,config = {}, children){
let proName;
// React 源码中的虚拟dom是props字段
const props = {};
for(proName in config) {
props[propName] = config[propName];
}
// aguments.length 获取参数的数量,减二获取children的数量
const childrenLength = aguments.length - 2;
if(childrenLength === 1){
props.children = children;
}else if{
props.children = Array.from(aguments).slice(2);
}
return ReactElement(type,props) // 返回出虚拟dom的格式
}
export default {creatElement};
渲染类组件
// src/react.js
class Component {
static isReactCompont = true;
constructor(props) {
this.props = props;
}
}
...
exprot default {createElement,Component};
上述代码中 isReactCompont 起到至关重要的作用,在下面重写的 render 函数中,它用来识别是不是类组件。
实现 render 函数
在此之前,请允许我修改一下 index.js 内的代码:
// src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
const element = React.createElement("h1",{ className: "title" }, "hello World");
ReactDOM.render(element,document.getElementById("root"))
大家可以对比一下这跟React脚手架生成的代码有什么区别,颇有异曲同工之妙。
创建 react-dom.js 文件,确定参数
在src目录下创建 react-dom.js 文件
// src/react-dom.js
function render(element,parentNode){
...
}
export default { render }
观察 index.js 中的render函数可以知道,实际上传给该函数的参数是 渲染元素 和 父元素
render 函数中的细节
相信熟悉react的友友们都知道,我们在所写的要渲染的代码,无疑就是 字符串、数字、函数式组件、类组件。其实 render 函数也就是分情况来编写代码逻辑,根据要渲染的东西类型的不同来进行不同的加工。
// react-dom.js
function render(element, parentNode) {
if (typeof element == "string" || typeof element == "number") {
return parentNode.appendChild(document.createTextNode(element));
}
let type, props;
type = element.type; //Welcome1
props = element.props;
if (type.isReactComponent) {
let returnedElement = new type(props).render();
type = returnedElement.type;
props = returnedElement.props;
} else if (typeof type == "function") {
let returnedElement = type(props);
type = returnedElement.type;
props = returnedElement.props;
}
let domElement = document.createElement(type); //span
for (let propName in props) {
if (propName == "className") {
domElement.className = props[propName];
} else if (propName == "style") {
let styleObj = props[propName];
let cssText = Object.keys(styleObj)
.map((attr) => {
return `${attr.replace(/([A-Z])/g, function () {
return "-" + arguments[1].toLowerCase();
})}:${styleObj[attr]}`;
})
.join(";");
domElement.style.cssText = "color:red;font-size:50px";
} else if (propName === "children") {
let children = Array.isArray(props.children)
? props.children
: [props.children];
children.forEach((child) => render(child, domElement));
} else {
domElement.setAttribute(propName, props[propName]);
}
}
parentNode.appendChild(domElement);
}
export default { render };
分情况讨论:
- 如果所渲染的是 字符串 或 数字,直接渲染。
- 如果所渲染的是 类组件:isReactComponent,new 一个实例进行渲染
- 如果所渲染的是 函数式组件:则将参数直接传给 type(因为它是一个函数)
- 接下来就是 children 们的处理了,因为 jsx 中书别的类名(className)跟传统的 class 有所不同,所以先转换一下。在 jsx 中样式用的懂是小驼峰命名,所以也要用正则表达式转换一下。最后万一children里面还有children,很明显递归它!
- 最后把所处理好的 domElement 插入刚刚传进来的父节点就万事大吉了。