手写简易版React框架
1.基础环境的搭建
1.1首先将自己配置好的webpack环境搭好,目录结构如下:
1.2 React最基本的功能就是渲染jsx语法,其中用到了babel-loader,我们这里在webpack配置文件里已经配置好了babel-loader。然后新建一个文件夹名为my-react,在里面建立一个index.js写我们自己的react。新建一个react-dom文件夹,在里面新建一个index.js写我们自己的react-dom。
babel的作用: 首先把相关的代码转换->调用React.createElement()方法,调用的时候会把转换后的结果以参数的新式传递给该方法
2.编写createElement方法,便于解析jsx语法
2.1在入口文件index.js中简单写一段jsx语法的代码,并做打印。
import React from './my-react/index'
import ReactDOM from './react-dom/index'
const elem = (
<div>hello</div>
)
console.log(elem);
2.2 在my-react中编写createElement方法
打印时,发现控制台报错:Uncaught TypeError: _index2.default.createElement is not a function。
经查询:React.createElement方法来自于React框架,作用就是返回对应的虚拟DOM。
在my-react中写createElement方法,并将我们自己编写的React实例对象导出。
//my-react/index.js
function createElement(tag,props,...children) {
return new Element(tag,props,children)
}
class Element {
constructor(tag,props,children) {
this.tag = tag
this.props = props
this.children = children
}
}
const React = {
createElement
}
export default React
3.编写ReactDOM.render方法用来渲染传入的对象,并挂载到DOM节点上。
3.1普通文本的渲染处理
/**
* //根据传入的虚拟DOM,返回真实DOM节点
* @param {虚拟DOM} vdom
*/
function createDom(vdom) {
if (vdom == 'undefined') return
//普通文本的处理
if (typeof vdom == 'string' || typeof vdom == 'number') {
return document.createTextNode(vdom)
}
}
/**
*
* @param {虚拟DOM} vdom
* @param {容器} container
*/
function render(vdom,container) {
//根据虚拟DOM转换为真实DOM
const dom = createDom(vdom)
//将真实DOM添加到容器DOM中
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM
3.2 jsx虚拟DOM渲染处理
/**
* //根据传入的虚拟DOM,返回真实DOM节点
* @param {虚拟DOM} vdom
*/
function createDom(vdom) {
if (vdom == 'undefined') return
//普通文本的处理
if (typeof vdom == 'string' || typeof vdom == 'number') {
return document.createTextNode(vdom)
}
//jsx对象处理
else if (typeof vdom.tag == 'string') {
const dom = document.createElement(vdom.tag)
if (vdom.props) {
//给dom添加属性
for (let key in vdom.props) {
setProperty(dom,key,vdom.props[key])
}
}
//递归处理子节点
if (vdom.children && vdom.children.length > 0) {
vdom.children.forEach(item => render(item,dom))
}
return dom
}
}
/**
*
* @param {属性名} key
* @param {属性值} value
* @param {DOM节点} dom
*/
function setProperty(dom,key,value) {
//事件的处理 如果属性名以on开头则是事件,再将事件的key全变小写
key.startsWith('on') && (key = key.toLowerCase())
//样式的处理
if (key == 'style' && value) {
if (typeof value == 'string') {
//如果value是字符串
dom.style.cssText = value
} else if (typeof value == 'object') {
//如果value是对象
for (let attr in value) {
dom.style[attr] = value[attr]
}
}
} else {
//样式以外的处理
dom[key] = value
}
}
/**
*
* @param {虚拟DOM} vdom
* @param {容器} container
*/
function render(vdom,container) {
//根据虚拟DOM转换为真实DOM
const dom = createDom(vdom)
//将真实DOM添加到容器DOM中
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM
3.3渲染组件处理
我们将组件分为函数组件和类组件,首先我们写出如何渲染类组件的方法。在index.js入口文件写一个类组件:
//index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'
// function App1(props) {
// return (
// <div className="App1">
// <h1>App1 function组件</h1>
// </div>
// )
// }
class App2 extends Component {
constructor(props){
super(props)
}
render() {
return (
<div>
<p>App2 class组件</p>
</div>
)
}
}
// const elem = (
// <div className="App" style={{border: '1px solid #ccc'}}>
// <h1 className="title" style="color:red;" onClick={()=>alert(111)}>hello</h1>
// </div>
// )
ReactDOM.render(<App2/>, document.getElementById('root'))
类组件需要继承自Component类,所以我们去my-react中创建一个Component类,并导出。
//my-react/index.js
export class Element {
constructor(tag,props,children) {
this.tag = tag
this.props = props
this.children = children
}
}
class Component {
constructor(props = {}) {
this.props = props,
this.state = {}
}
}
function createElement(tag,props,...children) {
return new Element(tag,props,children)
}
const React = {
createElement,
Component
}
export default React
export { Component }
此时,仍然无法渲染这个组件,因为我们在ReactDOM.render方法中还没有对如何渲染类组件做相应的处理。此时在react-dom的index.js中来处理类组件相关的方法。
//react-dom/index.js
/**
* //根据传入的虚拟DOM,返回真实DOM节点
* @param {虚拟DOM} vdom
*/
function createDom(vdom) {
if (vdom == 'undefined') return
//普通文本的处理
...
//jsx对象处理
...
//组件的处理
else if (typeof vdom.tag == 'function') {
//创建组件的实例
const instance = createComponentInstance(vdom.tag,vdom.props)
//生成实例对应的DOM节点
createDomForComponentInstance(instance)
return instance.dom
}
}
/**
*
* @param {属性名} key
* @param {属性值} value
* @param {DOM节点} dom
*/
function setProperty(DOM,key,value) {
//事件的处理 如果属性名以on开头则是事件,再将事件的key全变小写
...
//样式的处理
...
}
function createComponentInstance(comp,props) {
let instance = null
//类组件 直接用new生成一个组件实例
instance = new comp(props)
return instance
}
/**
*
* @param {组件实例} instance
*/
function createDomForComponentInstance(instance) {
//获取到虚拟DOM并挂载到实例上 因为类组件的render方法中return的就是jsx对象
//所以直接调用render方法获取获取虚拟DOM
instance.vdom = instance.render()
//生成真实的DOM节点,并且也挂载到实例上
instance.dom = createDom(instance.vdom)
}
/**
*
* @param {虚拟DOM} vdom
* @param {容器} container
*/
function render(vdom,container) {
//根据虚拟DOM转换为真实DOM
const dom = createDom(vdom)
//将真实DOM添加到容器DOM中
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM
对类组件处理完成以后,再对函数组件进行相应的处理。函数组件处理的方式与类组件有所不同。
我们将函数组件传到ReactDOM.render方法中,应该对函数组件和类组件做不同的区分,然后分别处理。
//入口文件 index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'
function App1(props) {
return (
<div className="App1">
<h1>App1 function组件</h1>
</div>
)
}
ReactDOM.render(<App1/>, document.getElementById('root'))
在处理函数组件的时候,它与处理类组件的不同在于,无法直接用new关键字创建一个实例。
所以在生成组件实例的createComponentInstance方法中,我们通过组件的原型对象上是否有render方法来判断传过来的组件是类组件还是函数组件。
原型对象上有render方法则是类组件,直接用new关键字创建一个实例
否则是函数组件,我们引入my-react中的Element类,通过new Element()生成一个Element类实例。该实例constructor指向自身,并添加一个render方法,返回的是调用自身的结果,即jsx对象。方便调用render方法创建虚拟DOM。
//react-dom/index.js
/**
* //根据传入的虚拟DOM,返回真实DOM节点
* @param {虚拟DOM} vdom
*/
function createDom(vdom) {
if (vdom == 'undefined') return
//普通文本的处理
...
//jsx对象处理
...
//组件的处理
else if (typeof vdom.tag == 'function') {
//类组件和函数组件的tag属性都等于'function'
//所以都能进入到这个分支里
//不同之处在创建组件的实例方法里
//创建组件的实例
const instance = createComponentInstance(vdom.tag,vdom.props)
//生成实例对应的DOM节点
createDomForComponentInstance(instance)
return instance.dom
}
}
/**
*
* @param {属性名} key
* @param {属性值} value
* @param {DOM节点} dom
*/
function setProperty(dom,key,value) {
//事件的处理 如果属性名以on开头则是事件,再将事件的key全变小写
...
//样式的处理
...
}
function createComponentInstance(comp,props) {
let instance = null
if (comp.prototype.render) {
//组件的原型对象上有render方法,则是类组件
//类组件 直接用new生成一个组件实例
instance = new comp(props)
} else {
//是函数组件
instance = new Element(comp)
instance.constructor = comp
instance.render = function (props) {
return comp(props)
}
}
return instance
}
/**
*
* @param {组件实例} instance
*/
function createDomForComponentInstance(instance) {
//获取到虚拟DOM并挂载到实例上 因为类组件的render方法中return的就是jsx对象
//所以直接调用render方法获取获取虚拟DOM
instance.vdom = instance.render()
//生成真实的DOM节点,并且也挂载到实例上
instance.dom = createDom(instance.vdom)
}
/**
*
* @param {虚拟DOM} vdom
* @param {容器} container
*/
function render(vdom,container) {
//根据虚拟DOM转换为真实DOM
const dom = createDom(vdom)
//将真实DOM添加到容器DOM中
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM
4.编写组件的更新以及this.setState方法
4.1简单编写this.setState方法
首先对之前的代码稍做测试,我们添加一个state,然后添加一个点击事件,结果是没有问题的。
//index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'
class App2 extends Component {
constructor(props){
super(props)
this.state = {
num: 0
}
}
handelClick() {
console.log(111);
}
render() {
return (
<div>
<p>App2 class组件======{this.state.num}</p>
<button onClick={this.handelClick.bind(this)}>按钮</button>
</div>
)
}
}
ReactDOM.render(<App2/>, document.getElementById('root'))
然后在点击事件里更新数据,调用this.setState方法,但是我们还没有该方法。
思考一下,该方法因为所有的组件都能调用,所以应该写在Component类里,这样所有的组件都有了这个方法。
//index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'
class App2 extends Component {
constructor(props){
super(props)
this.state = {
num: 0
}
}
handelClick() {
this.setState({
num: this.state.num + 1
})
}
render() {
return (
<div>
<p>App2 class组件======{this.state.num}</p>
<button onClick={this.handelClick.bind(this)}>按钮</button>
</div>
)
}
}
ReactDOM.render(<App2/>, document.getElementById('root'))
//my-react/index.js
export class Element {
...
}
class Component {
constructor(props = {}) {
this.props = props,
this.state = {}
}
setState(updatedState) {
//updatedState 是传入的需要更新的数据
//合并对象 将新的state合并到旧的state上
Object.assign(this.state,updatedState)
console.log(this.state);//{num: 1}
//再调用render方法重新生成虚拟DOM
const newVdom = this.render()
//根据虚拟DOM生成DOM
const newDom = createDom(newVdom)
//替换DOM节点 当调用过createDom方法后,组件实例上就已经挂载了DOM节点
if (this.dom.parentNode) {
this.dom.parentNode.replaceChild(newDom, this.dom)
}
//将最新的DOM节点挂载到实例上
this.dom = newDom
}
}
...
...
export default React
export { Component }
此时我们实现了更新数据,但是和官方React不同的是,React是用diff算法,根据虚拟DOM进行只更新需要更新的数据。
而我们现在是将整个DOM节点进行了更新。
4.2通过diff算法找出新旧虚拟DOM 之间的区别,然后只更新需要更新的DOM。
在react-dom文件夹里新建一个diff.js、patch.js和patches-type.js.
具体的diff算法对两棵树结构进行深度优先遍历,找出不同。这里可直接复制来使用。
//react-dom/diff.js
import { Element } from '../my-react'
import { PATCHES_TYPE } from './patches-type'
const diffHelper = {
Index: 0,
isTextNode: (eleObj) => {
return !(eleObj instanceof Element);
},
diffAttr: (oldAttr, newAttr) => {
let patches = {}
for (let key in oldAttr) {
if (oldAttr[key] !== newAttr[key]) {
// 可能产生了更改 或者 新属性为undefined,也就是该属性被删除
patches[key] = newAttr[key];
}
}
for (let key in newAttr) {
// 新增属性
if (!oldAttr.hasOwnProperty(key)) {
patches[key] = newAttr[key];
}
}
return patches;
},
diffChildren: (oldChild, newChild, patches) => {
if (newChild.length > oldChild.length) {
// 有新节点产生
patches[diffHelper.Index] = patches[diffHelper.Index] || [];
patches[diffHelper.Index].push({
type: PATCHES_TYPE.ADD,
nodeList: newChild.slice(oldChild.length)
});
}
oldChild.forEach((children, index) => {
dfsWalk(children, newChild[index], ++diffHelper.Index, patches);
});
},
dfsChildren: (oldChild) => {
if (!diffHelper.isTextNode(oldChild)) {
oldChild.children.forEach(children => {
++diffHelper.Index;
diffHelper.dfsChildren(children);
});
}
}
}
export function diff(oldTree, newTree) {
// 当前节点的标志 每次调用Diff,从0重新计数
diffHelper.Index = 0;
let patches = {};
// 进行深度优先遍历
dfsWalk(oldTree, newTree, diffHelper.Index, patches);
// 返回补丁对象
return patches;
}
function dfsWalk(oldNode, newNode, index, patches) {
let currentPatches = [];
if (!newNode) {
// 如果不存在新节点,发生了移除,产生一个关于 Remove 的 patch 补丁
currentPatches.push({
type: PATCHES_TYPE.REMOVE
});
// 删除了但依旧要遍历旧树的节点确保 Index 正确
diffHelper.dfsChildren(oldNode);
} else if (diffHelper.isTextNode(oldNode) && diffHelper.isTextNode(newNode)) {
// 都是纯文本节点 如果内容不同,产生一个关于 textContent 的 patch
if (oldNode !== newNode) {
currentPatches.push({
type: PATCHES_TYPE.TEXT,
text: newNode
});
}
} else if (oldNode.tag === newNode.tag) {
// 如果节点类型相同,比较属性差异,如若属性不同,产生一个关于属性的 patch 补丁
let attrs = diffHelper.diffAttr(oldNode.props, newNode.props);
// 有attr差异
if (Object.keys(attrs).length > 0) {
currentPatches.push({
type: PATCHES_TYPE.ATTRS,
attrs: attrs
});
}
// 如果存在孩子节点,处理孩子节点
diffHelper.diffChildren(oldNode.children, newNode.children, patches);
} else {
// 如果节点类型不同,说明发生了替换
currentPatches.push({
type: PATCHES_TYPE.REPLACE,
node: newNode
});
// 替换了但依旧要遍历旧树的节点确保 Index 正确
diffHelper.dfsChildren(oldNode);
}
// 如果当前节点存在补丁,则将该补丁信息填入传入的patches对象中
if (currentPatches.length) {
patches[index] = patches[index] ? patches[index].concat(currentPatches) : currentPatches;
}
}
//react-dom/patch.js
import { Element } from '../my-react'
import { setProperty, createDom } from './index'
import { PATCHES_TYPE } from './patches-type'
export function patch(node, patches) {
let patchHelper = {
Index: 0
}
dfsPatch(node, patches, patchHelper);
}
function dfsPatch(node, patches, patchHelper) {
let currentPatch = patches[patchHelper.Index];
node.childNodes.forEach(child => {
patchHelper.Index++
dfsPatch(child, patches, patchHelper);
});
if (currentPatch) {
doPatch(node, currentPatch);
}
}
function doPatch(node, patches) {
patches.forEach(patch => {
switch (patch.type) {
case PATCHES_TYPE.ATTRS:
for (let key in patch.attrs) {
if (patch.attrs[key] !== undefined) {
setProperty(node, key, patch.attrs[key]);
} else {
node.removeAttribute(key);
}
}
break;
case PATCHES_TYPE.TEXT:
node.textContent = patch.text;
break;
case PATCHES_TYPE.REPLACE:
let newNode = patch.node instanceof Element ? createDom(patch.node) : document.createTextNode(patch.node);
node.parentNode.replaceChild(newNode, node);
break;
case PATCHES_TYPE.REMOVE:
node.parentNode.removeChild(node);
break;
case PATCHES_TYPE.ADD:
patch.nodeList.forEach(newNode => {
let n = newNode instanceof Element ? createDom(newNode) : document.createTextNode(newNode);
node.appendChild(n);
});
break;
default:
break;
}
})
}
//react-dom/patches-type.js
export const PATCHES_TYPE = {
ATTRS: 'ATTRS',
REPLACE: 'REPLACE',
TEXT: 'TEXT',
REMOVE: 'REMOVE',
ADD: 'ADD'
}
把diff算法相关的文件引入完成以后,我们对my-react中的setState方法进行一个修改。
//my-react/index.js
import { createDom } from '../react-dom/index'
import { diff } from '../react-dom/diff'
import { patch } from '../react-dom/patch'
export class Element {
constructor(tag,props,children) {
this.tag = tag
this.props = props
this.children = children
}
}
class Component {
constructor(props = {}) {
this.props = props,
this.state = {}
}
/**
*
* @param {传入的需要更新的数据} updatedState
*/
setState(updatedState) {
//合并对象 将新的state合并到旧的state上
Object.assign(this.state,updatedState)
//再调用render方法重新生成新的虚拟DOM
const newVdom = this.render()
//根据diff算法找出新旧虚拟DOM的区别
const patches = diff(this.vdom,newVdom)
//根据不同,更新DOM节点
patch(this.dom,patches)
//将最新的虚拟DOM挂载到实例上
this.vdom = newVdom
}
}
function createElement(tag,props,...children) {
return new Element(tag,props,children)
}
const React = {
createElement,
Component
}
export default React
export { Component }
至此,我们已经完成了简易的setState的方法。
但是经测试,发现一个错误
这是因为我们在使用与diff算法相关方法时,使用到了react-dom里的setProperty方法,但是我们在定义该方法时,并没有导出。所以我们进入react-dom/index.js中,将该方法导出。
//react-dom/index.js
...
export function createDom(vdom) {
...
}
...
//将此方法导出
export function setProperty(dom,key,value) {
...
}
再次测试,没有任何问题,而且更新也是根据diff算法进行局部的更新。
5.生命周期
我们先看一下react的生命周期函数
其中,最常用的几个生命周期函数为constructor、render、componentDidMount、componentDidUpdated。
前两个函数,我们在之前写组件相关的代码时已经有写过,并且在相应的时间进行了调用。
因为我们写的只是简易版的react,所以其余的不常用的我们暂时不写。现在只需要写componentDidMount和componentDidUppdated这两个函数。
5.1 componentDidMount
首先分析,componentDidMount只在组件挂载完成后,执行一次。之后如果数据发生更新,则不再执行。
所以我们应该在react-dom中编写该方法。componentDidMount是在组件挂载完成以后执行,所以我们找到在组件实例中挂载虚拟DOM的方法。在此处添加componentDidMount方法。
//react-dom/index.js
import { Element } from '../my-react/index'
/**
* //根据传入的虚拟DOM,返回真实DOM节点
* @param {虚拟DOM} vdom
*/
export function createDom(vdom) {
...
//组件的处理
else if (typeof vdom.tag == 'function') {
//创建组件的实例
const instance = createComponentInstance(vdom.tag,vdom.props)
//生成实例对应的DOM节点
createDomForComponentInstance(instance)
return instance.dom
}
}
/**
*
* @param {属性名} key
* @param {属性值} value
* @param {DOM节点} dom
*/
export function setProperty(dom,key,value) {
...
}
function createComponentInstance(comp,props) {
let instance = null
if (comp.prototype.render) {
//组件的原型对象上有render方法,则是类组件
//类组件 直接用new生成一个组件实例
instance = new comp(props)
} else {
//是函数组件
instance = new Element(comp)
instance.constructor = comp
instance.render = function (props) {
return comp(props)
}
}
return instance
}
/**
*
* @param {组件实例} instance
*/
function createDomForComponentInstance(instance) {
//获取到虚拟DOM并挂载到实例上 因为类组件的render方法中return的就是jsx对象
//所以直接调用render方法获取获取虚拟DOM
instance.vdom = instance.render()
//如果实例上没有挂载过DOM,则是第一次创建
//之后再发生更新,则不会进入到该判断分支,就不会执行componentDidMount方法
if (!instance.dom) {
typeof instance.componentDidMount == 'function' && instance.componentDidMount()
}
//生成真实的DOM节点,并且也挂载到实例上
instance.dom = createDom(instance.vdom)
}
/**
*
* @param {虚拟DOM} vdom
* @param {容器} container
*/
function render(vdom,container) {
//根据虚拟DOM转换为真实DOM
const dom = createDom(vdom)
//将真实DOM添加到容器DOM中
container.appendChild(dom)
}
const ReactDOM = {
render
}
export default ReactDOM
5.2 componentDidUpdated
分析该方法,在每次组件更新完成后都会执行。所以我们找到my-react的index.js中,在setState方法的最后添加componentDidUpdated方法。
//my-react/index.js
import { createDom } from '../react-dom/index'
import { diff } from '../react-dom/diff'
import { patch } from '../react-dom/patch'
export class Element {
constructor(tag,props,children) {
this.tag = tag
this.props = props
this.children = children
}
}
class Component {
constructor(props = {}) {
this.props = props,
this.state = {}
}
/**
*
* @param {传入的需要更新的数据} updatedState
*/
setState(updatedState) {
//合并对象 将新的state合并到旧的state上
Object.assign(this.state,updatedState)
//再调用render方法重新生成新的虚拟DOM
const newVdom = this.render()
//根据diff算法找出新旧虚拟DOM的区别
const patches = diff(this.vdom,newVdom)
//根据不同,更新DOM节点
patch(this.dom,patches)
this.vdom = newVdom
//生命周期函数componentDidUpdated
typeof this.componentDidUpdated == 'function' && this.componentDidUpdated()
}
}
function createElement(tag,props,...children) {
return new Element(tag,props,children)
}
const React = {
createElement,
Component
}
export default React
export { Component }
6. 完善setState方法
目前我们写的setState方法已经能完成普通的更新操作。但是还有两个地方需要改进。
首先,用官方的react做一个小demo。
组件的初始数据为: num: 0, score: 100
点击按钮,调用两次setState,分别将num和score加1,并打印this.state
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
class App2 extends Component {
constructor(props){
super(props)
this.state = {
num: 0,
score: 100
}
}
handelClick() {
this.setState({
num: this.state.num + 1
})
this.setState({
score: this.state.score + 1
})
console.log(this.state.num);
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
render() {
return (
<div>
<p>{this.state.num}===={this.state.score}</p>
<button onClick={this.handelClick.bind(this)}>按钮</button>
</div>
)
}
}
ReactDOM.render(<App2/>, document.getElementById('root'))
由下图结果,我们发现了两个问题。
一、在点击事件中,先setState再打印this.state。打印出的是更新之前的旧数据。证明setState方法是异步的。而我们自己写的setState方法是同步的。
二、我们调用了两次setState方法,但是componentDidUpdated方法只执行了一次。证明React将两次更新操作合并处理了,只进行了一次更新,就把我们想要更新的两个数据都成功更新了。而我们自己写的setState则没有做这样的处理。
6.1 利用任务队列完善setState方法
我们利用任务队列的思想,当组件中调用setState方法时,我们先不直接进行更新操作,而是将要更新的数据和要更新的组件做为一个大的对象,放到一个任务队列中。
当多次调用setState方法,则一直进行入队操作。当进入队列完毕,将所有要更新的数据做一个合并,再统一进行一次更新操作。
首先,修改my-react的index.js中的setState方法,将之前的更新逻辑注掉。
在setState方法中,每被调用一次,我们就调用enqueue方法。
//my-react/index.js
import { createDom } from '../react-dom/index'
import { diff } from '../react-dom/diff'
import { patch } from '../react-dom/patch'
import { enqueue } from './queue'
export class Element {
...
}
class Component {
...
/**
*
* @param {传入的需要更新的数据} updatedState
*/
setState(updatedState) {
/*
//合并对象 将新的state合并到旧的state上
Object.assign(this.state,updatedState)
//再调用render方法重新生成新的虚拟DOM
const newVdom = this.render()
//根据diff算法找出新旧虚拟DOM的区别
const patches = diff(this.vdom,newVdom)
//根据不同,更新DOM节点
patch(this.dom,patches)
this.vdom = newVdom
//生命周期函数componentDidUpdated
typeof this.componentDidUpdated == 'function' && this.componentDidUpdated()
*/
//进入更新任务队列
enqueue(updatedState, this)
}
update() {
//调用render方法重新生成新的虚拟DOM
const newVdom = this.render()
//根据diff算法找出新旧虚拟DOM的区别
const patches = diff(this.vdom,newVdom)
//根据不同,更新DOM节点
patch(this.dom,patches)
this.vdom = newVdom
//生命周期函数componentDidUpdated
typeof this.componentDidUpdated == 'function' && this.componentDidUpdated()
}
}
function createElement(tag,props,...children) {
...
}
const React = {
createElement,
Component
}
export default React
export { Component }
在my-react中新建一个queue.js
//my-react/queue.js
//保存需要更新的数据和组件的对象的队列
const stateQueue = []
//保存需要更新的组件的队列
const compQueue = []
//入列方法
export function enqueue(updatedState, comp) {
//如果任务队列为0, 进行出列操作
if (stateQueue.length == 0) {
//当第一次进入,必定会进入到该判断分支里
//但是因为是异步,所以不会执行
//当所有同步执行完成后,也就是所有需要更新的数据都入列了
//再执行出列操作,并对所有要更新的数据做一个合并
//异步的调用出列函数
setTimeout(flush,0)
}
stateQueue.push({
updatedState,
comp
})
//判断组件队列中是否已经有该组件
const hasComp = compQueue.some(item => item == comp)
//如果组件队列中没有,才push进去
if (!hasComp) {
compQueue.push(comp)
}
}
//出列函数
function flush() {
let item, comp
//循环出列 并合并对象
while (item = stateQueue.shift()) {
const { updatedState, comp } = item
//合并对象 将所有需要更新的数据都合并到组件实例的state属性上
Object.assign(comp.state, updatedState)
}
//拿到需要更新的组件
while (comp = compQueue.shift()) {
//调用组件自身的update方法,更新数据及虚拟DOM
comp.update()
}
}
总结
至此,已经完成了react框架大部分基本的功能。
总结一下,首先,我们在reactDOM.render方法中,根据传入的不同的类型,生成不同的DOM节点,并对传入的属性做递归处理,挂载在容器DOM中,渲染不同的结果。重要的就是对jsx对象的渲染,在这里用到了babel,因为babel能解析jsx语法,通过调用createElement方法生成虚拟DOM对象。
由此,我们我们才能渲染组件。对组件我们又分别对函数组件和类组件做了不同的处理。最后都是将生成的虚拟DOM和真实DOM挂载到组件的实例上,以便于后期diff算法进行计算更新。
然后我们对更新方法进行了处理。利用到了diff算法进行虚拟DOM的比对,最后只更新需要更新的部分。并分别在组件挂载完毕后和组件更新完毕后添加了componentDidMount和componentDidUpdated这两个生命周期函数。
最后完善了setState方法。我们利用任务队列的思想,将每次需要更新的数据放到任务队列中,之后再进行对象合并,将所有需要更新的数据,合并到组件实例的state中,做一个统一的更新操作。这样,同时多次调用setState时,只需进行一次更新操作,就能把所有要更新的数据全部更新。在进行出列操作时,利用定时器setTimeout,来将setState方法变成了异步方法。