React基礎與原理實現

1、前言

React是用於構建用戶界面的 JavaScript。其有着許多優秀的特性,使其受到大衆的歡迎。
① 聲明式渲染:
所謂聲明式,就是關注結果而不是關注過程。好比咱們經常使用的html標記語言就是一種聲明式的,咱們只須要在.html文件上,寫上聲明式的標記如<h1>這是一個標題</h1>,瀏覽器就能自動幫咱們渲染出一個標題元素。一樣react中也支持jsx的語法,能夠在js中直接寫html,因爲其對DOM操做進行了封裝,react會自動幫咱們渲染出對應的結果。html

② 組件化:
組件是react的核心,一個完整的react應用是由若干個組件搭建起來的,每一個組件有本身的數據和方法,組件具體如何劃分,須要根據不一樣的項目來肯定,而組件的特徵是可複用,可維護性高。node

③ 單向數據流:
子組件對於父組件傳遞過來的數據是只讀的子組件不可直接修改父組件中的數據,只能經過調用父組件傳遞過來的方法,來間接修改父組件的數據,造成了單向清晰的數據流。防止了當一個父組件的變量被傳遞到多個子組件中時,一旦該變量被修改,全部傳遞到子組件的變量都會被修改的問題,這樣出現bug調試會比較困難,由於不清楚究竟是哪一個子組件改的,把對父組件的bug調試控制在父組件之中。react


以後的內容,咱們將一步步瞭解React相關知識,而且簡單實現一個react。webpack

2、jsx

剛接觸react的時候,首先要了解的就是jsx語法,jsx實際上是一種語法糖,是js的一種擴展語法它可讓你在js中直接書寫html代碼片斷,而且react推薦咱們使用jsx來描述咱們的界面,例以下面一段代碼:es6

// 直接在js中,將一段html代碼賦值給js中的一個變量

const element =  <h1>Hello, react!</h1\>;

在普通js中,執行這樣一段代碼,會提示Uncaught SyntaxError: Unexpected token '<',也就是不符合js的語法規則。那麼爲何react可以支持這樣的語法呢?
由於react代碼在打包編譯的過程當中,會經過babel進行轉化,會對jsx中的html片斷進行解析,解析出來標籤名屬性集子元素,而且做爲參數傳遞到React提供的createElement方法中執行。如上面代碼的轉換結果爲:web

// babel編譯轉換結果

const element = React.createElement("h1", null, "Hello, react!");

能夠看到,babel轉換的時候,識別到這是一個h1標籤,而且標籤上沒有任何屬性,因此屬性集爲null,其有一個子元素,純文本"Hello, react!",因此通過babel的這麼一個騷操做,React就能夠支持jsx語法了。由於這個轉換過程是由babel完成的,因此咱們也能夠經過安裝babel的jsx轉換包,從而讓咱們本身的項目代碼也能夠支持jsx語法。npm

3、讓咱們的項目支持jsx語法

由於咱們要實現一個簡單的react,因爲咱們使用react編程的時候是可使用jsx語法的,因此咱們首先要讓咱們的項目支持jsx語法。
① 新建一個名爲my-react的項目
在項目根目錄下新建一個src目錄,裏面存放一個index.js做爲項目的入口文件,以及一個public目錄,裏面存放一個index.html文件,做爲單頁面項目的入口html頁面,如:編程

cd /path/to/my-react // 進入到項目根目錄下

npm init --yes // 自動生成項目的package.json文件
// project_root/src/index.js 內容
const element = <h1>hello my-react</h1>;
// project_root/public/index.html 內容
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>my-react</title>
</head>
<body>
    <div id="root"></div>
    <script src="../src/index.js"></script>
</body>
</html>

② 安裝 parcel-bundler 模塊
parcel-bundler是一個打包工具,它速度很是快,而且能夠零配置,相對webpack而言,不須要進行復雜的配置便可實現web應用的打包,而且能夠以任何類型的文件做爲打包入口,同時自動啓動內置的web服務器方便調試。json

// 安裝parcel-bundler
npm install parcel-bundler --save-dev
// 修改package.json,執行parcel-bundler命令並傳遞入口文件路徑做爲參數
{
    "scripts": {
        "start": "parcel -p 8080 ./public/index.html"
    }
}
// 啓動項目
npm run start

parcel啓動的時候會在8080端口上啓動Web服務器,而且以public目錄下的index.html文件做爲入口文件進行打包,由於index.html文件中有一行<script src="../src/index.js"></script>,因此index.html依賴src目錄下的index.js,因此又會編譯src目錄下的index.js並打包。
此時執行npm run start會報錯,由於此時還不支持jsx語法的編譯。數組

③ 安裝@babel/plugin-transform-react-jsx模塊
安裝好@babel/plugin-transform-react-jsx模塊後,還須要新建一個.babelrc文件,配置以下:

// .babelrc
{
    "plugins": [
        ["@babel/plugin-transform-react-jsx", {
          "pragma": "React.createElement" // default pragma is React.createElement
        }]
    ]
}

其做用就是,遇到jsx語法的時候,將解析後的結果傳遞給React.createElement()方法,默認是React.createElement,能夠自定義。此時編譯就能夠經過了,能夠查看編譯後的結果,以下:

var element = React.createElement("h1", null, "hello my-react");

4、實現React.createElement

此時項目雖然能編譯jsx了,可是執行的時候會報錯,由於尚未引入React以及其createElement()方法,React的createElement()方法做用就是建立虛擬DOM,虛擬DOM其實就是一個普通的JavaScript對象,裏面包含了tagattrschildren等屬性。
① 在src目錄下新建一個react目錄
在react目錄下新建一個index.js做爲模塊的默認導出,同時新建一個create-element.js做爲createElement方法的實現,如:

// src/react/create-elemnt.js
// 做用就是接收babel解析jsx後的結果做爲參數,建立並返回虛擬DOM節點對象
function createElement(tag, attrs, ...children) {
    attrs = attrs || {}; // 若是元素的屬性爲null,即元素上沒有任何屬性,則設置爲一個{}空的對象
    const key = attrs.key || null; // 若是元素上有key,則去除key,若是沒有則設置爲null
    return { // 建立一個普通JavaScript對象,並將各屬性添加上去,做爲虛擬DOM進行返回
        key,
        tag,
        attrs,
        children
    }
}
export default createElement;

babel解析jsx後,若是有多個子節點,那麼全部的子節點都會以參數的形式傳入createElement函數中,因此createElement的第三個參數能夠用es6剩餘參數語法,以一個數組的方式來接收全部的子節點

// src/react/index.js

import createElement from "./create-element"; // 引入createElement函數
export default {
    createElement // 將createElement函數做爲react的方法導出
}

至此,react上已經添加了createElement函數了,而後在src/index.js中引入react模塊便可。

// src/index.js
import React from "./react"; // 引入react模塊
const element = <h1>hello my-react</h1>;
console.log(element);

引入react後,因爲React上有了createElement方法,因此能夠正常執行,而且拿到返回的虛擬DOM節點,以下:
虛擬DOM節點.png

5、實現ReactDOM.render

此時,咱們已經可以拿到對應的虛擬DOM節點了,因爲虛擬DOM只是一個普通的JavaScript對象,不是真正的DOM,因此須要對虛擬DOM進行render,建立對應的真實DOM並添加到頁面中,才能在頁面中看到,react中專門提供了一個ReactDOM模塊用於處理DOM相關的操做
① 在src目錄下新建一個react-dom目錄
在react-dom目錄下新建一個index.js做爲模塊的默認導出,同時新建一個render.js做爲render方法的實現,render函數須要接收一個虛擬DOM節點一個掛載點,即將虛擬DOM渲染成了真實DOM後,須要將其掛載到哪裏,這個掛載點就是一個容器,即應用的根節點。

// src/react-dom/render.js
// 接收一個虛擬DOM節點,而且將虛擬DOM節點渲染成真實的DOM節點,而後添加到container容器中進行掛載
function render(vnode, container) {
    // 因爲ReactDOM能夠直接渲染一段文本,因此這個vnode多是一個字符串或數字
    if (typeof vnode === "string" || typeof vnode === "number") {
        const textNode = document.createTextNode(vnode); // 直接建立一個文本節點
        container.appendChild(textNode); // 將建立的文本節點添加到容器中便可
        return 
    }
    // 若是vnode不是一個字符串,那麼其就是一個虛擬DOM對象,那麼咱們根據tag名建立對應的真實DOM元素便可
    const element = document.createElement(vnode.tag);
    // 處理屬性
    Object.keys(vnode.attrs).forEach((key) => {
        if (key === "className") {
            key = "class";
        }
        const value = vnode.attrs[key];
        if (typeof value === "function") { // 若是屬性值爲一個函數,說明是綁定事件
            element[key.toLowerCase()] = value;
        } else {
            element.setAttribute(key, vnode.attrs[key]);
        }
    });
    // 處理子節點,遞歸渲染子節點
    vnode.children.forEach((child) => {
        return render(child, element);
    });
    container.appendChild(element); // 將渲染好的虛擬DOM節點添加到容器中
}
export default render;

render函數主要就是判斷傳遞過來的vnode是一個字符串仍是虛擬DOM節點對象,若是是字符串,那麼直接建立文本節點並添加到容器中便可;若是是虛擬DOM對象,那麼根據其tag建立對應的真實DOM元素節點,而後遍歷節點屬性,將其添加到元素節點上,再處理子節點,遍歷子節點遞歸渲染便可,整個虛擬DOM渲染好以後,將其加到容器中便可。

// src/react-dom/index.js
import render from './render'
// reactDOM主要負責DOM相關的操做,好比虛擬DOM的渲染、虛擬DOM與真實DOM的diff比較
export default { // react-dom只須要提供一個render()方法便可
    render
}
// src/index.js
import React from "./react"; // 引入react模塊
import ReactDOM from "./react-dom"; // 引入react-dom模塊
function doClick() {
    console.log("doClick method run.");
}
const element = <h1 onClick={doClick}>hello my-react</h1>;
console.log(element);
ReactDOM.render(element, document.getElementById("root"));

這裏綁定了一個onClick事件,此時啓動項目執行,能夠看到頁面上已經能看到渲染後的結果了,而且點擊文字,能夠看到事件處理函數執行了。
render結果.png

6、實現組件功能

此時已經完成了基本的聲明式渲染功能了,可是目前只能渲染html中存在的標籤元素,而咱們的react是支持自定義組件的,可讓其渲染出咱們自定義的標籤元素。react中的組件支持函數組件類組件函數組件的性能比類組件的性能要高,由於類組件使用的時候要實例化,而函數組件直接執行函數取返回結果便可。爲了提升性能,儘可能使用函數組件。可是函數組件沒有this沒有生命週期沒有本身的state狀態

① 首先實現函數組件功能
函數組件相對較簡單,咱們先看一下怎麼使用函數組件,就是直接定義一個函數,而後其返回一段jsx,而後將函數名做爲自定義組件名,像html標籤元素同樣使用便可,如:

// src/index.js
import React from "./react"; // 引入react模塊
import ReactDOM from "./react-dom"; // 引入react-dom模塊
function App(props) {
    return <h1>hello my-{props.name}-function</h1>
}
console.log(<App name="react"/>);
ReactDOM.render(<App name="react"/>, document.getElementById("root"));

<App name="react"/>通過babel轉換以後,tag就變成了App函數,因此咱們不能直接經過document.createElement("App")去建立App元素了,咱們須要執行App()函數拿到其返回值<h1>hello my-{props.name}</h1>,而這個返回值是一段jsx,因此會被babel轉換爲一個虛擬DOM節點對象,而後把虛擬DOM節點替換成函數組件執行返回的結果這個時候,tag就變成了h1了,就能夠建立對應的DOM元素了,如:

// 修改src/react-dom/render.js
function render(vnode, container) {
    ......
    if (typeof vnode.tag === "function") { // 這是一個函數組件
        vnode = vnode.tag(vnode.attrs || {}); // 執行函數組件,並傳入屬性集,拿到對應的虛擬DOM節點便可
    }
    ......
}

把函數組件執行後,拿到的結果做爲新的虛擬DOM節點對象,就能夠根據tag建立對應的真實DOM了。
函數組件.png
②支持類組件
在定義類組件的時候,是經過繼承React.Component類的,咱們須要一個定義一個isReactComponent用於判斷是不是react的組件,在src/react目錄下新建一個component.js文件,以下:

// src/react/component.js
class Component {
    constructor(props = {}) {
        this.isReactComponent = true; // 是react組件
        this.props = props; // 保存props屬性集
        this.state = {}; // 保存狀態數據
    }
}
export default Component;

咱們在看一下類組件的使用方式,以下:

// src/index.js
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    render() {
        return <h1>hell my-{this.props.name}-class-state-{this.state.count}</h1>
    }
}
console.log(<App name="react"/>);
ReactDOM.render(<App name="react"/>, document.getElementById("root"));

<App name="react"/>組件通過babel轉換後,tag變成了一個class函數,若是class類函數的原型上有render()方法,那麼就是一個類組件,咱們能夠經過類組件的類名建立出對應的類組件對象,而後調用其render()函數拿到對應的虛擬DOM節點便可。

// 修改src/react-dom/render.js
function render(vnode, container) {
    ......
    if (vnode.tag.prototype && vnode.tag.prototype.render) { // 這是一個類組件
        vnode = new vnode.tag(vnode.attrs).render(); // 根據類組件名建立組件實例並調用render()函數拿到對應的虛擬DOM節點
    }
    ......
}

類組件.png

7、讓類組件支持setState

react中setState是Component中的一個方法,用於修改組件的狀態數據的。當組件中調用setState函數的時候,組件的狀態數據被更新,同時會觸發組件的從新渲染,因此須要修改Component.js並在其中添加一個setState函數。如:

// src/react/component.js
import ReactDOM from "../react-dom"
class Component {
    constructor(props = {}) {
        this._container = null; // 保存組件所在容器
    }
    setState(newState) {
        Object.assign(this.state, newState); // 更新狀態數據
        ReactDOM.render(this, this._container); // 從新渲染組件
    }
}

新增了一個_container屬性,用於保存組件所在的容器,當傳遞過來新狀態數據的時候,更新狀態數據,並從新渲染組件實例。由於setState()函數執行的時候,渲染的時候是直接傳遞的組件實例,因此咱們須要對render()函數進行修改,新增,若是是直接渲染組件實例的狀況,如:

function render(vnode, container) {
    if (vnode.isReactComponent) { // 若是是直接渲染類組件
        const component = vnode;
        component._container = container;
        vnode = vnode.render(); // 直接調用組件對象的render()方法拿到對應的虛擬DOM開始渲染
    }
    if (vnode.tag.prototype && vnode.tag.prototype.render) { // 這是一個類組件
        const component = new vnode.tag(vnode.attrs); // 建立類組件實例
        component._container = container; // 第一次渲染組件實例的時候,須要保存組件所在的容器
        vnode = component.render();
    }
}

主要是,在組件第一次渲染完成的時候,將組件所在的容器保存到了_container上,而後再次渲染的時候,直接調用render()函數更新虛擬DOM節點便可。

// src/index.js上測試
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    doClick() {
        this.setState({
            count: 1
        });
    }
    render() {
        return <h1 onClick={this.doClick.bind(this)}>hell my-{this.props.name}-class-state-{this.state.count}</h1>
    }
}
console.log(<App name="react"/>);
ReactDOM.render(<App name="react"/>, document.getElementById("root"));

此時點擊文本區域,能夠看到從新渲染出了一個組件,可是舊的組件還在,由於從新渲染的時候,以前的內容並無清空,因此須要在render()函數執行的時候,清空以前渲染的內容,能夠將render函數名修改成_render,而後從新定義一個render函數,render函數中先清空以前渲染的內容,而後調用_rener函數,如:

function _render(vnode, container) {
    // 以前rener的內容
    vnode.children.forEach((child) => {
        return _render(child, element); // 這裏遞歸的時候也要改爲_render
    });
}
function render(vnode, container) {
    container.innerHTML = ""; // 清空以前渲染的內容
    _render(vnode, container);
}

8、支持生命週期

這裏目前只先支持componentWillMountcomponentWillUpdate兩個生命週期,咱們只須要在類組件第一個渲染的位置進行判斷,組件實例上是否有componentWillMount便可,若是有則表示類組件即將渲染,而後在類組件從新渲染的位置進行判斷,組件實例是否有componentWillUpdate,若是有則表示組件即將更新,如:

function _render(vnode, container) {
    if (vnode.isReactComponent) { // 若是是直接渲染類組件
        const component = vnode;
        if (component.componentWillUpdate) { // 這裏是組件從新渲染,若是有componentWillUpdate觸發組件即將更新生命週期
            component.componentWillUpdate();
        }
        component._container = container;
        vnode = vnode.render(); // 直接調用組件對象的render()方法拿到對應的虛擬DOM開始渲染
    }
    if (vnode.tag.prototype && vnode.tag.prototype.render) { // 這是一個類組件
        const component = new vnode.tag(vnode.attrs); // 建立類組件實例
        if (component.componentWillMount) { // 這裏是類組件第一次渲染,若是有componentWillMount,則觸發組件即將掛載生命週期
            component.componentWillMount();
        }
        component._container = container;
        vnode = component.render();
    }
}

9、總結

至此,已經基本實現react的基本功能,包括聲明式渲染、組件支持、setSate、生命週期。其過程爲,首先經過babel將jsx語法進行編譯轉換,babel會將jsx語法解析爲三部分,標籤名、屬性集、子節點,而後用React.createElement()函數進行包裹,react實現createElement函數,用於建立虛擬DOM節點,而後調用render()函數對虛擬DOM節點進行分析,並建立對應的真實DOM,而後掛載到頁面中。而後提供自定義組件的支持,自定義組件,無非就是將jsx定義到了函數和類中,若是是函數,那麼就直接執行就可返回對應的jsx,也即拿到了對應的虛擬DOM,若是是類,那麼就建立組件類實例,而後調用其render()函數,那麼也能夠拿到對應的jsx,也即拿到了對應的虛擬DOM,而後掛載到頁面中。類組件中添加setSate函數,用於更新組件實例上的數據,而後setState函數會觸發組件的從新渲染,從而更新渲染出帶最新數據的組件。

相關文章
相關標籤/搜索