前言

通常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比照更新。