Mini-react day01 目标:在页面中呈现“app”文本

119 阅读6分钟

Mini-react day01 目标:在页面中呈现“app”文本

文章产出源自@阿崔cxrmini-react活动课程

image.png 采用小步走策略,分为三个步骤:

  1. vdom写死,dom渲染写死
  2. vdom动态生成,dom渲染写死
  3. vdom动态生成,dom动态生成(递归)
  4. 重构,使其看起来类似react api

v0.1 vdom写死,dom渲染写死

使用createElementcreateTextNode来创建元素和文本节点,将元素和文本节点挂载到页面上。

// 创建一个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的相关内容:

  • $$typeof
  • ref
  • key
  • type:标签名或者组件
  • 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
createElementcreateTextNode 的作用就是用来生成虚拟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('属性名)

            image.png image.png

    • 针对children,要递归生成真实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)

第一天完结!撒花