Mini-react day01 目标:在页面中呈现“app”文本
文章产出源自@阿崔cxr的mini-react活动课程。
采用小步走策略,分为三个步骤:
- vdom写死,dom渲染写死
- vdom动态生成,dom渲染写死
- vdom动态生成,dom动态生成(递归)
- 重构,使其看起来类似react api
v0.1 vdom写死,dom渲染写死
使用createElement和createTextNode来创建元素和文本节点,将元素和文本节点挂载到页面上。
// 创建一个div
const dom = document.createElement('div')
// 设置div的id
dom.id = 'app'
// 将div挂载到根节点上
document.querySelector('#root').append(dom)
// 创建一个文本节点
const textNode = document.createTextNode("")
// 设置文本节点的值
textNode.nodeValue = "app"
// 将文本节点添加到创建的div上
dom.append(textNode)
v0.2 vdom(virtualDOM)动态生成,dom渲染写死
生成动态的虚拟dom。
虚拟dom也称之为JSX元素、JSX对象、ReactChild对象。
虚拟DOM的相关内容:
$$typeofrefkeytype:标签名或者组件props:元素的相关属性&&子节点信息元素的相关属性children:子节点信息。没有子节点则没有这个属性,属性可能是一个值,也可能是一个数组
写死的虚拟DOM。
用js对象来描述虚拟DOM
// v2 react -> vdom -> js object
// type props children
const textEl = {
type: "TEXT_ELEMENT",
props: {
nodeValue: "app",
children: []
}
}
const el = {
type: "div",
props: {
id: "app",
children: [textEl]
}
}
// 创建一个div
const dom = document.createElement(el.type)
// 设置div的id
dom.id = el.props.id
// 将div挂载到根节点上
document.querySelector('#root').append(dom)
// 创建一个文本节点
const textNode = document.createTextNode("")
// 设置文本节点的值
textNode.nodeValue = textEl.props.nodeValue
// 将文本节点添加到创建的div上
dom.append(textNode)
将写死的虚拟DOM变成动态的虚拟DOM
将写死的js对象改成用函数生成并返回对象,动态生成虚拟DOM。
这里针对文本节点和元素做了区分。元素调用createElement,文本节点调用createTextNode
createElement和createTextNode 的作用就是用来生成虚拟DOM
// v2 react -> vdom -> js object
// type props children
// const textEl = {
// type: "TEXT_ELEMENT",
// props: {
// nodeValue: "app",
// children: []
// }
// }
// 将js对象改成函数,生成js对象
function createTextNode(text){
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
// const el = {
// type: "div",
// props: {
// id: "app",
// children: [textEl]
// }
// }
// 用下面这个方法替换上面写死的js对象
// ...children 剩余参数
function createElement(type, props, ...children){
return {
type,
props: {
...props,
children
}
}
}
const textEl = createTextNode('app1')
const app = createElement('div', { id: 'app'}, textEl)
// 创建一个div
const dom = document.createElement(app.type)
// 设置div的id
dom.id = app.props.id
// 将div挂载到根节点上
document.querySelector('#root').append(dom)
// 创建一个文本节点
const textNode = document.createTextNode("")
// 设置文本节点的值
textNode.nodeValue = textEl.props.nodeValue
// 将文本节点添加到创建的div上
dom.append(textNode)
v0.3 vdom动态生成,dom动态生成(递归)
render函数的作用:根据虚拟dom,动态生成真实dom。
- 第一步 动态创建节点
- 第二步 动态设置props, 针对children,要递归处理
- 循环props上的key,给生成的dom添加属性。
- 给元素添加属性有两种方式
- 第一种【内置/自定义属性】:
元素.属性名=属性值-
对于内置属性,是设置在元素的标签上
-
对于自定义属性,是给对象的堆内存空间中新增成员,不会设置在标签上。
获取:
元素.属性
删除:delete 元素.属性
-
- 第二种:
元素.setAtttribute('属性名','属性值')-
直接写在元素的标签上
获取:元素.getAtttribute('属性名)
删除:元素.removeAtttribute('属性名)
-
- 第一种【内置/自定义属性】:
- 给元素添加属性有两种方式
- 针对children,要递归生成真实DOM
- 循环props上的key,给生成的dom添加属性。
- 第三步 将生成的真实DOM挂载到容器上。
// 生成虚拟dom
function createTextNode(text){
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
// 生成虚拟dom
function createElement(type, props, ...children){
return {
type,
props: {
...props,
children
}
}
}
const textEl = createTextNode('app1')
const app = createElement('div', { id: 'app'}, textEl)
// // 创建一个div
// const dom = document.createElement(app.type)
// // 设置div的id
// dom.id = app.props.id
// // 将div设置到根节点上
// document.querySelector('#root').append(dom)
// // 创建一个文本节点
// const textNode = document.createTextNode("")
// // 设置文本节点的值
// textNode.nodeValue = textEl.props.nodeValue
// // 将文本节点添加到创建的div上
// dom.append(textNode)
// 用下面这个方法替换上面那一段写死的代码,动态生成DOM
function render(el, container) {
const { type, props } = el
// 动态生成dom,第一步动态创建节点, 第二步 动态设置props,第三步 父级进行添加
// 第一步动态创建节点
const dom = type === 'TEXT_ELEMENT' ?
document.createTextNode(''):
document.createElement(type)
// - 第二步 动态设置props
Object.keys(props).forEach((key) => {
if(key !== 'children'){
// style的处理, props[key]存储的是样式对象
if(key === 'style') {
Object.keys(props[key]).forEach((styleKey) =>{
dom.style[styleKey] = props[key][styleKey]
})
return
}
dom[key] = props[key]
}
})
// 处理孩子节点, 针对children,要递归处理
const children = props?.children
children?.forEach(chid=> {
render(chid, dom)
})
// - 第三步 父级进行添加
container.append(dom)
}
render(app, document.getElementById('root'))
兼容直接传文本字符串
在创建虚拟dom的时候,createTextNode直接传入文本字符串,由于文本字符串没有对应的虚拟节点,会报错。所以我们需要做处理。
function createTextNode(text){
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
function createElement(type, props, ...children){
return {
type,
props: {
...props,
// children
// (******这里是改动点) 处理生成虚拟DOM的时候,传入的child是文本字符串的情况
children: children.map(child => typeof child === 'string' ? createTextNode(child): child )
}
}
}
// const textEl = createTextNode('app1')
// const app = createElement('div', { id: 'app'}, textEl)
// (******这里是改动点)
const app = createElement('div', { id: 'app'}, 'app3333', 'MINI-REACT')
function render(el, container) {
const { type, props } = el
// 动态生成dom,第一步动态创建节点, 第二步 动态设置props,第三步 父级进行添加
// 第一步动态创建节点
const dom = type === 'TEXT_ELEMENT' ?
document.createTextNode(''):
document.createElement(type)
// - 第二步 动态设置props
// id class
Object.keys(props).forEach((key) => {
if(key !== 'children'){
// style的处理, props[key]存储的是样式对象
if(key === 'style') {
Object.keys(props[key]).forEach((styleKey) =>{
dom.style[styleKey] = props[key][styleKey]
})
return
}
dom[key] = props[key]
}
})
// 处理孩子节点
const children = props?.children
children?.forEach(chid=> {
render(chid, dom)
})
// - 第三步 父级进行添加
container.append(dom)
}
render(app, document.getElementById('root'))
v0.4 重构 --> react api
function createTextNode(text){
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
function createElement(type, props, ...children){
return {
type,
props: {
...props,
// 处理生成虚拟DOM的时候,传入的child是文本字符串的情况
children: children.map(child => typeof child === 'string' ? createTextNode(child): child )
}
}
}
const app = createElement('div', { id: 'app'}, 'app3333111', 'MINI-REACT')
function render(el, container) {
const { type, props } = el
// 动态生成dom,第一步动态创建节点, 第二步 动态设置props,第三步 父级进行添加
// 第一步动态创建节点
const dom = type === 'TEXT_ELEMENT' ?
document.createTextNode(''):
document.createElement(type)
// - 第二步 动态设置props
// id class
Object.keys(props).forEach((key) => {
if(key !== 'children'){
// style的处理, props[key]存储的是样式对象
if(key === 'style') {
Object.keys(props[key]).forEach((styleKey) =>{
dom.style[styleKey] = props[key][styleKey]
})
return
}
dom[key] = props[key]
}
})
// 处理孩子节点
const children = props?.children
children?.forEach(chid=> {
render(chid, dom)
})
// - 第三步 父级进行添加
container.append(dom)
}
// 重构代码,使其看起来像react-api
// render(app, document.getElementById('root'))
const ReactDOM = {
createRoot(container){
return {
render(app){
render(app, container)
}
}
}
}
ReactDOM.createRoot(document.getElementById('root')).render(app)
代码整体文件的调整
React.js
// code/mini-react/day01/core/React.js
function createTextNode(text){
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
function createElement(type, props, ...children){
return {
type,
props: {
...props,
// 处理生成虚拟DOM的时候,传入的child是文本字符串的情况
children: children.map(child => typeof child === 'string' ? createTextNode(child): child )
}
}
}
function render(el, container) {
const { type, props } = el
// 动态生成dom,第一步动态创建节点, 第二步 动态设置props,第三步 父级进行添加
// 第一步动态创建节点
const dom = type === 'TEXT_ELEMENT' ?
document.createTextNode(''):
document.createElement(type)
// - 第二步 动态设置props
// id class
Object.keys(props).forEach((key) => {
if(key !== 'children'){
// style的处理, props[key]存储的是样式对象
if(key === 'style') {
Object.keys(props[key]).forEach((styleKey) =>{
dom.style[styleKey] = props[key][styleKey]
})
return
}
dom[key] = props[key]
}
})
// 处理孩子节点
const children = props?.children
children?.forEach(chid=> {
render(chid, dom)
})
// - 第三步 父级进行添加
container.append(dom)
}
const React = {
render,
createElement
}
export default React
ReactDom.js
// code/mini-react/day01/core/ReactDom.js
import React from './React.js'
const ReactDOM = {
createRoot(container){
return {
render(app){
React.render(app, container)
}
}
}
}
export default ReactDOM
// main.js
import React from "./core/React.js"
import ReactDOM from "./core/ReactDom.js"
const app = React.createElement('div', { id: 'app'}, 'app3333111', 'MINI-REACT')
ReactDOM.createRoot(document.getElementById('root')).render(app)
// HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mini-react</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="main.js"></script>
</body>
</html>
创建app.js
// code/mini-react/day01/app.js
import React from "./core/React.js"
const app = React.createElement('div', { id: 'app'}, 'app3333111', 'MINI-REACT')
export default app
修改main.js
import ReactDOM from "./core/ReactDom.js"
import App from "./app.js"
ReactDOM.createRoot(document.getElementById('root')).render(App)
第一天完结!撒花