React學習篇-JSX(手寫一個JSX的插件)

學習和閱讀 vue 源碼有段時間了,最近在嘗試去學習 react,因爲眼前項目使用不上 react,並不想一股腦的學習它的 API(長時間不用仍是會忘),因此這次的學習過程打算換種方式,對於 react 涉及到的每一個點嘗試逐個深刻,瞭解其解析過程及整個框架的思路。html

對於每一個點的學習和深刻,將以文章的形式產出,主要是對於學習的內容的記錄(因此看來內容有點多),方便本身之後是用時查閱和回顧。vue

從demo開始

在此以前,曾屢次的在 react 入門的邊緣來回試探,每次都止於寫一個簡單的 demo,我相信下面這個你們確定很熟悉,本文也是從這裏開始的。node

npx create-react-app my-app
cd my-app
npm install
npm start
複製代碼

而後應該就能跑起來(環境和安裝沒有問題的話),簡化下代碼,而後面對下面的這個代碼陷入了思考,雖然之前見過也寫過不少次了。react

代碼以下:webpack

import React from 'react';

function App() {
  return (
    <div> <h1>good good study, day day up</h1> </div>
  );
}

export default App;
複製代碼

APP 返回的乍一看很像 html,固然相信不少人都知道這個是 JSX 的語法。那麼問題來了:web

JSX 語法寫的模版,如何生成真實的 dom?npm

類比咱們先看看Vuetemplate - ast - code -vnode - dom的實現。瀏覽器

簡單看下Vue 的parse過程

template 轉換成 ast 及 ast 轉換成 code 的過程推薦幾篇文章:bash

如下用一個簡單的例子來簡單說明下 parse 的過程babel

template 以下:

<template>
    <div>
        <h1>good good study, day day up</h1>
    </div>
</template>
複製代碼

對應生成的 ast 以下:

// 簡化版,主要是看下結構
{
    //...
    parent: undefined,
    children: [
        {
            parent: {
                //...
                tag: "div",
                type: 1
            },
            children: [
                // ...
                text: "good good study, day day up",
                type: 3
            ],
            tag: "h1",
            type: 1
        }
    ],
    tag: "div",
    type: 1
}
複製代碼

對應生成的 code 以下:

with(this){
    return _c(
        'div',
        [
            _c(
                'h1',
                [_v("good good study, day day up")]
            )
        ]
    )
}
複製代碼

最終獲得的結果就是這樣的渲染函數。

咱們再看看react的實現。首先直接看npm star後的main.chunk.js文件,能夠看到以下的代碼(簡化版):

function App() {
    return createElement(
        "div", 
        {
            __source: {
                fileName: _jsxFileName,
                lineNumber: 5
            },
            __self: this
        }, 
        createElement(
            "h1", 
            {
                __source: {
                    fileName: _jsxFileName,
                    lineNumber: 6
                },
                __self: this
            }, 
            "good good study, day day up"
        )
    );
}
複製代碼

對比 Vue 生成的 code,會發現很像,因此這裏能夠先總結一下:

react 也是經過一層轉換,把咱們寫的 JSX 模版,轉換成對應的函數。

因此這就算完了?來,接着來,JSX 是如何轉換的?

JSX 的轉換過程

瞭解 Vue parse過程的就知道,轉換是發生在編譯的階段:在首次$mount的時候會執行compileToFunctions(其中主要就是模版到渲染函數的過程)。

那 React 呢,嘗試去看了 ReactReactDOM 的源碼,根本找不到任何轉換的代碼。並且你們也看到了main.chunk.js的代碼,咱們寫的 JSX 已經轉換成對應的函數了。因此再此以前,已經完成了轉換。

好了不賣關子了,這裏用的是 babel 解析器(什麼是Babel,Babel能作什麼),咱們首先找到工程中配置的地方。

尋找 babel 配置的入口

因爲本人對於工程配置及工程化不是很瞭解,因此我這裏也是找了好久,要想找到 babel 配置的入口,需先執行(最好找個demo工程執行,該命令不可逆)

yarn eject
複製代碼

找到 /config/webpack.config.js, 相關代碼以下:

module: {
    {
        test: /\.(js|mjs|jsx|ts|tsx)$/,
        include: paths.appSrc,
        loader: require.resolve('babel-loader'),
        options: {
        customize: require.resolve(
            'babel-preset-react-app/webpack-overrides'
        ),
        
        plugins: [
            [
            require.resolve('babel-plugin-named-asset-import'),
            {
                loaderMap: {
                svg: {
                    ReactComponent: '@svgr/webpack?-svgo,+ref![path]',
                },
                },
            },
            ],
        ],
        cacheDirectory: true,
        cacheCompression: isEnvProduction,
        compact: isEnvProduction,
        },
    },
    {
        test: /\.(js|mjs)$/,
        exclude: /@babel(?:\/|\\{1,2})runtime/,
        loader: require.resolve('babel-loader'),
        options: {
        babelrc: false,
        configFile: false,
        compact: false,
        presets: [
            [
            require.resolve('babel-preset-react-app/dependencies'),
            { helpers: true },
            ],
        ],
        cacheDirectory: true,
        cacheCompression: isEnvProduction,
        sourceMaps: false,
    },
}
複製代碼

看到這裏相信就能知道,這裏其實就是配置了 loader,試了看各個解析器的源碼,可是仍然困難重重(各類引用),這裏也是換了種方式來學習解析的過程。

嘗試手寫一個 JSX 的插件。

手寫 JSX 的插件

這裏你們網上搜應該能搜出一堆關於babel 插件的代碼,我這裏也是找到一個基礎的例子。

console的插件的例子

如下是一個將log處理成console.log的插件的代碼:

const babel = require('@babel/core')
const t = require('babel-types')

const code = `
    const a = 3 * 103.5 * 0.8;
    log(a);
    const b = a + 105 - 12;
    log(b);
`

const visitor = {
    CallExpression(path) {
        // 這裏判斷一下若是不是log的函數執行語句則不處理
        if (path.node.callee.name !== 'log') return
        // t.CallExpression 和 t.MemberExpression分別表明生成對於type的節點,path.replaceWith表示要去替換節點,這裏咱們只改變CallExpression第一個參數的值,第二個參數則用它本身原來的內容,即原本有的參數
        path.replaceWith(t.CallExpression(
            t.MemberExpression(t.identifier('console'), t.identifier('log')),
            path.node.arguments
        ))
    }
}

const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
})

console.log(result.code)
複製代碼

處理結果:

const a = 3 * 103.5 * 0.8;
console.log(a);
const b = a + 105 - 12;
console.log(b);
複製代碼

看了代碼後應該差很少能瞭解插件的編寫過程,大體以下:code 首先會解析成 AST,而後會遍歷整個 AST 樹,每一個節點都有其特定的屬性,插件的vistor對象的處理函數會在解析的過程當中被調用,插件要作的事情就是在合適的地方(這裏是CallExpression),符合條件的狀況下(這裏是 path.node.callee.name === 'log'),對解析結果進行更改。知道原理之後,嘗試着寫 JSX 解析的插件。

照葫蘆畫瓢:

const code = `
    var html = <div>
        <h1>good good study, day day up</h1>
    </div>
`

const visitor = {
   
}

const result = babel.transform(code, {
    plugins: [
        {
            visitor: visitor
        }
    ]
})

console.log(result.code)
複製代碼

大體的結構就是這樣,指望達到的目標code對應的輸出以下:

var html = React.createElement(
    "div", 
    null, 
    React.createElement("h1", null, "good good study, day day up")
)
複製代碼

以上代碼執行後,會報錯,由於並非js的標準語法,沒法正常解析,因此這裏首先須要引入一個插件 plugin-syntax-jsx,讓解析器其能識別該種語法。

引入插件,修改的代碼以下:

babel.transform(code, {
    plugins: [
        '@babel/plugin-syntax-jsx',
        {
            visitor: visitor
        }
    ]
})
複製代碼

執行的結果爲:

var html = <div>
    <h1>good good study, day day up</h1>
</div>;
複製代碼

這裏能看到咱們能正常識別 JSX 模版,只是輸出並非咱們須要的,咱們須要把它轉換成咱們的函數。接下來的一步就是須要找到合適的時機。

尋找時機

這裏咱們只是知道咱們能正常識別了,可是在解析的過程當中,其對應的 AST 具體長什麼樣子呢?

這裏也是推薦一個網站,astexplorer.net/

AST的結構圖

這裏就能看到整個 AST 樹的結構(這裏還沒去看解析成 AST 生成的過程,目測和 Vue 中 parseHTML 的過程原理同樣,這裏後續會花點時間看下 babal 生成 AST 的過程),應該很快就能找到咱們想要的關鍵信息-JSXElement,對照以上的 AST 和關鍵信息,就當前這個例子,咱們就思考下‘合適的時機‘-JSXElement的變量賦值:

  • VariableDeclarator
  • init.type === 'JSXElement'

加入‘時機’代碼

加入‘時機’後代碼以下:

const babel = require('@babel/core')

const code = ` var html = <div> <h1>good good study, day day up</h1> </div> `

const visitor = {
    VariableDeclarator(path) {
        if (path.node.init.type === 'JSXElement'){
            console.log('start')
            // deal 
        }
    }
}

const result = babel.transform(code, {
    plugins: [
        '@babel/plugin-syntax-jsx',
        {
            visitor: visitor
        }
    ]
})

console.log(result.code)
複製代碼

獲得的結果以下:

start
var html = <div>
        <h1>good good study, day day up</h1>
    </div>;
複製代碼

固然這裏只是輸入標籤的信息,其中還有不少其餘的節點信息,其餘的信息那麼也就是 JSX 的語法規則了,如循環、class、條件語句、邏輯代碼等語法規則了。本文只作簡單的實現。接下來要作的就是要整合節點的信息,生成對應的函數代碼。

生成代碼

... 未完待續

(這裏涉及到babel-types的使用,因爲對此塊不是很熟悉,文章先進行到這裏,後續寫好會更新上來)

總結

那瞭解了JSX的解析過程後,咱們思考下,這個與vue的parse的過程差異在哪?

  • vue 是編譯階段生成對應的渲染函數,react 是babel解析階段就生成了對應的函數
  • 看過 vue parse階段源碼的同窗應該知道,vue 作了不少處理瀏覽器‘怪異’行爲的操做(爲了保持和瀏覽器行爲的一致性),如:標籤換行會有空格符、canBeLeftOpenTag標籤如:p,會補全關閉標籤等,也就是你們能夠像寫普通的html來寫template。而 react 的 JSX 就有不少的語法規則,如class必須寫className、標籤以前的換行後的空格會被忽略等等(仍在學習JSX語法中,後續會繼續補充完善這塊的區別)。

就第二點區別,能夠看出來,若是是原有的html項目,想要遷移成 vue 或 react,遷移成 Vue 的成本會小不少,Vue 不只在寫法上,還有對於瀏覽器特殊行爲的處理上,都保持了和 html 規範的統一。若要遷移成 react ,可能改形成本就會比較大。

以上只是react初學者的主觀的見解,更多的特性和優劣須要深刻學習後才能瞭解。

相關文章
相關標籤/搜索