天天看點

沒有用到React,為什麼我需要import引入React?沒有用到React,為什麼我需要import引入React?前言關于jsxReact.createElement和虛拟DOMReactDOM.render渲染和更新

沒有用到React,為什麼我需要import引入React?

本質上來說JSX是

React.createElement(component, props, ...children)

方法的文法糖。

是以我們如果使用了JSX,我們其實就是在使用React,是以我們就需要引入React

前言

React是前端最受歡迎的架構之一,解讀其源碼的文章非常多,但是我想從另一個角度去解讀React:從零開始實作一個React,從API層面實作React的大部分功能,在這個過程中去探索為什麼有虛拟DOM、diff、為什麼setState這樣設計等問題。

提起React,總是免不了和Vue做一番對比

Vue的API設計非常簡潔,但是其實作方式卻讓人感覺是“魔法”,開發者雖然能馬上上手,但其原理卻很難說清楚。

相比之下React的設計哲學非常簡單,雖然有很多需要自己處理的細節問題,但它沒有引入任何新的概念,相對更加的幹淨和簡單。

關于jsx

在開始之前,我們有必要搞清楚一些概念。

我們來看一下這樣一段代碼:

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

這段代碼并不是合法的js代碼,它是一種被稱為jsx的文法擴充,通過它我們就可以很友善的在js代碼中書寫html片段。

本質上,jsx是文法糖,上面這段代碼會被babel轉換成如下代碼:

const title = React.createElement(

    'h1',

    { className: 'title' },

    'Hello, world!'

);

React.createElement和虛拟DOM

前文提到,jsx片段會被轉譯成用

React.createElement

方法包裹的代碼。是以第一步,我們來實作這個

React.createElement

方法

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

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

第一個參數是DOM節點的标簽名,它的值可能是

div

h1

span

等等

第二個參數是一個對象,裡面包含了所有的屬性,可能包含了

className

id

從第三個參數開始,就是它的子節點

我們對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')

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

    React.createElement( 'h1', null, 'Hello, world!' ),

是以

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 => {

            const value = vnode.attrs[ key ];

             setAttribute( dom, key, value );    // 設定屬性

        } );

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

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

  設定屬性需要考慮一些特殊情況,我們單獨将其拿出來作為一個方法setAttribute

function setAttribute( dom, name, value ) {

    // 如果屬性名是className,則改回class

    if ( name === 'className' ) name = 'class';

    // 如果屬性名是onXXX,則是一個事件監聽方法

    if ( /on\w+/.test( name ) ) {

        name = name.toLowerCase();

        dom[ name ] = value || '';

    // 如果屬性名是style,則更新style對象

    } else if ( name === 'style' ) {

        if ( !value || typeof value === 'string' ) {

            dom.style.cssText = value || '';

        } else if ( value && typeof value === 'object' ) {

            for ( let name in value ) {

                // 可以通過style={ width: 20 }這種形式來設定樣式,可以省略掉機關px

                dom.style[ name ] = typeof value[ name ] === 'number' ? value[ name ] + 'px' : value[ name ];

            }

        }

    // 普通屬性則直接更新屬性

    } else {

        if ( name in dom ) {

            dom[ name ] = value || '';

        if ( value ) {

            dom.setAttribute( name, value );

        } else {

            dom.removeAttribute( name );

  這裡其實還有個小問題:當多次調用

render

函數時,不會清除原來的内容。是以我們将其附加到ReactDOM對象上時,先清除一下挂載目标DOM的内容:

const ReactDOM = {

    render: ( vnode, container ) => {

        container.innerHTML = '';

        return render( vnode, container );

渲染和更新

到這裡我們已經實作了React最為基礎的功能,可以用它來做一些事了。

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

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

  我們先來試試官方文檔中的

Hello,World

 

繼續閱讀