乐趣区

关于react.js:迷你版react代码实现

代码构造

在 React 代码执行前,JSX 会被 Babel 转换为 React.createElement 办法的调用,这里模仿 React.createElement 实现,.babelrc中须要配置一下

{
    "presets": [
        "@babel/preset-env",
        [
            "@babel/preset-react",
            {"pragma": "TinyReact.createElement"}
        ]
    ]
}

index.js中放测试代码,index.html中设置 id 为 root 的节点。
package.json中依赖

  "scripts": {"start": "webpack-dev-server --open"},
  "devDependencies": {
    "@babel/core": "^7.11.4",
    "@babel/preset-env": "^7.11.0",
    "@babel/preset-react": "^7.10.4",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^4.3.0",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }

webpack.config.js中也须要进行简略配置

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {path: path.resolve('dist'),
        filename: 'bundle.js'
    },
    devtool: 'inline-source-map',
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            use: 'babel-loader'
        }]
    },
    plugins: [
        new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns: ['./dist']
        }),
        new HtmlWebpackPlugin({template: './src/index.html'})
    ],
    devServer: {
        // 指定开发环境利用运行的依据目录
        contentBase: "./dist",
        // 指定控制台输入的信息
        stats: "errors-only",
        // 不启动压缩
        compress: false,
        host: "localhost",
        port: 5000
      }
}

次要实现逻辑放在 TinyReact 文件夹内的文件中
Component.js

import diff from "./diff"

export default class Component {constructor(props) {this.props = props}

    setState(state) {this.state = Object.assign({}, this.state, state)
        // 获取最新要渲染的 virtualDom 对象
        let virtualDom = this.render()
        // 获取旧的 virtualDom 进行比对
        let oldDom = this.getDom()
        // 获取容器
        let container = oldDom.parentNode
        // 进行 diff 比拟
        diff(virtualDom, container, oldDom)
    }

    setDom(dom) {this._dom = dom}

    getDom() {return this._dom}

    updateProps(props) {this.props = props}

    // 生命周期函数
    componentWillMount() {}
    componentDidMount() {}
    componentWillReceiveProps(nextProps) {}
    shouldComponentUpdate(nextProps, nextState) {return nextProps != this.props || nextState != this.state}
    componentWillUpdate(nextProps, nextState) {}
    componentDidUpdate(prevProps, preState) {}
    componentWillUnmount() {}
}

createDomElement.js

import mountElement from "./mountElement";
import updateNodeElement from "./updateNodeElement";

export default function createDomElement(virtualDom) {
    let newElement = null;
    // 解决不同 virtualDom 类型, 先解决最外层 div
    if (virtualDom.type === "text") {
        // 文本节点
        newElement = document.createTextNode(virtualDom.props.textContent)
    } else {
        // 元素节点
        newElement = document.createElement(virtualDom.type)
        // 为元素增加属性
        updateNodeElement(newElement,virtualDom)
    }

    // 创立时保留一份正本
    newElement._virtualDom = virtualDom

    // 递归创立子节点
    virtualDom.children.forEach(child => {mountElement(child, newElement)
    })

    // 解决 ref 属性
    // 判断其 Virtual DOM 对象中是否有 ref 属性,如果有就调用 ref 
    // 属性中所存储的办法并且将创立进去的 DOM 对象作为参数传递给 ref 办法,// 这样在渲染组件节点的时候就能够拿到元素对象并将元素对象存储为组件属性了。if(virtualDom.props && virtualDom.props.ref){virtualDom.props.ref(newElement)
    }
    return newElement
}

createElement

export default function createElement(type, props, ...children) {

    // 拷贝 children 并循环 children 数组,判断节点类型,如果
    const childElements = [].concat(...children).reduce((result, child) => {
        // 解决 jsx 节点值有布尔值,null 类型的状况
        if (child !== false && child !== true && child !== null) {if (child instanceof Object) { // 对象
                result.push(child)
            } else { // 文本
                result.push(createElement("text", {textContent: child}))
            }
        }
        return result
    }, [])



    // 返回一个 virtural Dom 对象
    /** @jsx TinyReact.createElement*/
    /* <div id ='container'>
    <p>hello world</p>
    </div> */
    // babel 在线转换
    /** @jsx TinyReact.createElement*/
    // TinyReact.createElement("div", {
    //     id: "container"
    //   }, TinyReact.createElement("p", null, "hello world"));
    return {
        type,
        props: Object.assign({children: childElements}, props), // 给 props 赋值 children 属性
        children: childElements
    }
}

diff.js

import createDomElement from "./createDomeElement"
import mountElement from "./mountElement"
import updateNodeElement from "./updateNodeElement"
import updateTextNode from "./updateTextNode"
import unmontNode from "./unmontNode"
import diffComponent from "./diffComponent"

export default function diff(virtualDom, container, oldDom) {
    const oldVirtualDom = oldDom && oldDom._virtualDom
    const oldComonent = oldVirtualDom && oldVirtualDom.component
    // 判断 oldDom 是否存在
    if (!oldDom) {
        // 间接转换 virtualDom 为实在 Dom
        mountElement(virtualDom, container)
    } else if (typeof virtualDom.type === 'function') {
        // 渲染的是一个组件
        diffComponent(virtualDom, oldComonent, oldDom, container)
    } else if (oldVirtualDom && virtualDom.type === oldVirtualDom.type) {
        // 节点类型雷同
        if (virtualDom.type === 'text') {
            // 文本类型节点更新内容
            // updateTextNode 办法承受最新的 virtualDom, 和旧的 virtualDom, 最初挂载到 oldDom(也就是页面元素)
            updateTextNode(virtualDom, oldVirtualDom, oldDom)
        } else {
            // 元素节点更新元素属性 新:virtualDom.props, 旧:oldVirtualDom.props
            updateNodeElement(oldDom, virtualDom, oldVirtualDom)
        }

        // 1. 将领有 key 属性的子元素搁置在一个独自的对象中
        let keyedElements = {}
        // 循环旧 dom 节点子元素
        for (let i = 0, len = oldDom.childNodes.length; i < len; i++) {let doMElement = oldDom.childNodes[i]
            if (doMElement.nodeType === 1) { // 判断元素节点
                let key = doMElement.getAttribute('key')
                if (key) {keyedElements[key] = doMElement
                }
            }
        }
        // 判断是否存在有 key 属性的子元素
        let hasNoKey = Object.keys(keyedElements).length === 0
        if (hasNoKey) {
            // 遍历子元素,挨个更新
            virtualDom.children.forEach((child, index) => {diff(child, oldDom, oldDom.childNodes[index])
            })
        } else {
            // 2. 循环 virtualDOM 的子元素 获取子元素的 key 属性
            virtualDom.children.forEach((child, index) => {
                let key = child.props.key
                if (key) {let doMElement = keyedElements[key]
                    if (doMElement) {
                        // 3. 看看以后地位的元素是不是咱们冀望的元素, 如果地位不统一,就插入
                        if (oldDom.childNodes[index] && oldDom.childNodes[index] !== doMElement) {oldDom.insertBefore(doMElement, oldDom.childNodes[index])
                        }
                    } else {
                        // 新增元素
                        // 第一个为现有 virtualDom, 第二个为父级容器,第三个为原有地位元素
                        mountElement(child, oldDom, oldDom.childNodes[index])
                    }
                }
            })
        }

        //  更新实现后,比对节点数量,判断是否有删除节点的状况,删除节点产生在批准父级下
        let oldChildNodes = oldDom.childNodes
        // 判断旧节点数量和新节点数量大小
        if (oldChildNodes.length > virtualDom.children.length) {if (hasNoKey) {
                // 没有找到有 key 属性的节点
                // 有节点须要被删除, 定位到最初一个节点
                for (let i = oldChildNodes.length - 1; i > virtualDom.children.length - 1; i--) {unmontNode(oldChildNodes[i])
                }
            } else {
                // 解决有 key 属性节点删除的状况
                for (let i = 0; i < oldChildNodes.length; i++) {let oldChild = oldChildNodes[i]
                    let oldchidKey = oldChild._virtualDom.props.key
                    let found = false; // 标记位
                    // 去新节点中查找是否有同样 key 的节点
                    for (let n = 0; n < virtualDom.children.length; n++) {if (oldchidKey === virtualDom.children[n].props.key) {
                            found = true
                            break
                        }
                    }
                    if (!found) {unmontNode(oldChild)
                    }
                }
            }

        }
    } else if (virtualDom.type !== oldVirtualDom.type && typeof virtualDom !== 'function') {
        // 节点类型不一样,没有必要比对
        const newElement = createDomElement(virtualDom)
        // 找到旧节点的父节点,替换 oldDom
        oldDom.parentNode.replaceChild(newElement, oldDom)
    }
}

diffComponent

import mountElement from "./mountElement"
import updateComponent from "./updateComponent"

// 要更新的是组件
// 1) 组件自身的 virtualDOM 对象 通过它能够获取到组件最新的 props
// 2) 要更新的组件的实例对象 通过它能够调用组件的生命周期函数 能够更新组件的 props 属性 能够获取到组件返回的最新的 Virtual DOM
// 3) 要更新的 DOM 象 在更新组件时 须要在已有 DOM 对象的身上进行批改 实现 DOM 最小化操作 获取旧的 Virtual DOM 对象
// 4) 如果要更新的组件和旧组件不是同一个组件 要间接将组件返回的 Virtual DOM 显示在页面中 此时须要 container 做为父级容器
export default function diffComponent(virtualDom, oldComonent, oldDom, container) {if (isSameComponet(virtualDom, oldComonent)) {
        // 同一个组件做组件更新操作
        console.log('same')
        updateComponent(virtualDom, oldComonent, oldDom, container)
    } else {
        // 不是同一个组件
        console.log('different')
        // 不是同一个组件 间接将组件内容显示在页面中
        // 这里为 mountElement 办法新增了一个参数 oldDOM 
        // 作用是在将 DOM 对象插入到页背后 将页面中已存在的 DOM 对象删除 否则无论是旧 DOM 对象还是新 DOM 对象都会显示在页面中
        mountElement(virtualDom, container, oldDom)
    }
}

function isSameComponet(virtualDOM, oldComponent) {
    //  virtualDOM.type 更新后的组件构造函数  oldComponent.constructor 未更新前的组件构造函数
    return oldComponent && virtualDOM.type === oldComponent.constructor
}

index.js中导出内部须要调用的办法

import createElement from "./createElement";
import render from "./render";
import Component from "./Component";
export default {
    createElement,
    render,
    Component
}

isFunction.js

export default function isFunction(virtualDoM) {
    // 组件元素的 virtualDom 的 type 值为函数
    return virtualDoM && typeof virtualDoM.type === 'function'
}

isFunctionComponent.js

import isFunction from "./isFunction"

export default function isFunctionComponent(vituralDom){
    const type = vituralDom.type
    // 判断原型身上是否有 render 办法
    return type && isFunction(vituralDom) && !(type.prototype && type.prototype.render)
}

mountComonent.js

import isFunction from "./isFunction";
import isFunctionComponent from "./isFunctionComponent";
import mountNativeElement from "./mountNativeElement";

export default function mountComonent(virtualDoM, container, oldDom) {
    let nextVirtualDom = null
    let component = null
    // 区分类组件和函数组件
    if (isFunctionComponent(virtualDoM)) {
        // 函数组件
        nextVirtualDom = buildFunctionComponent(virtualDoM)
    } else {
        // 类组件
        nextVirtualDom = buildClassComponent(virtualDoM)
        component = nextVirtualDom.component
    }

    // 函数组件中可能返回的还是一个组件,这种状况须要先判断解决
    if (isFunction(nextVirtualDom)) {mountComonent(nextVirtualDom, container, oldDom)
    } else {
        // 挂载到页面上
        mountNativeElement(nextVirtualDom, container, oldDom)
    }

    // 解决组件的 ref 属性,存在就调用 ref 办法并且将组件实例对象传递给 ref 办法
    if (component) {component.componentDidMount() // 调用生命周期函数
        if (component.props && component.props.ref) {component.props.ref(component)
        }
    }
}

function buildFunctionComponent(virtualDoM) {
    // type 属性存储的是函数组件自身
    return virtualDoM && virtualDoM.type(virtualDoM.props || {})
}

function buildClassComponent(virtualDOM) {
    // 失去组件的实例对象,拿到 render 办法
    const component = new virtualDOM.type(virtualDOM.props || {})
    const nextVirtualDom = component.render()
    // 类实例对象挂载 component 属性寄存 dom 对象
    nextVirtualDom.component = component
    return nextVirtualDom
}

mountElement.js

import mountNativeElement from "./mountNativeElement"
import isFunction from "./isFunction"
import mountComonent from "./mountComonent"

export default function mountElement(virtualDom, container,oldDom) {
    // 判断 virtualDom 是函组件还是一般的组件元素
    if (isFunction(virtualDom)) {mountComonent(virtualDom,container,oldDom)
    } else {mountNativeElement(virtualDom, container,oldDom)
    }

}

mountNativeElement.js

import createDomElement from "./createDomeElement";
import unmontNode from "./unmontNode";

export default function mountNativeElement(virtualDom, container, oldDom) {let newElement = createDomElement(virtualDom)

    // 将转换之后的 DOM 对象搁置在页面中, 如:key 属性存在时,插入原有地位元素之前
    if (oldDom) {container.insertBefore(newElement, oldDom)
    } else {
        // 把组装好的 Dom 对象挂载到页面中
        container.appendChild(newElement)
    }

    // 更新类组件时,会有 oldDom 参数传入, 此时如果有传入,就是对 diffComponent 办法中比对状况解决
    if (oldDom) {
        // 判断旧的 DOM 对象是否存在 如果存在 删除
        unmontNode(oldDom)
    }

    // 获取类组件实例对象, 类组件特有 mountComonent 办法中 buildClassComponent 办法外面挂载
    let componet = virtualDom.component
    if (componet) {
        // 挂载时,通过调用 setDom 办法挂载旧的 Dom 对象
        componet.setDom(newElement)
    }

}

render.js

import diff from "./diff"

// oldDom 默认值为挂载到页面的 root 第一个子元素,就是页面的元素,也就是 oldDom
export default function render(virtualDom, container, oldDom = container.firstChild) {diff(virtualDom, container, oldDom)
}

unmontNode.js

export default function unmontNode(node) {
    // 获取节点的 _virtualDOM 对象
    const virtualDOM = node._virtualDom

    // 文本节点能够间接删除
    if (virtualDOM.type === 'text') {node.remove()
        // 删除之后程序完结
        return
    }


    // -------- 解决元素节点删除状况 --------
    // 判断节点是否由组件生成
    let component = virtualDOM.component
    // 如果 component 存在 就阐明节点是由组件生成的
    if (component) {component.componentWillUnmount()
    }

    // 判断节点身上是否有 ref 属性
    if (virtualDOM.props && virtualDOM.props.ref) {virtualDOM.props.ref(null)
    }

    // 判断节点属性中是否由事件属性
    Object.keys(virtualDOM.props).forEach(propName => {if (propName.slice(0, 2) === "on") {const eventName = propName.toLowerCase().slice(0, 2)
            //   去除事件属性对应值
            const eventHandler = virtualDOM.props[propName]
            node.removeEventListener(eventName, eventHandler)
        }
    })


    // 判断节点是否有子节点,采纳递归形式解决
    if (node.childNodes.length > 0) {for (let i = 0; i < node.childNodes.length; i++) {unmontNode(node.childNodes[i])
            i--
        }
    }

    // 下面都是解决元素节点的非凡状况,解决完之后删除 node
    node.remove()}

updateComponent.js

import diff from "./diff"

export default function updateComponent(virtualDom, oldComponent, oldDom, container) {oldComponent.componentWillReceiveProps(virtualDom.props)
    if (oldComponent.shouldComponentUpdate(virtualDom.props)) {
        // 未更前前的 props
        let prevProps = oldComponent.props
        oldComponent.componentWillUpdate(virtualDom.props)
        // 组件更新,调用 componet 的实例办法
        oldComponent.updateProps(virtualDom.props)
        // 获取组件返回的最新的 virtualDOM
        let nextVirtualDom = oldComponent.render()
        // 更新 component 组件实例对象
        nextVirtualDom.component = oldComponent
        diff(nextVirtualDom, container, oldDom)
        oldComponent.componentDidUpdate(prevProps)
    }

}

updateNodeElement.js

// 第三个参数 oldVirtualDom 在设置属性的时候不传,值是 undefined,在更新属性的时候传
export default function updateNodeElement(newElement, virtualDom, oldVirtualDom = {}) {
    // 获取节点对应的属性对象
    const newProps = virtualDom.props || {}
    // 旧的节点属性
    const oldProps = oldVirtualDom.props || {}
    // 遍历属性
    Object.keys(newProps).forEach(propName => {
        // 获取属性值
        const newPropsValue = newProps[propName]
        const oldPropsValue = oldProps[propName]

        // 设置新属性和更新属性值,能够放在一个判断条件中解决,当设置新属性时,oldVirtualDom 不存在,virtualDom 和 oldVirtualDom 不相等
        if (newPropsValue !== oldPropsValue) {
            // 判断属性类型,转换如:onClick ->click
            if (propName.slice(0, 2) === 'on') { // 事件属性 
                // 获取事件名称
                const eventName = propName.toLowerCase().slice(2)
                //   为元素增加事件
                newElement.addEventListener(eventName, newPropsValue)
                // 更新事件属性时,间接删除原有的事件的处理函数
                if (oldPropsValue) {newElement.removeEventListener(eventName, oldPropsValue)
                }
            } else if (propName === 'value' || propName === 'checked') { // 标签特有属性
                newElement[propName] = newPropsValue
            } else if (propName !== 'children') { // props 数据结构中有 children 属性,然而它不属于属性,去除这种状况
                if (propName === 'className') {
                    // 类名
                    newElement.setAttribute('class', newPropsValue)
                } else {
                    // 一般属性
                    newElement.setAttribute(propName, newPropsValue)
                }
            }
        }
    })
    
    // 判断属性是否删除, 当有属性被删除时,oldProps 中会有,而 newProps 没有
    Object.keys(oldProps).forEach(propName => {const newPropsValue = newProps[propName]
        const oldPropsVlaue = oldProps[propName]
        if (!newPropsValue) {
            // 不存在,属性被删除
            if (propName.slice(0, 2) === 'on') {
                // 事件属性被删除
                const eventName = propName.toLowerCase().slice(2)
                newElement.removeEventListener(eventName, oldPropsVlaue) // 移除事件函数
            } else if (propName !== 'children') {if (propName === 'value') {newElement.value = ''} else {newElement.removeAttribute(propName)
                }
            }
        }
    })

}

updateTextNode.js

export default function updateTextNode(virtualDom, oldVirtualDom, oldDom) {
    // 更新文本内容
    if (virtualDom.props.textContent !== oldVirtualDom.props.textContent) {
        oldDom.textContent = virtualDom.props.textContent
        oldDom._virtualDom = virtualDom // oldDome 的 virtualDom 也须要被更新
    }
}
退出移动版