React原始碼解析(1):jsx語法是如何解析

fiveoneLei發表於2018-11-16

首先我們來看看下面的程式碼

  import "react" from "react";
  const element = (<div>
        <div>
            <span>1</span>
            <span>2</span>
            <span>3</span>
        </div>
        <div>1</div>
        <div>2</div>
</div>)
console.log(element)
複製程式碼

玩玩

問題來了,element是如何輸出上圖所示的結構的?

環境配置

安裝reactbabel

npm i react react-dom --save
npm i @babel/core @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
複製程式碼

配置babel

{
    test: /\.(js|jsx)$/,
    include: paths.appSrc,
    loader: require.resolve('babel-loader'),
    options: {
        {
            "presets": [
                "@babel/preset-env"
            ],
            "plugins": [
                "@babel/plugin-transform-react-jsx"
            ]
        },
        cacheDirectory: true,
    }
}
複製程式碼

@babel/plugin-transform-react-jsx做了什麼?

遇到
    <div>123</div>
執行
React.createElement("div", "123");

遇到
    <div>
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
執行
    React.createElement("div", 
        React.createElement("div", "1"),
        React.createElement("div", "2"),
        React.createElement("div", "3")
    )
// 也就是說,用react開發的時候只要你用到了jsx語法,那麼不管你有沒有用到React都必須import react from "react"
複製程式碼

寫個函式來模擬它的執行過程

為了便於理解 我們把
<div>
    <div>
        <span>1</span>
        <span>2</span>
        <span>3</span>
    </div>
    <div>1</div>
    <div>2</div>
</div>
當做一棵樹
let element = {
    type:"div",
    children:[{
        type:"div",
        children:[{
            type:"span",
            children:"1"
        }, {
            type:"span",
            children:"2"
        }, {
            type:"span",
            children:"3"
        }]
    }, {
        type:"div",
        children:1
    }, {
        type:"div",
        children:2
    }]
}
寫一個函式對這顆樹進行深度遍歷

function jsxTransformNode(element, callback){
    let children = [];
    if (Array.isArray(element.children)) {  
        children = element.children.map(child => jsxTransformNode(child, callback))
    } else {
        children = [element.chidren]
    }
    return callback(element.type, ...children);
}

let nodes = jsxTransformNode(child, function ReactCreateElement(type, ...children){
    return {
        tag: type,
        children
    }
}) 
複製程式碼

React原始碼解析(1):jsx語法是如何解析

@babel/plugin-transform-react-jsx的原理

babel不熟的話可以先看這邊文章從零開始編寫一個babel外掛

它其實就是將

<div className="name" age="12">
    <div>1</div>
    <div>2</div>
    <div>3</div>
</div>
轉化為
React.createElement(
    "div",
    {},
    React.createElement("div", {}, ...chidren),
    React.createElement("div", {}, ...chidren),
    React.createElement("div", {}, ...chidren)
)
程式碼塊
複製程式碼

廢話不多說直接上程式碼,下面是我寫的一個簡單的babel-plugin來對jsx語法進行解析

var generator = require("@babel/generator").default
function buildAttrsCall (attribs, t){
    let properties = [];
    attribs.forEach(attr => {
        let name = attr.name.name;
        let value = attr.value;
        properties.push(t.objectProperty(t.stringLiteral(name), value))
    });
    return t.ObjectExpression(properties);
}
const createVisitor = (t) => {
    const visitor = {};
    visitor.JSXElement = {
        // 為什麼是exit,因為jsx是DFS而不是BFS;
        exit(path, file){
            let openingPath = path.get("openingElement");
            let children = t.react.buildChildren(openingPath.parent);
            let tagNode = t.identifier(openingPath.node.name.name);
            // 建立React.createElement
            let createElement =  t.memberExpression(t.identifier("React"),t.identifier("createElement"));
            // 建立屬性
            let attribs = buildAttrsCall(openingPath.node.attributes, t);
            // 建立React.createElement(tag, attrs, ...chidren)表示式
            let callExpr = t.callExpression(createElement, [tagNode, attribs, ...children]);
            path.replaceWith(t.inherits(callExpr, path.node));
        }
    }
    return {
        visitor,
        // 配置jsx解析器
        inherits:() => {
            return {
                manipulateOptions(opts, parserOpts) {
                    parserOpts.plugins.push("jsx");
                }
            };

        }
    }
}
module.exports = function(babel){
    const t = babel.types;
    return createVisitor(t);
}
複製程式碼
  1. 建立tagNode變數
  2. 建立React.createElement表示式
  3. 建立attribs物件
  4. 建立React.createElement("div", {}, ...children)表示式
  5. 最後替換node

效果如下

原始碼如下

const a = <div className="name" age="12">
    <div>1</div>
    <div>2</div>
    <div>3</div>
</div>;

複製程式碼

編譯之後

var a = React.createElement(div, {
  "className": "name",
  "age": "12"
}, React.createElement(div, {}, "1"), React.createElement(div, {}, "2"), React.createElement(div, {}, "3"));
console.log(a);

複製程式碼

相關文章