从学习虚构dom工作原理登程,实现一个极简版的toy-react,其中蕴含了react的某几个要害个性,并非真正的react
起步
说到react,第一个想到的就是JSX,所以为了便于开发和调试,咱们的工程要具备以下几点:
- 反对ES6
- 反对JSX语法解析
- 主动打包输入
{
"@babel/core": "^7.2.2",
"@babel/plugin-transform-react-jsx": "^7.10.4",
"@babel/preset-env": "^7.3.1",
"babel-loader": "^8.0.5",
"css-loader": "^4.2.2",
"html-loader": "^1.2.1",
"html-webpack-plugin": "^4.3.0",
"style-loader": "^1.2.1",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
}
工程搭建
装置完了以上依赖,下一步是webpack的配置
-
babel-loader相干配置负责兼容ES6及JSX解析
- @babel/plugin-transform-react-jsx插件中指定pragma配置为createElement,前面会本人定制createElement办法,不改的话默认输入React.createElement
- css-loader相干配置解决css文件资源
- HtmlWebpackPlugin负责输入html文件
- 开启watch模式防止每次更改代码后手动编译
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './index.js',
watch: true,
optimization: {
minimize: false,
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: './',
filename: 'bundle.js',
chunkFilename: '[name].js',
},
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
},
],
},
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-react-jsx',
{ pragma: 'createElement' },
],
],
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: 'toy-react',
template: 'index.html',
}),
],
devServer: {
contentBase: path.join(__dirname, '/dist/'),
inline: true,
host: 'localhost',
port: 8080,
},
}
目录构造
依据以下目录构造创立对应文件
toy-react
├── README.md
├── index.html
├── index.js
├── package.json
├── style.css
├── toy-react-util.js
├── toy-react.js
└── webpack.config.js
执行webpack命令,试试成果
npx webpack
打包输入后果如下,bundle.js为最终打包文件,其中蕴含了js和css
执行index.html能够间接拜访打包后的网页成果
dist
├── bundle.js
└── index.html
toy-react
实现的性能和diff比照逻辑比较简单,上面会依据代码片段列出对应的性能实现
以react官网提供的《Tic Tac Toe》小游戏为演示demo,应用toy-react运行,成果如下:
createElement
通过webpack打包输入的JSX内容,能够看到createElement办法,一共蕴含了3个参数:
-
type
- 依据type类型解决并返回不同类型的element,目前只波及一般标签及文字内容,后续会对ElementWrapper和TextWrapper进行封装
-
attributes
- 包含事件、class、自定义属性等
-
children
- 子元素
/**
* 创立Element
* @param {*} type element类型: ElementWrapper or Component
* @param {*} attributes element属性: attributes or props
* @param {...any} children 子元素
*/
export const createElement = (type, attributes, ...children) => {
let element
typeof type === 'string'
? (element = new ElementWrapper(type))
: (element = new type())
// setProps
for (let p in attributes) {
element.setProps(p, attributes[p])
}
const insertChildren = children => {
for (let child of children) {
// text content
if (typeof child === 'string') {
child = new TextWrapper(child)
}
// element
if (typeof child === 'object' && child instanceof Array) {
insertChildren(child)
} else {
element.appendChild(child)
}
}
}
insertChildren(children)
return element
}
Component Class
次要实现了以下几点:
-
get vdom
- 初始化vdom
-
props
- 设置props
-
appendChild
- 插入子组件
-
RENDER_TO_DOM
- 保留range和vdom进行RENDER_TO_DOM
-
update
- 比照更新vdom后RENDER_TO_DOM
-
setState
- 初始化和更新state
/**
* Component类
*/
export class Component {
constructor() {
this.props = Object.create(null)
this.children = []
this._root = null
this._range = null
}
get vdom() {
return this.render().vdom
}
setProps(name, value) {
this.props[name] = value
}
appendChild(component) {
this.children.push(component)
}
[RENDER_TO_DOM](range) {
this._range = range
this._vdom = this.vdom
this._vdom[RENDER_TO_DOM](range)
}
update() {
const vdom = this.vdom
diff(this._vdom, vdom)
this._vdom = vdom
}
setState(newState) {
if (this.state === null || typeof this.state !== 'object')
return (this.state = newState)
merge(this.state, newState)
this.update()
}
}
update 、diff 简易版实现
依据实在dom树结构,创立虚构vdom,数据变更时会进行新旧vdom比照,并且将变动过的节点范畴range记录,递归比照实现后,依据变更的节点内容及range替换该范畴的dom内容,也就是RENDER_TO_DOM
- 首先要比照新旧vdom是否雷同,这里采纳比较简单粗犷的形式,间接比照节点类型、节点属性,文字类型的节点判断content,如果这几点产生扭转,间接RENDER_TO_DOM
/**
* 判断node是否雷同
* @param {*} oldNode
* @param {*} newNode
*/
export const isSameNode = (oldNode, newNode) => {
// 判断dom类型
if (oldNode.type !== newNode.type) return false
// 判断dom属性
for (let name in newNode.props) {
if (newNode.props[name] !== oldNode.props[name]) return false
}
if (Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
return false
// 文字内容
if (newNode.type == '#text') {
if (newNode.content !== oldNode.content) return false
}
return true
}
- 如果上述几点没产生扭转,递归比照新旧vdom的children,直到比照实现后RENDER_TO_DOM,保留以后vdom,实现Component的update操作;这里须要留神的是追加节点的边缘地位须要记录
/**
* _vdom比照
* @param {*} oldNode 旧的vdom
* @param {*} newNode 新的vdom
*/
export const diff = (oldNode, newNode) => {
if (!isSameNode(oldNode, newNode))
return newNode[RENDER_TO_DOM](oldNode._range)
newNode._range = oldNode._range
const newChildren = newNode.vchildren
const oldChildren = oldNode.vchildren
if (!newChildren || !newChildren.length) return
// 结点边缘地位
let tailRange = oldChildren[oldChildren.length - 1]._range
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i]
const oldChild = oldChildren[i]
if (i < oldChildren.length) {
diff(oldChild, newChild)
} else {
const range = getRange(
tailRange.endContainer,
tailRange.endOffset,
tailRange.endContainer,
tailRange.endOffset
)
// 追加newChild后批改tailRange
newChild[RENDER_TO_DOM](range)
tailRange = range
}
}
}
/**
* 替换node
* @param {*} range
* @param {*} node
*/
export const replaceContent = (range, node) => {
range.insertNode(node)
range.setStartAfter(node)
range.deleteContents()
range.setStartBefore(node)
range.setEndAfter(node)
}
setState
设置state实现形式是比照新旧状态,如果为初始化就间接用传入的状态,如果存在新旧状态就进行比照merge,状态merge后调用update来对dom构造进行更新渲染
/**
* 新旧状态比照替换
* @param {*} oldState
* @param {*} newState
*/
export const merge = (oldState, newState) => {
for (let p in newState) {
if (oldState[p] === null || typeof oldState[p] !== 'object') {
oldState[p] = newState[p]
} else {
merge(oldState[p], newState[p])
}
}
}
ElementWraper Class
继承自Component,实现以下几点:
-
get vdom
- 设置vchildren,确保不为null,用于RENDER_TO_DOM,
-
setAttribute
- 处理事件、class、自定义属性等
-
RENDER_TO_DOM
- 递归解决vchildren中的vdom,依据取到的range进行RENDER_TO_DOM
/**
* Element类
*/
class ElementWrapper extends Component {
constructor(type) {
super(type)
this.type = type
}
get vdom() {
this.vchildren = this.children.map(child => {
if (child) return child.vdom
})
return this
}
setAttribute(root) {
for (let name in this.props) {
const value = this.props[name]
// 过滤on事件命名所有字符
if (name.match(/^on([\s\S]+)$/)) {
// 按小写字母解决
root.addEventListener(
RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()),
value
)
} else {
if (name === 'className') {
root.setAttribute('class', value)
} else {
root.setAttribute(name, value)
}
}
}
}
[RENDER_TO_DOM](range) {
this._range = range
const root = document.createElement(this.type)
this.setAttribute(root)
if (!this.vchildren) this.vchildren = this.children.map(child => child.vdom)
for (let child of this.vchildren) {
if (child) {
child[RENDER_TO_DOM](
getRange(root, root.childNodes.length, root, root.childNodes.length)
)
}
}
replaceContent(range, root)
}
}
TextWrapper Class
继承自Component,实现以下几点:
-
vdom
- 获取vdom
-
RENDER_TO_DOM
- 依据range从新替换dom
/**
* Text类
*/
class TextWrapper extends Component {
constructor(content) {
super(content)
this.type = '#text'
this.content = content
}
get vdom() {
return this
}
[RENDER_TO_DOM](range) {
this._range = range
const root = document.createTextNode(this.content)
replaceContent(range, root)
}
}
render
Component通过RENDER_TO_DOM渲染到parentElement中,range间接应用parentElement的即可
/**
* 渲染函数
* @param {*} component
* @param {*} parentElement
*/
export const render = (component, parentElement) => {
component[RENDER_TO_DOM](
getRange(
parentElement,
0,
parentElement,
parentElement.childNodes.length,
true
)
)
}
// 将定义好的Game组件传入render, 渲染到id为root的父元素中
render(<Game />, document.getElementById('root'))
总结
实现以上流程,曾经初步理解了vdom的工作原理,能够作为学习react、vue等框架源码的参考,后续很多细节须要去学习、补充,例如:
- vdom的比照精度不够细
- 渲染形式毛糙
- 生命周期不够欠缺
- hooks
…
????????????
参考资料
- Web API:Range
- 残缺工程
发表回复