簡版react

1. 項目基本準備工作

1.1 創建項目

利用npx create-react-app my_react命令創建項目

項目已經放到github:https://github.com/caozhongjie/simple-react.git 有什麼不對的或者建議或者疑惑,歡迎指出!

!!!在倉庫代碼中,將 react-dom.js 和 react.js 合併爲了 src/react/index.js

1.2 項目結構

將一些用不到的文件刪除後,目錄變成這樣

 

 

 

此時的index.js

import React from 'react';
import ReactDOM from 'react-dom';
​
ReactDOM.render(
  "sunny",
  document.getElementById('root')
);
 

2.創建react.js和react-dom.js文件

 

 

 

我們就可以把需要引入react和react-dom的改成自己創建的文件啦

import React from './react';
import ReactDOM from './react-dom';
​
ReactDOM.render(
  "sunny",
  document.getElementById('root')
);
​

 

3.完成react-dom

我們在index.js文件中

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

 

以這樣的方式使用ReactDOM,說明他有render這個方法。

所以我們可以這樣實現react-dom

// react-dom.js
let ReactDOM = {
    render
}
function render(element,container){
    container.innerHTML = `<span>${element}</span>`
    
}
​
export default ReactDOM

 

我們看下運行結果

 

 

 

 

可喜可賀!萬里長城邁出了第一步

好了,現在我們給每一個 元素打上 一個標記 ,這樣的話 就可以通過這個標記 辨別出與其他 元素的關係,也可以直接通過這標記找到該元素了。

就像下面這張圖一樣,是不是就直接看出0.0和0.1的父節點就是0了呢?

 

 

 

// react-dom.js
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    container.innerHTML = `<span data-react_id=${ReactDOM.rootIndex}>${element}</span>`
}
​
export default ReactDOM

 

如代碼所示,我們給每一個元素添加了一個標記data-react_id

運行,發現確實標記成功了,哈哈哈

 

 

 

4. 重構render方法

我們前面的render方法

function render(element,container){
    container.innerHTML = `<span data-react_id=${ReactDOM.rootIndex}>${element}</span>`
}

 

默認傳入的element爲字符串, 但是實際情況是有可能是 文本節點,也有可能是DOM節點,也有可能是 自定義組件。

所以我們實現一個createReactUnit方法,將element傳入,讓它來判斷element是什麼類型的節點,。根據不同的數據類型來執行不同的渲染邏輯,並且添加對應的方法和屬性的對象 。例如,我們的element是字符串類型,那麼就返回一個字符串類型的對象,而這個對象自身有element 屬性和getMarkUp方法,這個getMarkUp方法,將element轉化成真實的dom

其實你也可以簡單地認爲createReactUnit 方法 就是 爲 element 對象添加 一個getMarkUp方法

// react-dom.js
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createReactUnit(element)
    let markUp = unit.getMarkUp();// 用來返回HTML標記
    $(container).html(markUp)
}
•
export default ReactDOM

 

如代碼所示,將element傳入createUnit方法,獲得的unit是一個對象

{
  _currentElement:element,
  getMarkUp(){
    ...
  }
}

 

再執行 unit的getMarkUp方法,獲得到 真實的dom,然後就可以掛載到container上去啦!

注意,如果傳入render的element是字符串"sunny",

import React from './react';
import ReactDOM from './react-dom';
​
ReactDOM.render(
  "sunny",
  document.getElementById('root')
);
​

 

也就是說傳入createUnit的element是字符串"sunny",那麼返回的unit是

{
    _currentElement:"sunny",
    getMarkUp(){
        
    }
}

 

那怎麼寫這個createUnit呢?

5. 實現createReactUnit方法

我們創建一個新的文件叫做unit.js

 

 

 

// Unit.js
class Unit{
   
}
class ReactTextUnit extends Unit{
    
}
​
function createReactUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new ReactTextUnit(element)
    }
}
​
export {
    createReactUnit
}

 

如代碼所示,createReactUnit判斷element是字符串時就 new 一個ReactTextUnit的對象,然後返回出去,這個也就是我們上面講到的unit對象了。

爲什麼要 ReactTextUnit 繼承 於 Unit呢?

這是因爲 element除了字符串 ,也有可能是 原生的標籤,列如div,span等,也有可能是我們自定義的組件,所以我們先寫 了一個 unit類,這個類實現 這幾種element 所共有的屬性。 然後 具體的 類 ,例如 ReactTextUnit 直接繼承 Unit ,再實現自有的 屬性就好了。

6. 實現Unit

 

new Unit 得到的對象應當是這樣的

{
  _currentElement:element,
  getMarkUp(){
    ...
  }
}

 

也就是說,這是所有的 種類都有的屬性,所以我們可以這樣實現 Unit

class Unit{
    constructor(element){
        this._currentElement = element
    }
}

 

7. 實現ReactTextUnit

到這一步,我們只要重寫getMarkUp方法就好了,不過不要忘記,給每一個元素添加一個 reactid,至於爲什麼,已經在上面說過了,也放了一張大圖了哈。

class ReactTextUnit extends Unit{
    getMarkUp(rootId) { // 保存當前元素id
        this._rootId = rootId
        const markUp = `<span data-react_id="${this._rootId}">${this._currentElement}</span>`
        return markUp
}

 

好了,到這裏先看下完整的Unit.js長什麼樣子吧

// Unit.js
class Unit{
    constructor(element){
        this._currentElement = element
    }
}
class ReactTextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-react_id=${reactid}>${this._currentElement}</span>`
    }
}
​
function createReactUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
         return new ReactTextUnit(element)
    }
}
​
export createReactUnit;

 

我們在react/index.js引入 unit測試下

// index.js
import React from './react';
import ReactDOM from './react-dom';
​
ReactDOM.render(
  "sunny",
  document.getElementById('root')
);
​
// react-dom.js
import createReactUnit from './unit.js'
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用來返回HTML標記
    $(container).html(markUp)
}
​
export default ReactDOM

 

在這裏插入圖片描述

渲染成功

8. 理解React.creacteElement方法

在第一次學習react的時候,我總會帶着許多疑問。比如看到下面的代碼就會想:爲什麼我們只是引入了React,但是並沒有明顯的看到我們在其他地方用,這時我就會想着既然沒有用到,那如果刪除之後會不會受到影響呢?答案當然是不行的。

import React from 'react';
import ReactDOM from 'react-dom';
​
let element = (
    <h1 id="title" className="bg" style={{color: 'red'}}>
        hello
        <span>world</span>
    </h1>
)
​
console.log({type: element.type, props:element.props})
​
ReactDOM.render(element,document.getElementById('root'));

 

當我們帶着這個問題去研究的時候會發現其實在渲染element的時候調了React.createElement(),所以上面的問題就在這裏找到了答案。

如下面代碼所示,這就是從jsx語法到React.createElement的轉化

<h1 id="title" className="bg" style={{color: 'red'}}>
        hello
        <span>world</span>
</h1>
//上面的這段代碼很簡單,但是我們都知道react是所謂的虛擬dom,當然不可能就是我們看到的這樣。當我們將上面的代碼經過babel轉譯後,我們再看看
​
React.createElement("h1", {
  id: "title",
  className: "bg",
  style: {
    color: 'red'
  }
}, "hello", React.createElement("span", null, "world"));

 

document有createElement()方法,React也有createElement()方法,下面就來介紹React的createElement()方法。

var reactElement = ReactElement.createElement(
    ... // 標籤名稱字符串/ReactClass,
    ... // [元素的屬性值對對象],
    ... // [元素的子節點]
)

 

1、參數:

1)第一個參數:可以是一個html標籤名稱字符串,也可以是一個ReactClass(必須);

2)第二個參數:元素的屬性值對對象(可選),這些屬性可以通過this.props.*來調用;

3)第三個參數開始:元素的子節點(可選)。

2、返回值:

一個給定類型的ReactElement元素

我們可以改下我們的index.js

// index.js
import React from './react';
import ReactDOM from './react-dom';

var li1 = React.createElement('li', {onClick:()=>{alert("click")}}, 'First');
var li2 = React.createElement('li', {}, 'Second');
var li3 = React.createElement('li', {}, 'Third');
var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3);
console.log(ul);
ReactDOM.render(ul,document.getElementById('root'))

可以就看下 ul 最終的打印 期待結果 在這裏插入圖片描述

 

由此 ,我們只知道了,React.createElement方法將生產一個給定類型的ReactElement元素,也就是Vnode。然後這個對象被傳入 render方法,然後進行了上面講到的 createUnit和getMarkUp操作。

9. 實現React.createElement方法

經過上面的講解,我們大概已經知道React.createElement方法的作用了,現在就來看看是怎麼實現的

 

 

 

 

 

 

 

 

 

我們創建了一個新的文件element.js(目的是爲了學習react的構建,不包括vnode的生成)

// element.js
class Element{
    constructor(type, props) {
        this.type = type
        this.props = props
    }
}
function createElement(type, props, ...children) {
    const currentProp = props || {}
    currentProp['children'] = children
    return new Element(type, currentProp)
}
// 該方法返回vNode,用對象來描述元素
export default createElement;

 

我們 定義了一個 Element 類 ,然後在createElement方法裏創建了這個類的對象, 並且return出去了

沒錯,這個對象就是上面所說的給定類型的ReactElement元素,也就是下面這張圖所顯示的 在這裏插入圖片描述

我們應當是這樣React.createElement()調用這個方法的,所以我們要把這個方法掛載到react身上。

我們前面還沒有實現react.js

其實,很簡單,就是返回一個React對象,這個對象有createElement方法

 // react.js
 import createElement from "./element"
 const React = {
    createElement
 }
 export default React

 

10. 實現ReactNativeUnit

上面實現了 createElement返回 給定類型的ReactElement元素 後,就將改元素傳入,render方法,因此 就會經過 createReactUnit方法, createReactUnit方法判斷是屬於什麼類型的 元素,如下面代碼

// Unit.js
class Unit {
    constructor(element) {
        this._currentElement = element
    }
}
class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-react_id=${reactid}>${this._currentElement}</span>`
    }
}
​
function createReactUnit(element){
    // 
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
    // 新增代碼
    if (typeof element === 'object' && typeof element.type === 'string') {
        return new ReactNativeUnit(element)
    }
}
​
export default createReactUnit;

 

好了,現在我們來實現ReactNativeUnit類,其實主要就是實現ReactNativeUnit的getMarkUp方法

class ReactNativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
    }
}

 

要明確的一點是,ReactNativeUnit的getMarkUp方法,是要把 在這裏插入圖片描述 這樣一個element 對象轉化爲 真實的dom的

因此,我們可以這樣完善getMarkUp方法

class ReactNativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} `
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            if(/^on[A-Z]/.test(propName)){ // 添加綁定事件
                
            }else if(propName === 'style'){ // 如果是一個樣式對象
​
            }else if(propName === 'className'){ // 如果是一個類名
​
            }else if(propName === 'children'){ // 如果是子元素
​
            }else { // 其他 自定義的屬性 例如 reactid
                tagStart += (` ${propName}=${props[propName]} `)
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}

 

這只是 大體上的 一個實現 ,其實就是 把標籤 和屬性 以及 子元素 拼接成 字符串,然後返回出去。

我們測試下,現在有沒有 把ul 渲染出來

// index.js
import React from './react';
import ReactDOM from './react-dom';
​
var li1 = React.createElement('li', {}, 'First');
var li2 = React.createElement('li', {}, 'Second');
var li3 = React.createElement('li', {}, 'Third');
var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3);
console.log(ul);
ReactDOM.render(ul,document.getElementById('root'))

 

在這裏插入圖片描述 發現確實成功渲染出來了,但是 屬性和 子元素還沒有,這是因爲我們 還沒實現 具體 的功能。

現在我們來實現事件綁定 功能

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} data-react_id="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            // 新增代碼
            if(/^on[A-Z]/.test(propName)){ // 添加綁定事件
                let eventName = propName.slice(2).toLowerCase(); // 獲取click
                $(document).delegate(`[data-react_id="${this._reactid}"]`,`${eventName}.${this._reactid}`,props[propName])
            }else if(propName === 'style'){ // 如果是一個樣式對象
               
            }else if(propName === 'className'){ // 如果是一個類名
                
            }else if(propName === 'children'){ // 如果是子元素
               
            }else { // 其他 自定義的屬性 例如 reactid
                
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}

 

在這裏,我們是用了事件代理的模式,之所以用事件代理,是因爲這些標籤元素還沒被渲染到頁面上,但我們又必須提前綁定事件,所以需要用到事件代理

接下來,實現 樣式對象的綁定

class ReactNativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} data-react_id="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            if(/^on[A-Z]/.test(propName)){ // 添加綁定事件
                ...
            }else if(propName === 'style'){ // 如果是一個樣式對象
                let styleObj = props[propName]
                let styles = Object.entries(styleObj).map(([attr, value]) => {
                    return `${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`;
                }).join(';')
                tagStart += (` style="${styles}" `)
            }else if(propName === 'className'){ // 如果是一個類名
                
            }else if(propName === 'children'){ // 如果是子元素
               
            }else { // 其他 自定義的屬性 例如 reactid
              
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}

 

這裏 其實就是把

{style:{backgroundColor:"red"}}

 

對象中的 style這個對象 屬性拿出來,

然後把backgroundColor 通過正則 變化成background-color

然後再拼接到tagStart中。

接下來再實現className,發現這個也太簡單了吧

class ReactNativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-react_id="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加綁定事件
                ...
            } else if (propName === 'style') { // 如果是一個樣式對象
                ...
            } else if (propName === 'className') { // 如果是一個類名
                tagStart += (` class="${props[propName]}"`)
            } else if (propName === 'children') { // 如果是子元素
               ...
            } else { // 其他 自定義的屬性 例如 reactid
                ...
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}

 

爲什麼這麼簡單呢? 因爲只需要把

className: 'list'

中的className變化成 class就可以了。OMG!!

接下來,是時候實現子元素的拼接了哈

class ReactNativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-react_id="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加綁定事件
                ...
            } else if (propName === 'style') { // 如果是一個樣式對象
                ...
            } else if (propName === 'className') { // 如果是一個類名
                ...
            } else if (propName === 'children') { // 如果是子元素
                let children = props[propName];
                children.forEach((child, index) => {
                    let childUnit = createUnit(child); // 可能是字符串 ,也可能是原生標籤,也可能是自定義屬性
                    let childMarkUp = childUnit.getMarkUp(`${this._reactid}.${index}`)
                    childString += childMarkUp;
                })
            } else { // 其他 自定義的屬性 例如 reactid
                
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}

 

發現子元素 ,其實只要進行遞歸操作,也就是將子元素傳進createUnit,把返回的childUnit 通過getMarkUp方法變成 真實DOM,再拼接到childString 就好了。 其實想想也挺簡單,遞歸深度優先。

好了,接下來就是 其他屬性了

class ReactNativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-react_id="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加綁定事件
               ...
            } else if (propName === 'style') { // 如果是一個樣式對象
               ...
            } else if (propName === 'className') { // 如果是一個類名
               ...
            } else if (propName === 'children') { // 如果是子元素
                ...
            } else { // 其他 自定義的屬性 例如 reactid
                tagStart += (` ${propName}=${props[propName]} `)
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}

 

其他屬性直接就拼上去就好了哈哈哈

 

好了。現在我們已經完成了ReactNativeUnit的getMarkUp方法。我們來測試一下是否成功了沒有吧! 在這裏插入圖片描述 害,不出所料地成功了。

11. 完成React.Component

接下來我們看看自定義組件是怎麼被渲染的,例如下面的Counter組件

// index.js
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"計時器"})
ReactDOM.render(element,document.getElementById('root'))

 

我們發現自定義組件好像需要繼承React.Component。這是爲什麼呢?

我之前一直誤認爲所有的生命週期都是從Component繼承過來的,也許有很多小夥伴都和我一樣有這樣的誤解,直到我看了Component源碼才恍然大悟,原來我們用的setState和forceUpdate方法是來源於這裏

知道這個原因後,我們就可以先簡單地實現React.Component了

// component.js
class Component{
    constructor(props){
        this.props = props
    }
}
​
export default Component
然後再引入react中即可

 // react.js
 import createElement from "./element"
 import Component from "./component"
 const React = {
    createElement,
    Component
 }
 export default React

 

跟 處理ReactNativeUnit一樣,先通過createReactUnit判斷element是屬於什麼類型,如果是自定義組件就 return ReactComponentUnit()

// Unit.js
import Element from "./element" // 新增代碼
import $ from "jquery"
class Unit {
    constructor(element) {
        this._currentElement = element
    }
    getMarkUp() {
        throw Error("此方法應該被重寫,不能直接被使用")
    }
}
class ReactTextUnit extends Unit {
    
}
​
class ReactNativeUnit extends Unit {
   
}
​
function CreateUnit(element) {
    if (typeof element === 'string' || typeof element === "number") {
        return new ReactTextUnit(element)
    }
    if (element instanceof Element && typeof element.type === "string") {
        return new ReactNativeUnit(element)
    }
    // 新增代碼
    if(element instanceof Element && typeof element.type === 'function'){
       return new ReactComponentUnit(element)
    }
​
}
​
​
export default CreateUnit

 

爲什麼是用 typeof element.type === 'function'來判斷 呢? 因爲Counter是 一個類,而類在js中的本質就是function

好了,接下來實現一下ReactComponentUnit類

class ReactComponentUnit extends Unit{
    getMarkUp(reactid){
      this._rootId = rootId
      let {type:Component,props} = this._currentElement // 實際上,在例子中type === Counter
      let componentInstance = new Component(props); // 獲取class(即組件)實例
      let reactComponentRender = componentInstance.render() // 獲取組件render數據
      // render返回的數據執行createReactUnit
      let reactComposeUnitInstance = createReactUnit(reactComponentRender)
      let markup = reactComposeUnitInstance.getMarkUp(this._rootId)
      return markup
    }
}

 

咦,好簡短 啊,不過 沒那麼 簡單,但是讓 我的三寸不爛之舌來講解一下,包懂

此時的_currentElement 是:

{
    type:Counter,
    props:{}
}
let {type:Component,props} = this._currentElement// 實際上,在例子中type就是Counter new Component(props);其實就是new Counter。

 

也就是我們上面例子中寫的

class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"計時器"})
ReactDOM.render(element,document.getElementById('root'))

 

可想而知 ,通過new Counter就獲得了Counter的實例

也就是componentInstance ,而每一個Counter的實例都會有render方法,所以執行componentInstance.render()

就獲得一個給定類型的reactComponentRender元素(好熟悉的一句話,對,我們在上面講到過)。

然後就把這個reactComponentRender元素對象傳給createReactUnit,獲得一個具有getMarkUp的對象, 然後就可以執行renderUnit.getMarkUp(this._reactid)獲得真實dom,就可以返回了。

其實,仔細想想,就會發現,在

let renderUnit = createReactUnit(reactComponentRender);

 

之前,我們是在處理自定義組件Counter。

而到了

let renderUnit = createReactUnit(reactComponentRender);

 

這一步,其實就是在處理ReactNativeUnit。(細思極恐。。)

好了,測試一下在這裏插入圖片描述 發現確實成功了。

12. 實現 componentWillMount

我們在之前的例子上添加個componentWillMount 生命週期函數吧

// index.js
import React from './react';
import ReactDOM from './react-dom';
​
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    componentWillMount(){
        console.log("陽光你好,我是componentWillMount");
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"計時器"})
ReactDOM.render(element,document.getElementById('root'))

 

我們知道componentWillMount 實在組件渲染前執行的,所以我們可以在render之前執行這個生命週期函數

class ReactComponentUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 實際上,在例子中type === Counter
      let componentInstance = new Component(props);
      componentInstance.componentWillMount && componentInstance.componentWillMount() // 添加生命週期函數
      let reactComponentRender = componentInstance.render();
      let reactComposeUnitInstance = createReactUnit(reactComponentRender);
      return reactComposeUnitInstance.getMarkUp(reactComposeUnitInstance)
    }
}

 

可能聰明的小夥伴會問,不是說componentWillMount是在組件重新渲染前執行的嗎?那組件沒掛到頁面上應該都是渲染前,所以componentWillMount也可以在return reactComposeUnitInstance.getMarkUp(this._reactid)前執行啊。

其實要回答這個問題,倒不如回答另一個問題:

父組件的componentWillMount和子組件的componentWillMount哪個先執行。

答案是父組件先執行。

這是因爲在父組件中會先執行 父組件的componentWillMount ,然後執行componentInstance.render();的時候,會解析子組件,然後又進入子組件的getMarkUp。又執行子組件的componentWillMount 。

若要回答 爲什麼componentWillMount 要在 render函數執行前執行,只能說,react就是這麼設計的哈哈哈

 

13. 實現componentDidMount

衆所周知,componentDidMount是在組件渲染,也就是掛載到頁面後才執行的。

所以,我們可以在返回組件的真實dom之前 就監聽 一個mounted事件,這個事件執行componentDidMount方法。

class CompositeUnit extends Unit{
   getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 實際上,在例子中type === Counter
      let componentInstance = new Component(props);
      componentInstance.componentWillMount && componentInstance.componentWillMount() // 添加生命週期函數
      let reactComponentRender = componentInstance.render();
      let reactComposeUnitInstance = createReactUnit(reactComponentRender);
      $(document).on('mounted', () => {
         componentInstance.componentDidMount && componentInstance.componentDidMount()
      })
      return reactComposeUnitInstance.getMarkUp(reactComposeUnitInstance)
    }
}

 

然後 再在 把組件的dom掛載到 頁面上後再觸發這個 mounted事件

// react-dom.js
import {createUnit} from './unit'
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
   // 寫一個工廠函數,來創建對應得react元素
    // 通過工廠函數來創建
    let createReactUnitInstance = createReactUnit(element)
    // console.log('createReactUnit生成的實例', createReactUnitInstance)
    const markUp = createReactUnitInstance.getMarkUp(React.nextRootIndex)
    // console.log('result', markUp)
    $(container).html(markUp)
    // 此處觸發mounted
    $(document).trigger('mounted')
}
​
export default ReactDOM

 

由此依賴,就實現了,componentDidMount 生命週期函數,哈哈哈。

測試一下,成功了沒有哈 在這裏插入圖片描述 啊,一如既往的成功,可能好奇的你問我爲什麼每次測試都成功,那是因爲,不成功也被我調試到成功了。

爲了下面 實現 setState 功能,我們 修改一下 ReactComponentUnit的getMarkUp方法。

class ReactComponentUnit extends Unit{
        getMarkUp(rootId) {
        // 將傳進來的id綁定到生成的實例上
        this._rootId = rootId
        // 將props和type取出,並將type重命名爲component
        let {type: Component, props} = this._currentElement
        // type爲class,類型爲function,此處則可以將props掛載到React.Components原型上
        // type(Component)爲實際傳進來的組件,通過new生成componentInstance組件實例
        let componentInstance = this._componentInstance = new Component(props)
        // 讓組件實例的currentUnit屬性等於當前的unit,把ReactComponentUnit實例綁定在當前組件實例上,之後setState時,則可以獲取該ReactComponentUnit上的所有屬性和方法(getMarkUp, update)
        componentInstance._currentUnit = this
        // 執行生命週期鉤子
        componentInstance.componentWillMount && componentInstance.componentWillMount()
        // 調用組件的render方法,獲取要渲染的元素
        let reactComponentRender = componentInstance.render() // class實例上定義了該render方法,獲取到render的返回值
// 得到這個元素對應的Unit. 將返回值重新遞歸渲染
        let reactComposeUnitInstance = this._reactComponentUnitInstance = createReactUnit(reactComponentRender)
        // 通過Unit可以獲得他的html標記
        let markup = reactComposeUnitInstance.getMarkUp(this._rootId)
        // 綁定事件 先序深度優先  有兒子就進去  樹的遍歷
        $(document).on('mounted', () => {
            componentInstance.componentDidMount && componentInstance.componentDidMount()
        })
        return markup
    }
}

 

我們爲這個 ReactComponentUnit的實例添加了

  1. _componentInstance :用了表示 當前組件的實例 (我們所寫的Counter組件)

  2. _reactComponentUnitInstance: 當前組件的render,用於之後對比新舊render出來的結果的變化

另外,我們也通過

componentInstance._currentUnit = this // 把 unit 掛到 實例componentInstance 

把當前 的unit 掛載到了 組件實例componentInstance身上。

可見 組件的實例保存了 當前 unit,當前的unit也保存了組件實例

 

14. 實現setState

我們看下面的例子,每隔一秒鐘就number+1

// index.js
import React from './react';
import ReactDOM from './react-dom';
import $ from 'jquery'
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    componentWillMount(){
        console.log("陽光你好,我是componentWillMount");
        $(document).on("mounted",()=>{
            console.log(456);
            
        })
    }
    componentDidMount(){
        setInterval(()=>{
            this.setState({number:this.state.number+1})
        },1000)
    }
    render(){
        
        return this.state.number
    }
}
let element = React.createElement(Counter,{name:"計時器"})
ReactDOM.render(element,document.getElementById('root'))

 

前面說到,setState方法是從Component組件繼承過來的。所以我們給Component組件添加setState方法

// component.js
class Component {
    constructor(props) {
        this.props = props
    }
    setState(partialState) {
        // 第一個參數是新的元素   第二個參數是新的狀態
        this._currentUnit.update(null, partialState)
    }
}
export default Component

 

我們發現原來是在setState方法裏調用了當前實例的對應的unit的update方法,它傳進去了 部分state的值。

看到這裏,我們就知道了,我們需要回到 ReactComponentUnit類添加一個update方法。

class ReactComponentUnit extends Unit{
    update(nextElement,partialState){
        // 有傳新元素的話就更新currentElement爲新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 獲取新的狀態,並且更新組件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的屬性對象
        let nextProps = this._currentElement.props
    }
    getMarkUp(reactid){
     ...
    }
}

 

我們首先 更換了_currentElement的值,這裏爲什麼會有 有或者沒有nextElement的情況呢?

(主要就是因爲,如果 _currentElement 是 字符串或者數字的話,那麼它就需要 傳nextElement 來替換掉舊的 _currentElement 。而如果不是字符串或者數字的話,是不需要傳的。而ReactComponentUnit必定是組件的,所以不用傳nextElement )。

接着,我們 通過下面這句代碼獲取了最新的state,並且更新了組件的state

 let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);

 

獲取 最新的 props跟獲取state的方式不一樣,props是跟_currentElement 綁定在一起的,所以獲取最新的props是通過

let nextProps = this._currentElement.props

 

接下來,我們要先獲取新舊的渲染元素,然後拿來比較,怎麼獲取呢?

class ReactComponentUnit extends Unit{
    update(nextElement,partialState){
        // 有傳新元素的話就更新currentElement爲新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 獲取新的狀態,並且更新組件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的屬性對象
        let nextProps = this._currentElement.props
        // 下面要進行比較更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._reactComponentUnitInstance // 上次渲染的render
        // 通過上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement =  this._componentInstance.render()
​
    }
    getMarkUp(reactid){
        
    }
}

 

我們先得到上次渲染的unit,再通過上次渲染的unit得到上次渲染的元素preRenderedElement,

再通過this._componentInstance.render()得到下次渲染的元素nextRenderElement。

接下來就可以進行比較這兩個元素了

我們首先會判斷要不要進行深度比較。

如果不用進行深度比較就非常簡單

直接獲取新的渲染unit,然後通過getMarkUp獲得要渲染的dom,接着就把當前的組件裏的dom元素替換掉

class ReactComponentUnit extends Unit{
    update(nextElement,partialState){
        // 先獲取到新的元素
        this._currentElement = nextElement || this._currentElement
        // 獲取新的狀態   不管數據更新與否,狀態會改變
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state, partialState)
        // 新的屬性對象
        let nextProps = this._currentElement.props
        if (this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps, nextState)) {
            return
        }
        // 下面要比較變更  看render的返回值是否一樣
        let preRenderedUnitInstance = this._reactComponentUnitInstance // 上次渲染的render
        //  得到上次渲染的元素
        let preRenderedElement = preRenderedUnitInstance._currentElement
        // 將要渲染的render
        let nextRenderElement = this._componentInstance.render()
        // 如果新舊兩個元素類型一樣,則可以進行深度比較,如果不一樣,則直接幹掉老的元素。新建新的
        if (shouldDeepCompare(preRenderedElement, nextRenderElement)) {
            // 如果可以進行深比較,則把更新的工作交給上次渲染出來的那個element元素對應的unit來處理
            preRenderedUnitInstance.update(nextRenderElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        } else {
            // 重新渲染時清除已經綁定的事件
            $(document).undelegate(`.${this._rootId}`);
            this._renderedUnitInstance = createReactUnit(nextRenderElement)
            let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._rootId)
            $(`[data-react_id="${this._rootId}"]`).replaceWith(nextMarkUp)
        }
​
    }
    getMarkUp(reactid){
     
    }
}

 

我們先簡單地寫一下shouldDeepCompare方法,直接return false,來測試一下 非深度比較,是否能夠正確執行

function shouldDeepCompare(){
    return false
}
class ReactComponentUnit extends Unit{
    update(nextElement,partialState){
        // 有傳新元素的話就更新currentElement爲新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 獲取新的狀態,並且更新組件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的屬性對象
        let nextProps = this._currentElement.props
        // 下面要進行比較更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 通過上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 如果新舊兩個元素類型一樣,則可以進行深度比較,如果不一樣,直接幹掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
​
        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-react_id="${this._reactid}"]`).replaceWith(nextMarkUp)
        }
​
    }
    getMarkUp(reactid){
     
    }
}

 

在這裏插入圖片描述

發現確實成功了。

如果可以進行深度比較呢?

class ReactComponentUnit extends Unit {
    // 這裏負責組件的更新操作
    update(nextElement, partialState) {
        // 先獲取到新的元素
        this._currentElement = nextElement || this._currentElement
        // 獲取新的狀態   不管數據更新與否,狀態會改變
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state, partialState)
        // 新的屬性對象
        let nextProps = this._currentElement.props
        if (this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps, nextState)) {
            return
        }
        // 下面要比較變更  看render的返回值是否一樣
        let preRenderedUnitInstance = this._reactComponentUnitInstance // 上次渲染的render
        //  得到上次渲染的元素
        let preRenderedElement = preRenderedUnitInstance._currentElement
        // 將要渲染的render
        let nextRenderElement = this._componentInstance.render()
        // 如果新舊兩個元素類型一樣,則可以進行深度比較,如果不一樣,則直接幹掉老的元素。新建新的
        if (shouldDeepCompare(preRenderedElement, nextRenderElement)) {
            // 如果可以進行深比較,則把更新的工作交給撒謊給你次渲染出來的那個element元素對應的unit來處理
            preRenderedUnitInstance.update(nextRenderElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        } else {
            // 重新渲染時清除已經綁定的事件
            $(document).undelegate(`.${this._rootId}`);
            this._renderedUnitInstance = createReactUnit(nextRenderElement)
            let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._rootId)
            $(`[data-react_id="${this._rootId}"]`).replaceWith(nextMarkUp)
        }
    }
​
    getMarkUp(rootId) {
        // 將傳進來的id綁定到生成的實例上
        this._rootId = rootId
        // 將props和type取出,並將type重命名爲component
        let {type: Component, props} = this._currentElement
        // type爲class,類型爲function,此處則可以將props掛載到React.Components原型上
        // type(Component)爲實際傳進來的組件,通過new生成componentInstance組件實例
        let componentInstance = this._componentInstance = new Component(props)
        // 讓組件實例的currentUnit屬性等於當前的unit,把ReactComponentUnit實例綁定在當前組件實例上,之後setState時,則可以獲取該ReactComponentUnit上的所有屬性和方法(getMarkUp, update)
        componentInstance._currentUnit = this
        // 執行生命週期鉤子
        componentInstance.componentWillMount && componentInstance.componentWillMount()
        // 調用組件的render方法,獲取要渲染的元素
        let reactComponentRender = componentInstance.render() // class實例上定義了該render方法,獲取到render的返回值
// 得到這個元素對應的Unit. 將返回值重新遞歸渲染
        let reactComposeUnitInstance = this._reactComponentUnitInstance = createReactUnit(reactComponentRender)
        // 通過Unit可以獲得他的html標記
        let markup = reactComposeUnitInstance.getMarkUp(this._rootId)
        // 綁定事件 先序深度優先  有兒子就進去  樹的遍歷
        $(document).on('mounted', () => {
            componentInstance.componentDidMount && componentInstance.componentDidMount()
        })
        return markup
    }
}

 

如果可以深度,就執行

 preRenderedUnitInstance.update(nextRenderElement)

 

這是什麼意思?

我們當前是在執行渲染Counter的話,那preRenderedUnitInstance 是什麼呢?

沒錯!它是Counter組件 執行render方法 ,再執行createReactUnit獲得的

在這裏插入圖片描述

這個字符串的 unit

然後調用了這個 unit的 update方法

注意,這裏 的unit是字符串的 unit,也就是說是 ReactTextUnit

所以我們需要實現 TextUnit 的update 方法

class ReactTextUnit extends Unit {
   getMarkUp(rootId) { // 保存當前元素id
        this._rootId = rootId
        const markUp = `<span data-react_id="${this._rootId}">${this._currentElement}</span>`
        return markUp
    }
​
    update(nextElement) {
        if (this._currentElement !== nextElement) {
            this._currentElement = nextElement
            $(`[data-react_id="${this._rootId}"]`).html(this._currentElement)
        }
    }
}

 

ReactTextUnit 的update方法非常簡單,先判斷 渲染內容有沒有變化,有的話就 替換點字符串的內容

並把當前unit 的_currentElement 替換成最新的nextElement

我們簡單的把shouldDeepCompare 改成 return true,測試一下深度比較

function shouldDeepCompare(){
    return true
}

 

在這裏插入圖片描述 一如既往成功

15. 實現shouldComponentUpdate方法

我們知道有個shouldComponentUpdate,用來決定要不要 重渲染 該組件的

shouldComponentUpdate(nextProps, nextState) {
  return nextState.someData !== this.state.someData
}

 

顯然,它要我們傳入 兩個參數,分別是 組件更新後的nextProps和nextState

而在 還是上面,實現 update的過程中,我們已經得到了nextState 和nextProps

class ReactComponentUnit extends Unit{
    update(nextElement,partialState){
        。。。
        // 獲取新的狀態,並且更新組件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的屬性對象
        let nextProps = this._currentElement.props
        // 下面要進行比較更新
        。。。
​
    }
}

 

所以,我們可以在update裏執行shouldComponentUpdate方法,來確定要不要重新渲染組件

class ReactComponentUnit extends Unit{
        // 這裏負責組件的更新操作
    update(nextElement, partialState) {
        // 先獲取到新的元素
        this._currentElement = nextElement || this._currentElement
        // 獲取新的狀態   不管數據更新與否,狀態會改變
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state, partialState)
        // 新的屬性對象
        let nextProps = this._currentElement.props
        if (this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps, nextState)) {
            return
        }
        // 下面要比較變更  看render的返回值是否一樣
        let preRenderedUnitInstance = this._reactComponentUnitInstance // 上次渲染的render
        //  得到上次渲染的元素
        let preRenderedElement = preRenderedUnitInstance._currentElement
        // 將要渲染的render
        let nextRenderElement = this._componentInstance.render()
        // 如果新舊兩個元素類型一樣,則可以進行深度比較,如果不一樣,則直接幹掉老的元素。新建新的
        if (shouldDeepCompare(preRenderedElement, nextRenderElement)) {
            // 如果可以進行深比較,則把更新的工作交給撒謊給你次渲染出來的那個element元素對應的unit來處理
            preRenderedUnitInstance.update(nextRenderElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        } else {
            // 重新渲染時清除已經綁定的事件
            $(document).undelegate(`.${this._rootId}`);
            this._renderedUnitInstance = createReactUnit(nextRenderElement)
            let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._rootId)
            $(`[data-react_id="${this._rootId}"]`).replaceWith(nextMarkUp)
        }
    }
​
}

 

16. 實現componentDidUpdate生命週期函數

只要在更新後觸發這個事件就好了

class ReactComponentUnit extends Unit{
    update(nextElement,partialState){
        
        if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){
            return;
        }
   
         if (shouldDeepCompare(preRenderedElement, nextRenderElement)) {
            // 如果可以進行深比較,則把更新的工作交給撒謊給你次渲染出來的那個element元素對應的unit來處理
            preRenderedUnitInstance.update(nextRenderElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        } else {
            // 重新渲染時清除已經綁定的事件
            $(document).undelegate(`.${this._rootId}`);
            this._renderedUnitInstance = createReactUnit(nextRenderElement)
            let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._rootId)
            $(`[data-react_id="${this._rootId}"]`).replaceWith(nextMarkUp)
        }
​
    }
    getMarkUp(reactid){
     
    }
}

 

17. 實現shouDeepCompare

判斷是否需要深比較極其簡單,只需要判斷 oldElement 和newElement 是否 都是字符串或者數字,這種類型的就走深比較

接着判斷 oldElement 和newElement 是否 都是 Element類型,不是的話就return false,是的 再判斷 type是否相同(即判斷是否是同個組件,是的話 return true)

其他情況都return false

function shouldDeepCompare(oldElement,newElement){
    if(oldElement != null && newElement != null){
        let oldType = typeof oldElement
        let newType = typeof newElement
        if((oldType === 'string' || oldType === "number")&&(newType === "string" || newType === "number")){
            return true
        }
        if(oldElement instanceof Element && newElement instanceof Element){
            return oldElement.type === newElement.type
        }
    }
    return false
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章