项目总结构:
├─ 📁core
│ ├─ 📄React.js
│ └─ 📄ReactDom.js
├─ 📁node_modules
├─ 📁tests
│ └─ 📄createElement.spec.js
├─ 📄App.js
├─ 📄index.html
├─ 📄main.js
├─ 📄package-lock.json
├─ 📄package.json
└─ 📄pnpm-lock.yaml
原生js怎么写?
javascript
const dom = document.createElement("div")
dom.id="app"
document.querySelector('#root').append(dom)
const textNode = document.createTextNode("")
textNode.nodeValue = "app"
dom.append(textNode)
为什么需要虚拟dom?
通过以上原生写法可以看出,虚拟dom可以简化开发,使代码更加框架、结构化,清晰可读易于维护。
框架层面:频繁的dom操作会一直导致浏览器重排和重绘,会严重影响性能。
原生层面:相对于原生层面虚拟dom对性能提示微乎其微,并且在复杂情况会损耗性能。
当然虚拟dom还有个非常重要的作用就是跨端运行。
面试怎么答?
- 减少浏览器重排和重绘(框架层面、原生层面)
- 跨平台运行,不局限于浏览器
- 减少心智负担,提高开发效率
当然不能干巴巴的把这几点甩出去,记得拓展。
极简版React内核
此处代码实现了一个极简的创建虚拟dom和虚拟dom转真实dom
/core/React.js
javascript
//创建文本对象虚拟dom
function createTextNode(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
//创建虚拟dom对象
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
return typeof child === "string" ? createTextNode(child) : child;
}),
},
};
}
//渲染器,vdom->tdom
function render(el, container) {
const dom =
el.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(el.type);
// id class
Object.keys(el.props).forEach((key) => {
if (key !== "children") {
dom[key] = el.props[key];
}
});
const children = el.props.children;
children.forEach((child) => {
render(child, dom);
});
container.append(dom);
}
const React = {
render,
createElement,
};
export default React;
可以看到以上代码中createElement
函数就实现了一个极简版的虚拟dom,里面有三个元素分别是:
type
:类型props
:传入的值children
:子节点
而render
函数则实现了一个极简版的渲染器,用于将虚拟dom转化成真实dom,传入的值分别是:
el
:虚拟dom对象container
:具体在哪个真实dom节点渲染
创建虚拟dom对象
/App.js
通过上面定义的createElement
函数来创建一个虚拟dom对象app
并将其导出
javascript
import React from './core/React.js';
const App = React.createElement("div", { id: "app" }, "hi- ", "mini-react");
export default App
极简React渲染组件
定义了一个createRoot
函数,通过传入的渲染容器container
和虚拟dom对象App
来将虚拟dom元素渲染到真实dom上。
/core/ReactDom.js
javascript
import React from "./React.js";
const ReactDOM = {
createRoot(container) {
return {
render(App) {
React.render(App, container);
},
};
},
};
export default ReactDOM;
创建ReactDOM
/main.js
javascript
import ReactDOM from "./core/ReactDom.js";
import App from "./App.js";
ReactDOM.createRoot(document.querySelector("#root")).render(App);
使用vitest单元测试验证
控制台运行npm i vitest
下载依赖。
然后再package.json
中自定义测试脚本
/package.json
javascript
{
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vitest": "^1.1.3"
}
}
然后创建tests
文件夹并编写测试文件,具体路径于代码如下:
/tests/createElement.spec.js
javascript
import React from "../core/React";
import { it, expect, describe } from "vitest";
describe("createElement", () => {
it("props is null", () => {
const el = React.createElement("div", null, "hi");
expect(el).toMatchInlineSnapshot(`
{
"props": {
"children": [
{
"props": {
"children": [],
"nodeValue": "hi",
},
"type": "TEXT_ELEMENT",
},
],
},
"type": "div",
}
`)
});
it("should return element vdom", () => {
const el = React.createElement("div", {id:"id"}, "hi");
expect(el).toMatchInlineSnapshot(`
{
"props": {
"children": [
{
"props": {
"children": [],
"nodeValue": "hi",
},
"type": "TEXT_ELEMENT",
},
],
"id": "id",
},
"type": "div",
}
`)
});
});
创建完成后在控制台输入npm test
即可运行测试脚本。
在上面代码中,describe
用于创建测试组,每个it
是测试组内的单个测试单元,而it
内就是具体测试内容。
这里使用的是toMatchInlineSnapshot
快照测试,用于比对el
结果是否与快照函数toMatchInlineSnapshot
内容一致,如果一致则测试通过。
总结
createRoot
用于将虚拟dom渲染成真实dom
createElement
用于创建虚拟dom对象
render
是createRoot
的内核
贴上main.js
:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="main.js"></script>
</body>
</html>