没用到React为啥我写jsx还需要import它啊喂?

#react

提问

当我写一个纯函数组件的时候,为什么还特么需要import React from 'react' 代码如下

import React from 'react';

export default () => <div>你是傻X,爱我吗?</div>

从JSX说起

const Fool = (
    <div className="fool"><b>I'm fool, But I love you</b></div>
)

我们还知道想要编译这样的语法有一个Babel插件,babel-plugin-transform-react-jsx。如上JSX会编译成以下数据结构

既然能知道它会被Babel编译成啥样,那我们只需要解析这个对象即可组装成真正的DOM。

所以React有一个createElement方法,用来解析这编译后的对象

React.createElement(
  "div",
  { className: "fool" },
  "I'm fool, But I love you"
);

假装实现一个假的React?那我就叫她Tcaer

在这我们把createElement方法改写成与Vue一样(在Vue里创建Vdom用得是h方法。虽然我不知道为什么叫h。这么带有颜色的字母) 同时我们需要配置以下我们的.babelrc,否则不支持这样的语法编译。

{
  "presets": ["env"],
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "Tcaer.h" // 这里改成你想要的,比如Love.you
    }]
  ]
}
// tcaer.js
const Tcaer = {
  h,
};

/**
React.createElement(
  "div",
  { className: "fool" },
  "I'm fool, But I love you"
);
**/

// 通过上面的样本代码,我们可以发现也就3个参数罢了。
function h( tag, attrs, ...children ) {
  return {
    tag,
    attrs,
    children
  }
}

export default Tcaer;

接着我们知道React是通过ReactDOM.render(<Component>, root),挂载我们组装后的DOM。那我们也需要实现一个TcaerDOM.render方法。

再看一遍这张图,我们来实现render方法。

<div onClick={test} a="aaa" 
     className={'sdf'} 
     style={{width: 10, color: 'red'}} 
     id='id1'
>
    I'm fool, But I love you
</div>

这里其实要做两件事,第一件事是要把vnode转换为DOM,第二件事则是处理attrs设置到DOM上。

关于处理vnode,我们只需要获取tag,然后递归渲染children即可。如下

  // 处理Node为text的情况
  if (typeof vnode === 'string') {
    const textNode = document.createTextNode(vnode);
    return container.appendChild(textNode);
  }

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

  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach((item) => {
      // TODO 设置样式,之后实现
      setAttributes()
    });
  }

  vnode.children.length && vnode.children.forEach((child) => render(child, parentDOM));

DOM处理好后,我们来实现以下setAttributes。其实要做的处理很简单,className转换为class,解析onXXX事件类型,处理style对象。剩下的就只是单纯的DOM属性设置了,我们统统setAttribute即可搞定。

于是我们的逻辑代码就类似如下。

function setAttributes(dom, key, value) {
    // 1.转换className
    if (name === 'className') name = 'class';

    // 2.处理事件类型
    if (/on\w+/.test(name)) {
      name = name.toLowerCase();
      dom[name] = value;
      return;
    }

    // 3.处理style对象
    if (name === 'style') {
        Object.keys(value).forEach((item) => {
          const styleValue = value[item];
          dom.style[item] = typeof styleValue === 'number' ? `${styleValue}px` : styleValue;
        });
      return;
    }

    // 4.直接粗暴处理通常的DOM属性
    dom.setAttribute(name, value);
}

把这两个过程实现了之后,我们就可以实现TcaerDom.render了。以下是完整代码

function setAttributes(dom, name, value) {
  if (name === 'className') name = 'class';

  // event
  if (/on\w+/.test(name)) {
    name = name.toLowerCase();
    dom[name] = value;
    return;
  }

  // style
  if (name === 'style') {
    Object.keys(value).forEach((item) => {
      const styleValue = value[item];
      dom.style[item] = typeof styleValue === 'number' ? `${styleValue}px` : styleValue;
    });
    return;
  }

  dom.setAttribute(name, value);
}

function render(vnode) {
  if (typeof vnode === 'string') {
    return document.createTextNode( vnode );
  }

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

  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach((item) => {
      setAttributes(parentDOM, item, vnode.attrs[item]);
    });
  }

  vnode.children.length && vnode.children.forEach((child) => render(child, parentDOM));

  return parentDOM;
}

const TcaerDom = {
  // 没想到把,render还有个callback方法,很多人会遗忘它。这里我们暂时没有实现得与react一样。只会回调一次。
  render(vnode, container, callback) {
    container.innerHTML = ''; // 每次渲染需要清空一次root DOM

    container.appendChild(render(vnode,container));

    callback && callback();
  },
};

export default tcaerDom;

最后我们把它拿去测试一下就好,可以发现效果与React官网上的类似。

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

setInterval(tick, 1000);

整理后的代码都在这里 >> Tcaer <<