关于javascript:如何构建自定义React基础虚拟Dom框架一

4次阅读

共计 5763 个字符,预计需要花费 15 分钟才能阅读完成。

前言

通常 React 我的项目蕴含大量 jsx 代码,babel 在编译代码的时候,会将 jsx 代码块转换为 React.createElement 办法的调用。

在 babel repl 站点中能够查看 jsx 的转换后果:

默认状况下,babel 总会将 jsx 代码转换成 react.createElement 办法的调用,如果想实现自定义的虚构 Dom 框架,能够在 jsx 代码上增加正文:

/** @jsx MyReact.createlement */

该正文用于指定 babel 转换时调用的办法,增加正文后,转换后果如下:

在我的项目中,能够通过配置 babelrc 文件达到同样的目标:

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

本我的项目根底文件构造如下:

其中 MyReact 文件夹中寄存的是所有自定义 react 根底代码,index.js 是我的项目入口文件,用于申明 jsx,类组件,函数组件和 render 挂载。

残缺代码下载地址:

MyReact

MyReact

createElement

虚构 Dom 实现的根底是 createElement 办法,此办法用于将 babel 转换后的后果生成虚构 Dom 对象。

在 MyReact 文件夹中增加 createElement.js 文件,导出 createElement 办法:

export default function createElement (type, props, ...children) {
    // 将参数转换成虚构 Dom 对象
    return {
        type,
        props,
        children
    }
}

在入口文件中,构建一段 jsx 代码,并打印最终转换后的后果:

import * as MyReact from './MyReact'

const virtualDOM = (
    <div className="container">
        <h1>hello MyReact</h1>
        <h2 data-test="test"> 测试嵌套 Dom</h2>
        <div>
            嵌套 1 <div> 嵌套 1.1</div>
        </div>
        <h3>(察看: 这个将会被扭转)</h3>
       {2 == 1 && <div> 如果 2 和 1 相等渲染以后内容 </div>}
        {2 == 2 && <div>2</div>}
        <span> 这是一段内容 </span>
        <button onClick={() => alert("你好")}> 点击我 </button>
        <h3> 这个将会被删除 </h3>
         2, 3
        <input type="text" value="13" />
    </div>
)
console.log(virtualDOM) 

后果如下:

解决文本节点

在上节中,尽管生成了虚构 Dom,然而生成的后果有问题,即文本节点是字符串,而不是一个对象:

须要批改 createElement 办法如下:

export default function createElement(type, props, ...children) {const childrenElements = [].concat(...children).map(child => {
        // 如果 child 是对象,阐明是元素节点
        if (child instanceof Object) {return child} else {
            // 否则是文本节点,须要用 createElement 创立文本节点对象
            return createElement('text', { textContent: child})
        }
    })
    return {
        type,
        props,
        children: childrenElements
    }
}

此时,打印的后果中文本节点被正确处理:

解决 jsx 中的逻辑判断

在入口文件的 jsx 中有一段:

失常状况下,该段逻辑判断 2 和 1 是否相等,如果相等,则输入前面的内容,如果不相等,最终的虚构 Dom 中应该不蕴含此节点,然而最终生成的后果中生成了一个内容为 false 的文本节点:

针对这种状况,须要在 createElement 中增加判断,即如果节点为 bool 值或者 null 的时候,排除该节点。

export default function createElement(type, props, ...children) {const childrenElements = [].concat(...children).reduce((result, child) => {if (child !== false && child !== true && child !== null) {
            // 如果 child 是对象,阐明是元素节点
            if (child instanceof Object) {result.push(child)
            } else {
                // 否则是文本节点,须要用 createElement 创立文本节点对象
                result.push(createElement('text', { textContent: child}))
            }
        }

        return result
    }, [])
    return {
        type,
        // 反对通过 children 属性获取子元素
        props: Object.assign({children: childrenElements}, props),
        children: childrenElements
    }
}

渲染虚构 Dom 对象

在 react 中通过 render 办法将虚构 Dom 渲染成实在 Dom 对象,并增加到占位节点中。

因而增加 render 办法,如果是首次渲染,就间接将 Dom 对象替换到占位节点中,如果是更新操作,就通过 diff 算法更新节点。

在 index.js 中增加 render 调用:

import * as MyReact from './MyReact'
const virtualDOM = (
    <div className="container">
        <h1>hello MyReact</h1>
        <h2 data-test="test"> 测试嵌套 Dom</h2>
        <div>
            嵌套 1 <div> 嵌套 1.1</div>
        </div>
        <h3>(察看: 这个将会被扭转)</h3>
        {2 == 1 && <div> 如果 2 和 1 相等渲染以后内容 </div>}
        {2 == 2 && <div>2</div>}
        <span> 这是一段内容 </span>
        <button onClick={() => alert("你好")}> 点击我 </button>
        <h3> 这个将会被删除 </h3>
         2, 3
        <input type="text" value="13" />
    </div>
)
console.log(virtualDOM) 
// 新增加:用于启动 Dom 渲染
MyReact.render(virtualDOM, document.getElementById('root'))

在 MyReact 文件夹中增加 render.js 文件,用于执行渲染,其外部调用 diff 办法。

render.js:

import diff from './diff'

export default function render(
    // 虚构 dom
    virtualDom,
    // 容器
    container,
    // 旧节点
    oldDom = container.firstChild
) {
    // 在 diff 办法外部判断是否须要比照更新
    diff(virtualDom, container, oldDom)
}

diff.js: 判断是否传入旧的 Dom 节点,如果没有传入就是首次渲染,
否则执行更新操作(期待补充)。

import mountElement from './mountElement'

export default function diff(
    // 虚构 dom
    virtualDom,
    // 容器
    container,
    // 旧节点
    oldDom = container.firstChild
) {
    // 如果旧的节点不存在,不须要比照,间接渲染并挂载到容器下
    if(!oldDom) {mountElement(virtualDom, container)
    }
}

mountElement.js: 调用 mountNativeElement 办法

import mountNativeElement from './mountNativeElement'

export default function mountElement(
    virtualDom,
    container
) {
    // 调用 mountNativeElement 办法将虚构 Dom 转换为实在 Dom
    mountNativeElement(virtualDom, container)
}

mountNativeElement.js: 执行 dom 渲染和挂载工作。

import createDomElement from './createDomElement'

export default function mountNativeElement(virtualDom, container) {
    // 调用 createDomElement 创立 Dom
    const newElement = createDomElement(virtualDom)
    // 挂载
    container.appendChild(newElement)
}

createDomElement.js: 真正实现渲染虚构 Dom,并递归解决子节点。

import mountElement from './mountElement'

// 真正执行 Dom 渲染工作,蕴含文本节点和元素节点的渲染
export default function createDomElement(virtualDom) {
    let newElement = null
    if (virtualDom.type === 'text') {
        // 创立文本节点
        newElement = document.createTextNode(virtualDom.props.textContent)
    } else {
        // 创立元素节点
        newElement = document.createElement(virtualDom.type)
    }

    // 递归解决子节点
    virtualDom.children.forEach(child => {
        // 再次调用 mountElement 办法,将 child 作为虚构 Dom 节点,newElement 作为容器
        mountElement(child, newElement)
    })

    return newElement
}

至此,首次渲染的工作根本实现,因为波及到大量可复用代码,所以用上面的调用关系示意大抵流程:

render: 执行渲染入口。diff: 用于判断是首次加载还是更新操作
mountElement: 渲染 Dom 节点并挂载到容器中,可复用。mountNativeElement:appendChild 实现挂载。createDomElement: 真正执行 Dom 渲染,并递归渲染子节点。

此时,页面上能够失常显示内容:

元素节点增加属性

在入口文件的 jsx 代码中:

为 h2 元素节点增加了 data-test 属性,然而在渲染实现后的页面上,并没有该属性:

因而,须要在渲染元素节点的时候,解决元素节点的属性,生成元素节点是在 createDomElement 办法中。

批改 createDomElement 办法:

if (virtualDom.type === 'text') {
    // 创立文本节点
    newElement = document.createTextNode(virtualDom.props.textContent)
} else {
    // 创立元素节点
    newElement = document.createElement(virtualDom.type)
    // 元素节点创立实现后,须要解决元素属性
    updateElementNode(newElement, virtualDom)
}

元素节点的所有属性存储在虚构 Dom 的 props 中,能够分为如下 5 类:

  1. 以 on 结尾,代表注册事件。
  2. children,代表子元素,不是属性。
  3. className,元素 class,可通过 setAttribute 增加到元素节点上,须要指定属性名为 class。
  4. value、checked,元素节点上的属性,通过. 的形式赋值。
  5. 一般属性,通过 setAttribute 增加到元素节点上,不须要扭转属性名。

因而在 updateElementNode.js 文件中,须要对各种状况进行判断:

export default function updateElementNode(
    // 实在的 Dom 元素节点
    element,
    // 虚构 Dom 对象,蕴含所有属性信息
    virtualDOM
) {
    // 获取要解析的 VirtualDOM 对象中的属性对象
    const newProps = virtualDOM.props
    // 将属性对象中的属性名称放到一个数组中并循环数组
    Object.keys(newProps).forEach(propName => {const newPropsValue = newProps[propName]
        // 思考属性名称是否以 on 结尾 如果是就示意是个事件属性 onClick -> click
        if (propName.slice(0, 2) === "on") {const eventName = propName.toLowerCase().slice(2)
            element.addEventListener(eventName, newPropsValue)
            // 如果属性名称是 value 或者 checked 须要通过 [] 的模式增加} else if (propName === "value" || propName === "checked") {element[propName] = newPropsValue
            // 刨除 children 因为它是子元素 不是属性
        } else if (propName !== "children") {
            // className 属性独自解决 不间接在元素上增加 class 属性是因为 class 是 JavaScript 中的关键字
            if (propName === "className") {element.setAttribute("class", newPropsValue)
            } else {
                // 一般属性
                element.setAttribute(propName, newPropsValue)
            }
        }
    })
}

属性解决实现后,刷新页面:

未完待续:后续将解决类组件,函数组件和虚构 Dom 比照更新。

正文完
 0