前言
通常 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 类:
- 以 on 结尾,代表注册事件。
- children,代表子元素,不是属性。
- className,元素 class,可通过 setAttribute 增加到元素节点上,须要指定属性名为 class。
- value、checked,元素节点上的属性,通过. 的形式赋值。
- 一般属性,通过 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 比照更新。