代码结构
在 React 代码执行前,JSX 会被 Babel 转换为 React.createElement 方法的调用,这里模拟React.createElement实现,.babelrc
中需要配置一下
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"pragma": "TinyReact.createElement"
}
]
]
}
index.js
中放测试代码,index.html
中设置id为root的节点。package.json
中依赖
"scripts": {
"start": "webpack-dev-server --open"
},
"devDependencies": {
"@babel/core": "^7.11.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^4.3.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
webpack.config.js
中也需要进行简单配置
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve('dist'),
filename: 'bundle.js'
},
devtool: 'inline-source-map',
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}]
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['./dist']
}),
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
devServer: {
// 指定开发环境应用运行的根据目录
contentBase: "./dist",
// 指定控制台输出的信息
stats: "errors-only",
// 不启动压缩
compress: false,
host: "localhost",
port: 5000
}
}
主要实现逻辑放在TinyReact
文件夹内的文件中Component.js
import diff from "./diff"
export default class Component {
constructor(props) {
this.props = props
}
setState(state) {
this.state = Object.assign({}, this.state, state)
// 获取最新要渲染的virtualDom对象
let virtualDom = this.render()
// 获取旧的virtualDom进行比对
let oldDom = this.getDom()
// 获取容器
let container = oldDom.parentNode
// 进行diff比较
diff(virtualDom, container, oldDom)
}
setDom(dom) {
this._dom = dom
}
getDom() {
return this._dom
}
updateProps(props) {
this.props = props
}
// 生命周期函数
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state
}
componentWillUpdate(nextProps, nextState) {}
componentDidUpdate(prevProps, preState) {}
componentWillUnmount() {}
}
createDomElement.js
import mountElement from "./mountElement";
import updateNodeElement from "./updateNodeElement";
export default function createDomElement(virtualDom) {
let newElement = null;
// 处理不同virtualDom类型,先处理最外层div
if (virtualDom.type === "text") {
// 文本节点
newElement = document.createTextNode(virtualDom.props.textContent)
} else {
// 元素节点
newElement = document.createElement(virtualDom.type)
// 为元素添加属性
updateNodeElement(newElement,virtualDom)
}
// 创建时保留一份副本
newElement._virtualDom = virtualDom
// 递归创建子节点
virtualDom.children.forEach(child => {
mountElement(child, newElement)
})
// 处理ref属性
// 判断其 Virtual DOM 对象中是否有 ref 属性,如果有就调用 ref
// 属性中所存储的方法并且将创建出来的DOM对象作为参数传递给 ref 方法,
// 这样在渲染组件节点的时候就可以拿到元素对象并将元素对象存储为组件属性了。
if(virtualDom.props && virtualDom.props.ref){
virtualDom.props.ref(newElement)
}
return newElement
}
createElement
export default function createElement(type, props, ...children) {
// 拷贝children并循环children数组,判断节点类型,如果
const childElements = [].concat(...children).reduce((result, child) => {
// 处理jsx节点值有布尔值,null类型的情况
if (child !== false && child !== true && child !== null) {
if (child instanceof Object) { // 对象
result.push(child)
} else { // 文本
result.push(createElement("text", {
textContent: child
}))
}
}
return result
}, [])
// 返回一个virtural Dom对象
/** @jsx TinyReact.createElement*/
/*
hello world
*/
// babel在线转换
/** @jsx TinyReact.createElement*/
// TinyReact.createElement("div", {
// id: "container"
// }, TinyReact.createElement("p", null, "hello world"));
return {
type,
props: Object.assign({
children: childElements
}, props), // 给props赋值children属性
children: childElements
}
}
diff.js
import createDomElement from "./createDomeElement"
import mountElement from "./mountElement"
import updateNodeElement from "./updateNodeElement"
import updateTextNode from "./updateTextNode"
import unmontNode from "./unmontNode"
import diffComponent from "./diffComponent"
export default function diff(virtualDom, container, oldDom) {
const oldVirtualDom = oldDom && oldDom._virtualDom
const oldComonent = oldVirtualDom && oldVirtualDom.component
// 判断oldDom是否存在
if (!oldDom) {
// 直接转换virtualDom为真实Dom
mountElement(virtualDom, container)
} else if (typeof virtualDom.type === 'function') {
// 渲染的是一个组件
diffComponent(virtualDom, oldComonent, oldDom, container)
} else if (oldVirtualDom && virtualDom.type === oldVirtualDom.type) {
// 节点类型相同
if (virtualDom.type === 'text') {
// 文本类型节点更新内容
// updateTextNode方法接受最新的virtualDom,和旧的virtualDom,最后挂载到oldDom(也就是页面元素)
updateTextNode(virtualDom, oldVirtualDom, oldDom)
} else {
// 元素节点更新元素属性 新:virtualDom.props, 旧:oldVirtualDom.props
updateNodeElement(oldDom, virtualDom, oldVirtualDom)
}
// 1. 将拥有key属性的子元素放置在一个单独的对象中
let keyedElements = {}
// 循环旧dom节点子元素
for (let i = 0, len = oldDom.childNodes.length; i < len; i++) {
let doMElement = oldDom.childNodes[i]
if (doMElement.nodeType === 1) { // 判断元素节点
let key = doMElement.getAttribute('key')
if (key) {
keyedElements[key] = doMElement
}
}
}
// 判断是否存在有key属性的子元素
let hasNoKey = Object.keys(keyedElements).length === 0
if (hasNoKey) {
// 遍历子元素,挨个更新
virtualDom.children.forEach((child, index) => {
diff(child, oldDom, oldDom.childNodes[index])
})
} else {
// 2. 循环 virtualDOM 的子元素 获取子元素的 key 属性
virtualDom.children.forEach((child, index) => {
let key = child.props.key
if (key) {
let doMElement = keyedElements[key]
if (doMElement) {
// 3. 看看当前位置的元素是不是我们期望的元素,如果位置不一致,就插入
if (oldDom.childNodes[index] && oldDom.childNodes[index] !== doMElement) {
oldDom.insertBefore(doMElement, oldDom.childNodes[index])
}
} else {
// 新增元素
// 第一个为现有virtualDom,第二个为父级容器,第三个为原有位置元素
mountElement(child, oldDom, oldDom.childNodes[index])
}
}
})
}
// 更新完成后,比对节点数量,判断是否有删除节点的情况,删除节点发生在同意父级下
let oldChildNodes = oldDom.childNodes
// 判断旧节点数量和新节点数量大小
if (oldChildNodes.length > virtualDom.children.length) {
if (hasNoKey) {
// 没有找到有key属性的节点
// 有节点需要被删除,定位到最后一个节点
for (let i = oldChildNodes.length - 1; i > virtualDom.children.length - 1; i--) {
unmontNode(oldChildNodes[i])
}
} else {
// 处理有key属性节点删除的情况
for (let i = 0; i < oldChildNodes.length; i++) {
let oldChild = oldChildNodes[i]
let oldchidKey = oldChild._virtualDom.props.key
let found = false; // 标志位
// 去新节点中查找是否有同样key的节点
for (let n = 0; n < virtualDom.children.length; n++) {
if (oldchidKey === virtualDom.children[n].props.key) {
found = true
break
}
}
if (!found) {
unmontNode(oldChild)
}
}
}
}
} else if (virtualDom.type !== oldVirtualDom.type && typeof virtualDom !== 'function') {
// 节点类型不一样,没有必要比对
const newElement = createDomElement(virtualDom)
// 找到旧节点的父节点,替换oldDom
oldDom.parentNode.replaceChild(newElement, oldDom)
}
}
diffComponent
import mountElement from "./mountElement"
import updateComponent from "./updateComponent"
// 要更新的是组件
// 1) 组件本身的 virtualDOM 对象 通过它可以获取到组件最新的 props
// 2) 要更新的组件的实例对象 通过它可以调用组件的生命周期函数 可以更新组件的 props 属性 可以获取到组件返回的最新的 Virtual DOM
// 3) 要更新的 DOM 象 在更新组件时 需要在已有DOM对象的身上进行修改 实现DOM最小化操作 获取旧的 Virtual DOM 对象
// 4) 如果要更新的组件和旧组件不是同一个组件 要直接将组件返回的 Virtual DOM 显示在页面中 此时需要 container 做为父级容器
export default function diffComponent(virtualDom, oldComonent, oldDom, container) {
if (isSameComponet(virtualDom, oldComonent)) {
// 同一个组件做组件更新操作
console.log('same')
updateComponent(virtualDom, oldComonent, oldDom, container)
} else {
// 不是同一个组件
console.log('different')
// 不是同一个组件 直接将组件内容显示在页面中
// 这里为 mountElement 方法新增了一个参数 oldDOM
// 作用是在将 DOM 对象插入到页面前 将页面中已存在的 DOM 对象删除 否则无论是旧DOM对象还是新DOM对象都会显示在页面中
mountElement(virtualDom, container, oldDom)
}
}
function isSameComponet(virtualDOM, oldComponent) {
// virtualDOM.type 更新后的组件构造函数 oldComponent.constructor 未更新前的组件构造函数
return oldComponent && virtualDOM.type === oldComponent.constructor
}
index.js
中导出外部需要调用的方法
import createElement from "./createElement";
import render from "./render";
import Component from "./Component";
export default {
createElement,
render,
Component
}
isFunction.js
export default function isFunction(virtualDoM) {
// 组件元素的virtualDom的type值为函数
return virtualDoM && typeof virtualDoM.type === 'function'
}
isFunctionComponent.js
import isFunction from "./isFunction"
export default function isFunctionComponent(vituralDom){
const type = vituralDom.type
// 判断原型身上是否有render方法
return type && isFunction(vituralDom) && !(type.prototype && type.prototype.render)
}
mountComonent.js
import isFunction from "./isFunction";
import isFunctionComponent from "./isFunctionComponent";
import mountNativeElement from "./mountNativeElement";
export default function mountComonent(virtualDoM, container, oldDom) {
let nextVirtualDom = null
let component = null
// 区分类组件和函数组件
if (isFunctionComponent(virtualDoM)) {
// 函数组件
nextVirtualDom = buildFunctionComponent(virtualDoM)
} else {
// 类组件
nextVirtualDom = buildClassComponent(virtualDoM)
component = nextVirtualDom.component
}
// 函数组件中可能返回的还是一个组件,这种情况需要先判断处理
if (isFunction(nextVirtualDom)) {
mountComonent(nextVirtualDom, container, oldDom)
} else {
// 挂载到页面上
mountNativeElement(nextVirtualDom, container, oldDom)
}
// 处理组件的ref属性,存在就调用 ref 方法并且将组件实例对象传递给 ref 方法
if (component) {
component.componentDidMount() // 调用生命周期函数
if (component.props && component.props.ref) {
component.props.ref(component)
}
}
}
function buildFunctionComponent(virtualDoM) {
// type属性存储的是函数组件本身
return virtualDoM && virtualDoM.type(virtualDoM.props || {})
}
function buildClassComponent(virtualDOM) {
// 得到组件的实例对象,拿到render方法
const component = new virtualDOM.type(virtualDOM.props || {})
const nextVirtualDom = component.render()
// 类实例对象挂载component属性存放dom对象
nextVirtualDom.component = component
return nextVirtualDom
}
mountElement.js
import mountNativeElement from "./mountNativeElement"
import isFunction from "./isFunction"
import mountComonent from "./mountComonent"
export default function mountElement(virtualDom, container,oldDom) {
// 判断virtualDom是函组件还是普通的组件元素
if (isFunction(virtualDom)) {
mountComonent(virtualDom,container,oldDom)
} else {
mountNativeElement(virtualDom, container,oldDom)
}
}
mountNativeElement.js
import createDomElement from "./createDomeElement";
import unmontNode from "./unmontNode";
export default function mountNativeElement(virtualDom, container, oldDom) {
let newElement = createDomElement(virtualDom)
// 将转换之后的DOM对象放置在页面中,如:key属性存在时,插入原有位置元素之前
if (oldDom) {
container.insertBefore(newElement, oldDom)
} else {
// 把组装好的Dom对象挂载到页面中
container.appendChild(newElement)
}
// 更新类组件时,会有oldDom参数传入,此时如果有传入,就是对diffComponent方法中比对情况处理
if (oldDom) {
// 判断旧的DOM对象是否存在 如果存在 删除
unmontNode(oldDom)
}
// 获取类组件实例对象,类组件特有mountComonent方法中buildClassComponent方法里面挂载
let componet = virtualDom.component
if (componet) {
// 挂载时,通过调用setDom方法挂载旧的Dom对象
componet.setDom(newElement)
}
}
render.js
import diff from "./diff"
// oldDom默认值为挂载到页面的root第一个子元素,就是页面的元素,也就是oldDom
export default function render(virtualDom, container, oldDom = container.firstChild) {
diff(virtualDom, container, oldDom)
}
unmontNode.js
export default function unmontNode(node) {
// 获取节点的 _virtualDOM 对象
const virtualDOM = node._virtualDom
// 文本节点可以直接删除
if (virtualDOM.type === 'text') {
node.remove()
// 删除之后程序结束
return
}
// --------处理元素节点删除情况--------
// 判断节点是否由组件生成
let component = virtualDOM.component
// 如果 component 存在 就说明节点是由组件生成的
if (component) {
component.componentWillUnmount()
}
// 判断节点身上是否有ref属性
if (virtualDOM.props && virtualDOM.props.ref) {
virtualDOM.props.ref(null)
}
// 判断节点属性中是否由事件属性
Object.keys(virtualDOM.props).forEach(propName => {
if (propName.slice(0, 2) === "on") {
const eventName = propName.toLowerCase().slice(0, 2)
// 去除事件属性对应值
const eventHandler = virtualDOM.props[propName]
node.removeEventListener(eventName, eventHandler)
}
})
// 判断节点是否有子节点,采用递归方式处理
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
unmontNode(node.childNodes[i])
i--
}
}
// 上面都是处理元素节点的特殊情况,处理完之后删除node
node.remove()
}
updateComponent.js
import diff from "./diff"
export default function updateComponent(virtualDom, oldComponent, oldDom, container) {
oldComponent.componentWillReceiveProps(virtualDom.props)
if (oldComponent.shouldComponentUpdate(virtualDom.props)) {
// 未更前前的props
let prevProps = oldComponent.props
oldComponent.componentWillUpdate(virtualDom.props)
// 组件更新,调用componet的实例方法
oldComponent.updateProps(virtualDom.props)
// 获取组件返回的最新的 virtualDOM
let nextVirtualDom = oldComponent.render()
// 更新 component 组件实例对象
nextVirtualDom.component = oldComponent
diff(nextVirtualDom, container, oldDom)
oldComponent.componentDidUpdate(prevProps)
}
}
updateNodeElement.js
// 第三个参数oldVirtualDom在设置属性的时候不传,值是undefined,在更新属性的时候传
export default function updateNodeElement(newElement, virtualDom, oldVirtualDom = {}) {
// 获取节点对应的属性对象
const newProps = virtualDom.props || {}
// 旧的节点属性
const oldProps = oldVirtualDom.props || {}
// 遍历属性
Object.keys(newProps).forEach(propName => {
// 获取属性值
const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
// 设置新属性和更新属性值,可以放在一个判断条件中处理,当设置新属性时,oldVirtualDom不存在,virtualDom和 oldVirtualDom不相等
if (newPropsValue !== oldPropsValue) {
// 判断属性类型,转换如:onClick ->click
if (propName.slice(0, 2) === 'on') { // 事件属性
// 获取事件名称
const eventName = propName.toLowerCase().slice(2)
// 为元素添加事件
newElement.addEventListener(eventName, newPropsValue)
// 更新事件属性时,直接删除原有的事件的处理函数
if (oldPropsValue) {
newElement.removeEventListener(eventName, oldPropsValue)
}
} else if (propName === 'value' || propName === 'checked') { // 标签特有属性
newElement[propName] = newPropsValue
} else if (propName !== 'children') { // props数据结构中有children属性,但是它不属于属性,去除这种情况
if (propName === 'className') {
// 类名
newElement.setAttribute('class', newPropsValue)
} else {
// 普通属性
newElement.setAttribute(propName, newPropsValue)
}
}
}
})
// 判断属性是否删除,当有属性被删除时,oldProps中会有,而newProps没有
Object.keys(oldProps).forEach(propName => {
const newPropsValue = newProps[propName]
const oldPropsVlaue = oldProps[propName]
if (!newPropsValue) {
// 不存在,属性被删除
if (propName.slice(0, 2) === 'on') {
// 事件属性被删除
const eventName = propName.toLowerCase().slice(2)
newElement.removeEventListener(eventName, oldPropsVlaue) // 移除事件函数
} else if (propName !== 'children') {
if (propName === 'value') {
newElement.value = ''
} else {
newElement.removeAttribute(propName)
}
}
}
})
}
updateTextNode.js
export default function updateTextNode(virtualDom, oldVirtualDom, oldDom) {
// 更新文本内容
if (virtualDom.props.textContent !== oldVirtualDom.props.textContent) {
oldDom.textContent = virtualDom.props.textContent
oldDom._virtualDom = virtualDom // oldDome的virtualDom也需要被更新
}
}