手把手教你手写一个React - 01 (实现JSX与createElement)

784 阅读3分钟

前言

大家好这里是阳九,一个中途转行的野路子码农,热衷于研究和手写前端工具.

我的宗旨就是 万物皆可手写

本专栏会慢慢更新手写react的过程,请关注 造轮子系列-手把手教你手写一个React - 不月阳九的专栏 - 掘金 (juejin.cn)

新手创作不易,有问题欢迎指出和轻喷,谢谢

本专栏适合有一定React开发经验,对webpack,babel有一定了解的前端攻城狮,如果没有请绕道恶补基础知识)

今天我们来手写React的第一部分 JSX和createElement

react中的虚拟dom

首先我们来看一下react中的div是什么

const reactDiv = (
  <div>
    <span>内容</span>
  </div>
)
console.log(reactDiv);

结果如下 我们可以看到 在.jsx文件中写下的标签 首先会转变成一个React.element数据结构,这就是react中的虚拟dom,里面存放了props,type,key等属性, 子节点存放在props.children中,形成虚拟dom树 image.png

JSX的转换

我们知道,在react中写的js代码会被转化为jsx,从而生成虚拟dom

JSX的支持是由babel来做的,我们可以看一下babel/preset这个工具做了些什么

我们打开babel官网选择试一试

image.png

// div会被转换成一个函数,React.createElement, 里面传入三个参数
React.createElement(tag,props,[...children])

嵌套的dom

image.png

所以当我们执行App函数时,会执行一连串的createElement函数

相信看到这里大家已经有一定的思路去实现了,那么我们来通过代码实现一下

使用@babel/preset-react编译.jsx文件内代码

npm i @babel/preset-react 
npm i @babel/core
  1. 先写一个编译.jsx文件的webpack loader
const jsx = require("@babel/core")
 // 这里的source会接收.jsx文件内的所有字符串
function jsxLoader(source){
    // 使用@babel/preset-react进行编译获取编译后代码
     const { code } = jsx.transformSync(source, {
        presets: ["@babel/preset-react"],
    });
    // 将新的字符串返回 重新写入文件
    return `${code}`
}
  1. 配置webpack 使用写下的loader
const myJsxLoader = require('...') // loader路径

module.exports = {
   ...
   rules:[
       {
            test: /\.jsx$/,
            use: [myJsxLoader] //! .jsx文件经过myJsxLoader编译
        }
   ]
}

通过babel编译后,我们就可以获得这样的.jsx文件

"use strict";

function App() {
  return /*#__PURE__*/ React.createElement(
    "div",
    {id: 1},
    null
  );
}

实现createElement方法

这里是本人实现的简易代码 和真实react有出入 大致思路就是将createElement函数传入的三个参数分别做解析,并生成对应的element数据结构

// 通过解析来的JSX创建Element树
export function createElement(...args: any[]): any {
    let key;
    let ref;
    let children = [];
    const tag = args[0]
    const config = args[1]
    const childNodes = args.slice(2)


    // 处理tag为函数组件的情况(创建组件Element  执行函数并返回ElementNode)
    if (typeof tag === 'function') {
        let fc = tag
        return {
            $$typeof: Symbol.for('lzyElement'),
            tag: fc.name,
            ref: fc,
            key,
            props: config,
            children: [],
            fiber: undefined
        }
    }

    // 单独处理ref和key
    if (config) {
        ref = config.ref
        key = config.key
        // 删除属性
        delete config?.ref
        delete config?.key
    }

    // 遍历处理childrenNode
    if (childNodes.length > 0) {
        childNodes.forEach((child) => {
            if (isElement(child)) {
                children.push(child)
            } else {
                children.push({
                    $$typeof: Symbol.for('lzyElement'),
                    tag: 'text',
                    text: child,
                    fiber: undefined
                })
            }
        })
    }

    //返回生成的虚拟dom
    return {
        $$typeof: Symbol.for('lzyElement'),
        tag,
        ref,
        key,
        props: config,
        children,
        fiber: undefined
    }
}

最终使用

// --react.js--
// 准备一个自己的React对象, 用于导出各种方法
import {createElement} from './..../createElement'

const React = {createElement}

export default React

编译前

import React from './react.js'

function App(){
    return <div id={1}>文字</div>
}

编译后

"use strict";
import React from './react.js'

function App() {
  return /*#__PURE__*/ React.createElement("div", {id: 1}, "文字");
}

最后我们执行App函数 即可获得构造好的element树

创作不易,请多多支持