手寫 React 系列第一篇之【初始渲染】

畢業生求職中,有坑聯繫~javascript

【github】 【我的簡歷】html

最近感悟

剛畢業不久,轉行前端,自學半年多,以後去找工做,發現工做機會真的很難找,內心焦急萬分。這時候前輩鼓勵我,穩定心態,你所須要作的就是投資本身,當工做機會真正來臨的時候,可以作到一把抓住就能夠了。忽然以爲這句話頗有道理,我想只要天天都有進步,哪怕一時找不到工做,我也不吃虧,頂多不能儘早賺錢。。前端

找準方向

我決定找一個比較流行的框架進行深度學習,在 Vue 和 React 之間,我選擇了 React,主要是由於對 React 框架比較熟,並且大廠對 React 的使用度比較高。java

明確目標

這個系列的目標有三個:react

  • 首先在學習過程當中,可以加深本身對於 React 的理解,作到手寫一個簡單的 React 框架。
  • 其次總結 React 框架中的一些優秀的算法思想。
  • 最後但願這些內容可以幫助到他人。

正文開始

本文以 15.X 版本的 React 框架進行學習,在實現 類 React 框架以前,咱們先看看須要有什麼前置知識?webpack

  • 熟練的原生 JS 技能。
  • JSX 語法。
  • Webpack 構建能力。

JSX 和虛擬 DOM

咱們聲明以下一個組件實例。git

var component = <div>測試中</div>;
複製代碼

webpack 打包的時候會自動將上面的寫法轉換成 React.createElement 的方式,最終返回虛擬DOM 對象,以下所示。github

var virtualDOM = {
    props:{
        children:['children'],
    },
    type:'div'

    
}
複製代碼

實際上 React 的虛擬 DOM 對象還有一些其餘屬性,好比 key,ref,這裏爲了簡單起見,本節咱們只講解初始渲染過程,因此咱們只關注和渲染相關的 propstype 兩個關鍵屬性,到後面講解 diff 的時候,再去關注他們。web

JSX 轉化成虛擬 DOM

上面的轉換須要藉助 JSX 語法解析器的能力,解析器可以將 JSX 元素編譯爲一個虛擬 DOM 對象,即 virtualDOM。算法

藉助虛擬 DOM,咱們能夠不須要真正操做一個 DOM 對象,而是將實際的 DOM 對象映射給一個 JS 對象中,經過對比虛擬 DOM 來決定是否要操做真實 DOM。

JSX 首先在編譯器由 webpack 結合 babel 將 JSX 元素編譯爲以下格式的代碼,以上面代碼爲例。

var component = <div>測試中</div>
複製代碼

這段代碼在編譯期間轉化爲以下代碼:

var component = React.createElement("div", null, "\u6D4B\u8BD5\u4E2D");
複製代碼

當有兩個子元素的時候,

var component = <div>測試中<button>知道了</button></div>
複製代碼

編譯爲

var component = React.createElement("div", null, "\u6D4B\u8BD5\u4E2D", React.createElement("button", null, "\u77E5\u9053\u4E86"));
複製代碼

那麼,當webpack將 JSX 語法編譯爲 React.createElement 方法調用以後,如何再經過createElement 方法返回虛擬 DOM 對象呢?

這就要涉及到 createElement 的函數實現了,接下來咱們實現它。

首先,該方法接收一系列參數。

createElement(type, props, children1, children2, ...);
複製代碼
  • type 表明虛擬 DOM 的節點類型。
  • props 表明虛擬 DOM 的屬性。
  • children1,children2,... 表明子節點。

咱們的 createElement 方法須要將這些參數組裝成一個 虛擬DOM 對象輸出出去,實現比較簡單,以下:

function createElement(type, props, ...children){
    props = {...props};
    if(children.length > 0){
        props.children = children;
    }
    
    return {
        type,
        props
    }
    
}
複製代碼

render 實現

那麼,有了虛擬 DOM 了,下一步要作的事就是將虛擬 DOM 轉化爲真實 DOM ,而且將真實 DOM 插入到頁面中。

咱們來看一段經典的 React 代碼。

import React from 'react';
import ReactDOM from 'react-dom';

var app = <div>首次渲染</div>;

ReactDOM.render(app, document.querySelector('#root'));
複製代碼

咱們看下 render 方法如何實現。

元素、組件、組件實例的關係

在繼續往下講解以前,咱們先了解一下 React 中的概念。

  • 元素 元素是指經過 createElement 方法建立出來的 JS 對象,即虛擬 DOM,它會對應 html 文檔裏的一個真實 DOM 片斷。

  • 組件類

    • 組件類是一個構造函數或者一個類,它可以生成組件實例,組件實例經過必定的方法生成元素,具體的方法後面咱們也會講解。
    • 組件的另外一個好處是方便複用和擴展。
  • 組件實例。 咱們一般說組件的時候,其實有時候是指組件類,有時候是指組件實例,很容易引發歧義。接下來的篇幅,咱們會分清楚這兩個概念。

渲染方法的實現

  • 最簡單的虛擬 DOM

看下面最簡單的元素:

var component = 1;
var component1 = '2';
var component2 = true;
var component3 = null;
var component4 = undefined;
複製代碼

咱們的 render 方法須要可以處理它們,處理策略以下:

  • 針對 null、undefined、布爾類型的值,返回一個空字符串的 text 節點。
  • 針對字符串,返回該字符串對應的 text 節點。
  • 針對數字,返回該數字對應的 text 節點。

針對以上這三種狀況,render 的實現以下:

function render(vdom, container){
    let dom = renderElement(vdom);
    container.appendChild(dom);
}

function renderElement(vdom){
    if(typeof vdom === 'boolean' ||  vdom === null || vdom === undefined){
        return document.createTextNode('');
    }
    if(typeof vdom === 'number'){
        return document.createTextNode(String(vdom));
    }
    if(typeof vdom === 'string'){
        return document.createTextNode(vdom);
    }
}
複製代碼
  • 稍微簡單一些的虛擬 DOM

假設這樣一個元素:

var component = <div>測試中</div>;
複製代碼

對應虛擬 DOM 也就是

{
    props: {
        children: ["測試中"]
    },
    type: "div"
}
複製代碼

那咱們的render方法須要要怎麼改進,才能夠對其進行渲染呢?

很明顯咱們能夠根據 type 區分,對 type 爲 string 類型的虛擬 DOM 單獨處理,下面看下具體實現。

//改造 renderElement 方法。
function renderElement(vdom){
    //基礎數據類型處理
    //此處略。
    if(typeof vdom.type === 'string'){
        return renderNativeDom(vdom);
    }
}
// 將類型爲原生節點的虛擬 DOM 轉化爲真實 DOM。
function renderNativeDom(vdom){
    let dom = document.createElement(vdom.type);
    const { props: { children }} = vdom;
    for(var i = 0; i < children.length; i++){
        // 渲染子節點
        let childNode = renderElement(children[i]);
        dom.appendChild(childNode);
    }
    return dom;
}
複製代碼
  • 函數組件

再來看比較經常使用的函數組件。

function A(props){
    return <div>測試中</div>
}
複製代碼

上面這種方式實際上定義了一個函數組件類,注意是組件類,不是組件實例。

那麼咱們用的時候,就要經過這樣的方式來實例化組件了。

var component = <A />; 複製代碼

咱們看下,該如何改造 renderElement 方法。

按照慣例,咱們先看下函數組件實例對應的虛擬 DOM 形式。

{
    props: {},
    type: ƒ B()
}
複製代碼

可見,type 是一個函數,這個函數就是組件類的構造函數。因此咱們能夠依然能夠根據 type進行區分。

  • 策略以下:

    • 針對 type 爲 function 的虛擬 DOM,經過執行該 function 來拿到待渲染的虛擬 DOM。

    • 以後的策略就很簡單了,遞歸執行 renderElement 方法就好了。

咱們看下實現

//改造 renderElement
function renderElement(vdom){
    //函數組件實例的判斷
    if(typeof vdom.type === 'function'){
        return return renderFunctionComponent(vdom);
    }
}
// 增長對函數組件實例的渲染。
function renderFunctionComponent(vdom){
    let inst = vdom.type(vdom.props);
    return renderElement(inst);
}
複製代碼
  • 類組件

最後,咱們看下類組件,看一個最經常使用的組件類的定義。

class Header extends React.Component{
    render(){
        return <div>類組件實例</div>
    }
}
複製代碼

構造一個類組件實例:

var component = <Header />; console.log(component); 複製代碼

看下這種類型實例對應的虛擬 DOM 是什麼樣子的。

{
    props: {}
    type: ƒ Header()
}
複製代碼

可見,類組件實例對應的虛擬 DOM 的 type 也是 函數,因此,咱們沒法單純根據虛擬 DOM 的type 來區分函數組件和類組件的實例了,那怎麼區分這兩種實例呢?

還記得類組件定義的時候,要繼承的 Component 抽象類嗎?咱們能夠根據這個特色進行區分,只須要判斷當前組件實例是不是 Component 的實例便可。

看下代碼實現。

// 增長組件基類 Component
class Component(){
    constructor(props){
        this.state = {};
        this.props = props || {};
    }
    setState(nextState){
        
    }
}

//改造 renderElement 方法,識別類組件和函數組件。
function renderElement(vdom){
    if(typeof vdom.type === 'function'){
        //區分類組件仍是函數組件。
        if(vdom.type.prototype instanceof React.Component){
            //類組件
            return renderClassComponent(vdom);
        } else {
            //函數組件
            return renderFunctionComponent(vdom);
        }
    }
}

function renderClassComponent(vdom){
    let inst = new vdom.type(vdom.props);
    let vd = inst.render();
    return renderElement(vd);
}
複製代碼

總結

以上就是咱們 React 的第一個環節,初始渲染,代碼整理以下:

  • React.js
function createElement(type, props, ...children){
    props = {...props};
    if(children.length > 0){
        props.children = children;
    }
    
    return {
        type,
        props
    }
}

class Component(){
    constructor(props){
        this.state = {};
        this.props = props || {};
    }
    setState(nextState){
        
    }
}

export default {
    createElement,
    Component
}
複製代碼
  • React-Dom.js
function render(vdom, container){
    let dom = renderElement(vdom);
    container.appendChild(dom);
}

function renderElement(vdom){
    if(typeof vdom === 'boolean' ||  vdom === null || vdom === undefined){
        return document.createTextNode('');
    }
    if(typeof vdom === 'number'){
        return document.createTextNode(String(vdom));
    }
    if(typeof vdom === 'string'){
        return document.createTextNode(vdom);
    } 
    if(typeof vdom.type === 'function'){
        //區分類組件仍是函數組件。
        if(vdom.type.prototype instanceof React.Component){
            //類組件
            return renderClassComponent(vdom);
        } else {
            //函數組件
            return renderFunctionComponent(vdom);
        }
    }
}


function renderClassComponent(vdom){
    let inst = new vdom.type(vdom.props);
    let vd = inst.render();
    return renderElement(vd);
}


function renderFunctionComponent(vdom){
    let inst = vdom.type(vdom.props);
    return renderElement(inst);
}

function renderNativeDom(vdom){
    let dom = document.createElement(vdom.type);
    const { props: { children }} = vdom;
    for(var i = 0; i < children.length; i++){
        // 渲染子節點
        let childNode = renderElement(children[i]);
        dom.appendChild(childNode);
    }
    return dom;
}
複製代碼

結語

本節咱們只實現了 React 的初始渲染,你們會發現咱們並無針對屬性作處理,也並無針對 state 變化引起界面渲染的邏輯。

不要緊,咱們一步步來,下一節咱們講解屬性、state,以及組件生命週期的處理。

再見~

相關文章
相關標籤/搜索