React是用於構建用戶界面的 JavaScript
庫。其有着許多優秀的特性,使其受到大衆的歡迎。
① 聲明式渲染:
所謂聲明式,就是關注結果,而不是關注過程。好比咱們經常使用的html標記語言就是一種聲明式的,咱們只須要在.html文件上,寫上聲明式的標記如<h1>這是一個標題</h1>
,瀏覽器就能自動幫咱們渲染出一個標題元素。一樣react中也支持jsx的語法,能夠在js中直接寫html,因爲其對DOM操做進行了封裝,react會自動幫咱們渲染出對應的結果。html
② 組件化:
組件是react的核心,一個完整的react應用是由若干個組件搭建起來的,每一個組件有本身的數據和方法,組件具體如何劃分,須要根據不一樣的項目來肯定,而組件的特徵是可複用,可維護性高。node
③ 單向數據流:
子組件對於父組件傳遞過來的數據是只讀的。子組件不可直接修改父組件中的數據,只能經過調用父組件傳遞過來的方法,來間接修改父組件的數據,造成了單向清晰的數據流。防止了當一個父組件的變量被傳遞到多個子組件中時,一旦該變量被修改,全部傳遞到子組件的變量都會被修改的問題,這樣出現bug調試會比較困難,由於不清楚究竟是哪一個子組件改的,把對父組件的bug調試控制在父組件之中。react
以後的內容,咱們將一步步瞭解React相關知識,而且簡單實現一個react。webpack
剛接觸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
由於咱們要實現一個簡單的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");
此時項目雖然能編譯jsx了,可是執行的時候會報錯,由於尚未引入React以及其createElement()方法,React的createElement()方法做用就是建立虛擬DOM,虛擬DOM其實就是一個普通的JavaScript對象,裏面包含了tag、attrs、children等屬性。
① 在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節點了,因爲虛擬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事件,此時啓動項目執行,能夠看到頁面上已經能看到渲染後的結果了,而且點擊文字,能夠看到事件處理函數執行了。
此時已經完成了基本的聲明式渲染功能了,可是目前只能渲染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了。
②支持類組件
在定義類組件的時候,是經過繼承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節點 } ...... }
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); }
這裏目前只先支持componentWillMount
和componentWillUpdate
兩個生命週期,咱們只須要在類組件第一個渲染的位置進行判斷,組件實例上是否有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(); } }
至此,已經基本實現react的基本功能,包括聲明式渲染、組件支持、setSate、生命週期。其過程爲,首先經過babel將jsx語法進行編譯轉換,babel會將jsx語法解析爲三部分,標籤名、屬性集、子節點,而後用React.createElement()函數進行包裹,react實現createElement函數,用於建立虛擬DOM節點,而後調用render()函數對虛擬DOM節點進行分析,並建立對應的真實DOM,而後掛載到頁面中。而後提供自定義組件的支持,自定義組件,無非就是將jsx定義到了函數和類中,若是是函數,那麼就直接執行就可返回對應的jsx,也即拿到了對應的虛擬DOM,若是是類,那麼就建立組件類實例,而後調用其render()函數,那麼也能夠拿到對應的jsx,也即拿到了對應的虛擬DOM,而後掛載到頁面中。類組件中添加setSate函數,用於更新組件實例上的數據,而後setState函數會觸發組件的從新渲染,從而更新渲染出帶最新數據的組件。