手寫簡易版React框架
手寫簡易版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方法變成了非同步方法。
相關文章
- 手寫Spring MVC框架(一) 實現簡易版mvc框架SpringMVC框架
- 手寫Android事件匯流排框架Eventbus(簡易版)Android事件框架
- 動手寫個Retrofit簡易版
- 閉關修煉180天--手寫持久層框架(mybatis簡易版)框架MyBatis
- React簡易版老虎機React
- 手寫簡易PromisePromise
- 手寫簡易webpackWeb
- react筆記--手動實現一個react-router(簡易版)React筆記
- 手寫mini版MVC框架MVC框架
- 手寫一個簡易的WebpackWeb
- 基於React搭建一個簡易版豆瓣React
- 【like-react】手寫一個類似 react 的框架React框架
- react/vue中dom-diff簡易版實現ReactVue
- 手寫簡易打包工具webpack-demoWeb
- 簡易版的Spring框架之IOC簡單實現Spring框架
- Java多執行緒之Executor框架和手寫簡易的執行緒池Java執行緒框架
- 【肥朝】如何手寫實現簡易的Dubbo?
- C++簡易計算器自寫棧版C++
- 使用Go寫一個簡易的MVC的Web框架GoMVCWeb框架
- goioc:一個使用 Go 寫的簡易的 ioc 框架Go框架
- [React]簡易留言板React
- 手擼一個簡易Android資料庫框架Android資料庫框架
- 手把手教你寫一個簡易的微前端框架前端框架
- 自己手寫一個SpringMVC框架(簡化)SpringMVC框架
- 基於React跑一個簡易版九宮格抽獎React
- 簡易版管道模式模式
- 自制簡易前端MVC框架前端MVC框架
- 手寫一個簡易的多週期 MIPS CPU
- 入門 go 隨手寫了個簡易聊天室Go
- C#基於Mongo的官方驅動手擼一個Super簡易版MongoDB-ORM框架C#MongoDBORM框架
- 手寫mybatis框架MyBatis框架
- 自己動手寫一個簡單的MVC框架MVC框架
- 簡易版 vue實現Vue
- 簡易版“推箱子”遊戲遊戲
- Google日曆簡易版Go
- 簡易RPC框架實現RPC框架
- [React Native]使用App Center CLI釋出CodePush更新--iOS簡易版React NativeAPPiOS
- 手寫React(下週補坑)React