react 学习(一) 实现简版虚拟 dom 和挂载
react 学习(一) 实现简版虚拟 dom 和挂载
楼主最近入职新单位了,恰好新单位使用的技术栈是 react
,因为之前一直进行的是 vue2/vue3
和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试的也都能答出来。但对于 react
只是有一定的了解,没有真实的学习过实现,虽然之前也看过一些文章,但是只停留在表面,因为别人这么写了,也就下意识的认为是这样。本次正好配合工作的契机,我们从零开始学习一下,使用的话呢就简单一过,相信大家也都用过或者看完官网也都了解了。如果您是大佬,欢迎批评指正;如果您是初级选手,希望能够一起学习。
初始化项目
我们借助脚手架实现开发环境,内部使用的库用自己开发的。
-
npx create-react-app react-dome1 (当然也可以全局安装脚手架) public
目录只留下index.html
,src
目录下只留下index.js
-
修改 scripts 命令 我们需要使用旧的转换方式,这样我们可以自己实现 createElement
方法
// cross-env 需要自己安装
scripts": {
"start": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts start",
"build": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts build",
"test": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts test",
"eject": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts eject"
},
“”
react 17
引入了新的jsx
转换特性,因为之前的开发,即使页面中未直接使用React
,但是也要引入,这是因为babel
在转译之后会触发React.createElement
,如果不引入会报错,但是引入了可能也会触发eslint
的报错,引入但是未使用。新特性可以单独使用JSX
而无需引入React
。
新特性一些好处
-
使用全新的转换,你可以单独使用 JSX
而无需引入React
。 -
根据你的配置, JSX
的编译输出可能会略微改善bundle
的大小。 -
它将减少你需要学习 React
概念的数量,以备未来之需
之前的转换方式
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
====================================
import React from 'react';
function App() {
return React.createElement('h1', null, 'Hello world');
}
新特性转换方式
function App() {
return <h1>Hello World</h1>;
}
==================================
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
实现 React.createElement
我们先看下原生 createElement
的返回结果
// src/index.js
import React from 'react'
const jsx = <h1 className='title' style={{color: 'red'}}>hello</h1>
console.log(jsx)
我们看到返回了对象,几个重要属性,$$typeof
, props
, type
。我们实现下自己的 createElement
函数。
定义类型常量
// src/constants.js
// react 内的元素都是这个类型
export const REACT_ELEMENT = Symbol("react.element");
// react 文本类型
export const REACT_TEXT = Symbol("react.text");
实现 createElement
// src/react.js
// 这三个参数是 babel 解析完,调用React.createElement 传入的,从第三个参数开始都是儿子
function createElement(type, config, children) {
if(config) {
// 这里可写可不写,就是为了简化下我们自己写的,只把必要的返回,没用的参数越少越清晰嘛
delete config.__source
delete config.__self
}
const props = {...config}
if (arguments.length > 3) {
// 有多个儿子
props.chidlren = Array.prototype.slice.call(arguments, 2)
} else if (argument.length === 3) {
// 只有一个子,直接赋值
props.children = children
}
return {
$$typeof: REACT_ELEMENT,
type,
props
}
}
const React = {
createElement
}
export default React
“这里也可以
”...children
形式,判断只要判断children
长度就可以了,但是属于es6
的用法,我们按照源码实现
实现 toVdom 辅助函数
我们这里还要进行一下处理,因为如果是文本类型的话,直接就是字符串了,没有类型这种标识了,所以我们要对 children
进行一下包裹,也为了后面的 diff
。
// src/utils.js
// 统一规范,方便 diff
export function toVdom(element) {
return typeof element === "string" || typeof element === "number"
? { // 字符串包裹
$$typeof: REACT_ELEMENT,
type: REACT_TEXT,
props: element,
}
: element;
}
修改 createElement
函数,包裹儿子节点
...
props.children = Array.prototype.slice.call(arguments, 2).map(toVdom);
...
props.children = toVdom(children);
调用我们自己的实现,我们可以得到如下结果
页面挂载
我们引入 react-dom
,看下原生渲染
import React from "react";
import ReactDOM from "react-dom";
let jsx = (
<h1 className="title" style={{ color: "red", backgroundColor: "pink" }}>
hello
<span>111</span>
</h1>
);
ReactDOM.render(jsx, document.getElementById("root"));
实现 reactDOM.render
大家可以按我写的第几步阅读,基本都做了注视
// 做了两件事
// 1. 虚拟dom变真实dom
// 2. 挂载
function render(vdom, container) { //。 第二步
//1
const newDOM = createDOM(vdom) // 不同功能写在不同函数里,清晰 // 第三步
//2
container.appendChild(newDOM)
}
// 创建真实 dom
function createDOM(vdom) {
let {type, props} = vdom // 我们知道虚拟dom就是我们生成的那个对象
let dom // 最后要返回的
if (type === REACT_TEXT) {
// 如果是个文本
dom = document.createTextNode(props)
} else {
// 标签节点
dom = document.createElement(type)
}
// 需要对props 中的 style 和 children 和其他进行处理
if(props) {
// 单独处理属性
updateProps(dom, {}, props) // 第四步
// 单独处理 chidlren
if(props.chidlren && typeof props.children === 'object' && props.chidlren.$$typeof) {
// 文本
render(props.chidlren, dom)
} else if (Array.isArray(props.children)) {
// 子为数组,把子挂载到当前的父 dom
reconcileChildren(props.children, dom) // 第五步
}
}
return dom
}
// 子虚拟节点,父真实节点
function reconcileChildren(chidlrenVdom, parentDom) {
// 循环递归处理, 算法题里非二叉树的多子树节点,也是 for 循环遍历处理
for (let i = 0; i < childrenVdom.length; i++) {
render(childrenVdom[i], parentDOM);
}
}
// 对 dom 进行新属性赋值,旧属性没有的删除, vue中也是类型的操作,遍历新属性,旧属性
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 const key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
delete dom[key]
}
}
}
// 根据调用,返回的一定是对象 第一步
const ReactDOM = {
render
}
export default ReactDOm
在入口文件使用我们自己的方法
// src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
可以看到,也实现了渲染
本篇就介绍到这里,我们了解了虚拟 dom
的对象形式,了解了如果挂载到页面上,下一节我们学习下类组件和函数组件的实现,如果有不对,欢迎指正!