代码构造
在 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*/ /* <div id ='container'> <p>hello world</p> </div> */ // 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第一个子元素,就是页面的元素,也就是oldDomexport 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也须要被更新 }}