【JS】如何构建自定义React基础虚拟Dom框架(一)

前言

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

在babel repl站点中可以查看jsx的转换结果:

【JS】如何构建自定义React基础虚拟Dom框架(一)

默认情况下,babel总会将jsx代码转换成react.createElement方法的调用,如果想实现自定义的虚拟Dom框架,可以在jsx代码上添加注释:

/** @jsx MyReact.createlement */

该注释用于指定babel转换时调用的方法,添加注释后,转换结果如下:

【JS】如何构建自定义React基础虚拟Dom框架(一)

在项目中,可以通过配置babelrc文件达到同样的目的:

{

"presets": [

"@babel/preset-env",

[

"@babel/preset-react",

{

"pragma": "MyReact.createElement"

}

]

]

}

本项目基础文件结构如下:

【JS】如何构建自定义React基础虚拟Dom框架(一)

其中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)

结果如下:

【JS】如何构建自定义React基础虚拟Dom框架(一)

处理文本节点

在上节中,虽然生成了虚拟Dom,但是生成的结果有问题,即文本节点是字符串,而不是一个对象:

【JS】如何构建自定义React基础虚拟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

}

}

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

【JS】如何构建自定义React基础虚拟Dom框架(一)

处理jsx中的逻辑判断

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

【JS】如何构建自定义React基础虚拟Dom框架(一)

正常情况下,该段逻辑判断2和1是否相等,如果相等,则输出后面的内容,如果不相等,最终的虚拟Dom中应该不包含此节点,但是最终生成的结果中生成了一个内容为false的文本节点:

【JS】如何构建自定义React基础虚拟Dom框架(一)

针对这种情况,需要在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渲染,并递归渲染子节点。

此时,页面上可以正常显示内容:

【JS】如何构建自定义React基础虚拟Dom框架(一)

元素节点添加属性

在入口文件的jsx代码中:

【JS】如何构建自定义React基础虚拟Dom框架(一)

为h2元素节点添加了data-test属性,但是在渲染完成后的页面上,并没有该属性:

【JS】如何构建自定义React基础虚拟Dom框架(一)

因此,需要在渲染元素节点的时候,处理元素节点的属性,生成元素节点是在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)

}

}

})

}

属性处理完成后,刷新页面:

【JS】如何构建自定义React基础虚拟Dom框架(一)

未完待续: 后续将处理类组件,函数组件和虚拟Dom对比更新。

以上是 【JS】如何构建自定义React基础虚拟Dom框架(一) 的全部内容, 来源链接: utcz.com/a/104280.html

回到顶部