從零開始實現一個React(一):JSX和虛擬DOM

前言

React是前端最受歡迎的框架之一,解讀其源碼的文章很是多,可是我想從另外一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程當中去探索爲何有虛擬DOM、diff、爲何setState這樣設計等問題。html

提起React,老是免不了和Vue作一番對比前端

Vue的API設計很是簡潔,可是其實現方式卻讓人感受是「魔法」,開發者雖然能立刻上手,可是爲何能實現功能卻很難說清楚。node

相比之下React的設計哲學很是簡單,雖然常常有須要本身處理各類細節問題,可是卻讓人感受它很是「真實」,能清楚地感受到本身仍然是在寫js。react

關於jsx

在開始以前,咱們有必要搞清楚一些概念。git

咱們來看一下這樣一段代碼:es6

const title = <h1 className="title">Hello, world!</h1>;

這段代碼並非合法的js代碼,它是一種被稱爲jsx的語法擴展,經過它咱們就能夠很方便的在js代碼中書寫html片斷。github

本質上,jsx是語法糖,上面這段代碼會被babel轉換成以下代碼npm

const title = React.createElement(
    'h1',
    { className: 'title' },
    'Hello, world!'
);

你能夠在babel官網提供的在線轉譯測試jsx轉換後的代碼,這裏有一個稍微複雜一點的例子json

準備工做

爲了集中精力編寫邏輯,在代碼打包工具上選擇了最近火熱的零配置打包工具parcel,須要先安裝parcel:數組

npm install -g parcel-bundler

接下來新建index.jsindex.html,在index.html中引入index.js

固然,有一個更簡單的方法,你能夠直接下載這個倉庫的代碼:

https://github.com/hujiulong/...

注意一下babel的配置
.babelrc

{
    "presets": ["env"],
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

這個transform-react-jsx就是將jsx轉換成js的babel插件,它有一個pragma項,能夠定義jsx轉換方法的名稱,你也能夠將它改爲h(這是不少類React框架使用的名稱)或別的。

準備工做完成後,咱們能夠用命令parcel index.html將它跑起來了,固然,如今它還什麼都沒有。

React.createElement和虛擬DOM

前文提到,jsx片斷會被轉譯成用React.createElement方法包裹的代碼。因此第一步,咱們來實現這個React.createElement方法

從jsx轉譯結果來看,createElement方法的參數是這樣:

createElement( tag, attrs, child1, child2, child3 );

第一個參數是DOM節點的標籤名,它的值多是divh1span等等
第二個參數是一個對象,裏面包含了全部的屬性,可能包含了classNameid等等
從第三個參數開始,就是它的子節點

咱們對createElement的實現很是簡單,只須要返回一個對象來保存它的信息就好了。

function createElement( tag, attrs, ...children ) {
    return {
        tag,
        attrs,
        children
    }
}

函數的參數 ...children使用了ES6的rest參數,它的做用是將後面child1,child2等參數合併成一個數組children。

如今咱們來試試調用它

// 將上文定義的createElement方法放到對象React中
const React = {
    createElement
}

const element = (
    <div>
        hello<span>world!</span>
    </div>
);
console.log( element );

打開調試工具,咱們能夠看到輸出的對象和咱們預想的一致

圖片描述

咱們的createElement方法返回的對象記錄了這個DOM節點全部的信息,換言之,經過它咱們就能夠生成真正的DOM,這個記錄信息的對象咱們稱之爲虛擬DOM

ReactDOM.render

接下來是ReactDOM.render方法,咱們再來看這段代碼

ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.getElementById('root')
);

通過轉換,這段代碼變成了這樣

ReactDOM.render(
    React.createElement( 'h1', null, 'Hello, world!' ),
    document.getElementById('root')
);

因此render的第一個參數實際上接受的是createElement返回的對象,也就是虛擬DOM
而第二個參數則是掛載的目標DOM

總而言之,render方法的做用就是將虛擬DOM渲染成真實的DOM,下面是它的實現:

function render( vnode, container ) {
    
    // 當vnode爲字符串時,渲染結果是一段文本
    if ( typeof vnode === 'string' ) {
        const textNode = document.createTextNode( vnode );
        return container.appendChild( textNode );
    }

    const dom = document.createElement( vnode.tag );

    if ( vnode.attrs ) {
        Object.keys( vnode.attrs ).forEach( key => {
            if ( key === 'className' ) key = 'class';            // 當屬性名爲className時,改回class
            dom.setAttribute( key, vnode.attrs[ key ] )
        } );
    }

    vnode.children.forEach( child => render( child, dom ) );    // 遞歸渲染子節點

    return container.appendChild( dom );    // 將渲染結果掛載到真正的DOM上
}

這裏注意React爲了不類名class和js關鍵字class衝突,將類名改爲了className,在渲染成真實DOM時,須要將其改回。

這裏其實還有個小問題:當屢次調用render函數時,不會清除原來的內容。因此咱們將其附加到ReactDOM對象上時,先清除一下掛載目標DOM的內容:

const ReactDOM = {
    render: ( vnode, container ) => {
        container.innerHTML = '';
        return render( vnode, container );
    }
}

渲染和更新

到這裏咱們已經實現了React最爲基礎的功能,能夠用它來作一些事了。

咱們先在index.html中添加一個根節點

<div id="root"></div>

咱們先來試試官方文檔中的Hello,World

ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.getElementById('root')
);

能夠看到結果:
圖片描述

試試渲染一段動態的代碼,這個例子也來自官方文檔

function tick() {
    const element = (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {new Date().toLocaleTimeString()}.</h2>
        </div>
      );
    ReactDOM.render(
        element,
        document.getElementById( 'root' )
    );
}

setInterval( tick, 1000 );

能夠看到結果:
圖片描述

後話

這篇文章中,咱們實現了React很是基礎的功能,也瞭解了jsx和虛擬DOM,下一篇文章咱們將實現很是重要的組件功能。

最後留下一個小問題
在定義React組件或者書寫React相關代碼,無論代碼中有沒有用到React這個對象,咱們都必須將其import進來,這是爲何?

例如:

import React from 'react';    // 下面的代碼沒有用到React對象,爲何也要將其import進來
import ReactDOM from 'react-dom';

ReactDOM.render( <App />, document.getElementById( 'editor' ) );

不知道答案的同窗再仔細看看這篇文章哦

從零開始實現React系列

React是前端最受歡迎的框架之一,解讀其源碼的文章很是多,可是我想從另外一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程當中去探索爲何有虛擬DOM、diff、爲何setState這樣設計等問題。

整個系列大概會有六篇左右,我每週會更新一到兩篇,我會第一時間在github上更新,有問題須要探討也請在github上回復我~

博客地址: https://github.com/hujiulong/blog
關注點star,訂閱點watch
相關文章
相關標籤/搜索