React系列:一个简化版react 核心API

1,308 阅读6分钟

介绍

官网上这么解释React的“用于构建用户界面的JavaScript库”,React 使用声明式组件化的方式编写UI,让你的代码更加可靠,且方便调试。

React源码还是非常复杂的,我们今天就来简化的实现下React结构核心API

  • React.createElement
  • React.Component
  • ReactDom.render

先了解下JSX

1.什么是 JSX?

  • React使用 JSX 来替代常规的 JavaScript
  • JSX 是一个看起来很像XML的JavaScript语法扩展。

2.为什么需要在React用 JSX?

  • JSX 执行更快,因为它的编译为JavaScript代码后进行了优化。
  • 进行类型安全检查,可防止注入攻击,在编译过程中就能发现错误。
  • 编写模块更加简单快捷。

3.怎么用 JSX,这里就不详细阐述了,建议看看官方文档。

4.原理:babel-loader会预编译 JSX 为React.createElement(...)

接下来我逐步来实现简易的React核心 API,我从原生DOM、函数组件、类组件3种类型来剖析。

先看看测试示例源码 index.js

大家知道,JSX 最终转换成普通的JavaScipt对象,我在这直接声明一个 JSX 变量里面包括元素,另外增加了自定义组件,这里创建是函数组件并接受props参数,这为什么传props?带着这个疑问往下看。 组件写就得让其在页面渲染出来,应该导入我们熟悉的react-dom包,这包提供个render方法,该方法最常见接收2个参数,一个是我要渲染的 JSX,另个就是挂的根容器。

import ReactDOM from 'react-dom'; 

// function Component
function Comp(props){
  return <h2>hi {props.name}</h2>
}

const jsx  = (
  <div id="demo" style={{color:"red",border:'1px solid blue'}}>
    <span>hi</span>
    <Comp name="函数组件"></Comp>
  </div>
);
console.log(jsx); // 输出看下具体结构
ReactDOM.render(jsx,document.querySelector('#root'))

以上代码测试肯定是编译不成功的,结果报了一坨错误: 错误提示说根本没有声明React,因为我这根本没导入React。 为什么要导入React? 原因是 JSX 在webpack进行打包的时候都会用过babel-loader转换成React.createElement的形式。如下图:

// function Component
function Comp(props) {
  return React.createElement(
    "h2",
    null,
    "hi ",
    props.name
  );
}

const jsx = React.createElement(
  "div",
  { id: "demo", style: { color: "red", border: '1px solid blue' } },
  React.createElement(
    "span",
    null,
    "hi"
  ),
  React.createElement(Comp, { name: "函数组件" })
);

运行起来,我们看下 JSX 输出是怎么样一个数据结构:

从数据结构上看就是个普通的Object,这里有很多属性值得注意,比如:

  • key好比是渲染列表的时候传的唯一值,主要作用是为了提高在diff过程的效率;
  • props属性里既包含当前元素属性又包含子元素(children),仔细观察children元素数据结构与其父元素是一样的;
  • ref用于引用 DOM;
  • type就更有用了,可以直接标明当前标签的类型是什么;

从上可看出,其实VDOM就是用来描述咱们DOM结构的JavaScript对象,为什么需要这个虚拟 DOM 后面会详情说明。

实践

了解了JSX,React.createElement,接下来我们首先来实现下React.createElement这个接口,新建一个react.js文件, index.js让用导入我们新建的React

import React from 'react.js'; // 新建的react
import ReactDOM from 'react-dom'; 

// function Component
function Comp(props){
  return <h2>hi {props.name}</h2>
}

const jsx  = (
  <div id="demo" style={{color:"red",border:'1px solid blue'}}>
    <span>hi</span>
    <Comp name="函数组件"></Comp>
  </div>
);
console.log(jsx); // 输出看下具体结构
ReactDOM.render(jsx,document.querySelector('#root'))

如果不知道createElement传说明参数,我们可以输出下auguments了解下

从输出参数结果看,肯定有一个type参数来表示标签类型的,参数2表示元素属性的,参数3则是表示若干个子元素,另外我们通过上面 JSX 输出结果得知props属性下有个children属性,children不是独立的,而是要把收集到一个数组里去,然后单独放props里。

通过标签类型的处理增加vtype类型属性,用于在vdom转 DOM 时区分组件类型

react.js 代码如下:

/**
 *
 * createElement
 * @param {any} type 标签类型 或者组件类型,如div
 * @param {Element Attribute} props 标签属性
 * @param {something child Element} children  若干数量不等的子元素
 */
function createElement(type, props, ...children) {
  console.log('createElement', arguments);
  props.children = children;
  return { type, props};
}
export default { createElement };

运行会以下错误: 为什么报错,因为本身React本身非常健壮,接收参数的时候会进行检查,我们这里支持返回了{ type, props }两参数,React认为是不足的,比如之前我们看到refkey 等;所有我们这里只能自己创建个react-dom.js,用自己创建的render方法来实现渲染。 react-dom.js代码如下:

/**
 *  render渲染函数
 *
 * @param {Object} vnode 就是createElement创建的虚拟DOM
 * @param {Element} container  挂载容器
 */
function render(vnode, container) {
  container.innerHTML = `<pre>${JSON.stringify(vnode, null, 2)}</pre>`;
}

export default { render };

页面输出结果 紧接着考虑,有没有办法能把render接收到的vnode转化成真正的DOMNode,这里逻辑有点多,所有我新建了一个vdom.js文件来处理。

vdom.js 代码如下:

 /**
 * createVNode 创建虚拟节点,对createElement返回的vdom做些加工处理
 * @export
 * @param {Number} vtype 元素的类型,1:原生元素,2:function组件,3:class组件
 * @param {Object} type 标签元素类型
 * @param {Object} props  标签属性
 */
export function createVNode(vtype, type, props) {
  const vnode = { vtype, type, props };
  // console.log('vnode', vnode);
  return vnode;
}

这里看参数多加了一个vtype,为何要这么做呢?我这考虑到原生HTML原生,另外考虑自定义的function组件和Class类型组件,这里用vtype参数来判断,看楼上新建的react.js中的返回并没有vtype,所以这里需要通过type 特殊处理下:

function createElement(type, props, ...children) {
  props.children = children;
  delete props.__source; // 移除无用的属性
  delete props.__self;
  // type: 标签类型,如div
  // vtype :组件类型
  let vtype;
  if (typeof type === 'string') {
    // 原生标签
    vtype = 1;
  } else if (typeof type === 'function') {
    if (type.isClassComponent) {
      // 类组件
      vtype = 2;
    } else {
      // 函数组件
      vtype = 3;
    }
  }
  return createVNode(vtype, type, props); // vdom.js 
}

// 用来实现class组件的
export class Component {
  //用于区分组件是class还是function, 因为typeof对类和函数都返回funciton而无法区分类和函数
  static isClassComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  // 可以放心大胆的用setState
  setState() {}
}

接下来要做件重要的事,就是把VDOM转换成真实DOM,我们在vdom.js 增加initVNode方法并导出。函数内部分别对文本节点、元素标签、函数组件、类组件分别做了处理。 并且对属性和特殊属性做了处理。代码如下:

/**
 * vdom 转换为dom
 * 初始化虚拟节点
 * @export
 * @param {Object} vnode
 */
export function initVNode(vnode) {
  const { vtype } = vnode;
  if (!vtype) {
    // 文本节点
    return document.createTextNode(vnode);
  }
  if (vtype === 1) {
    // 原生标签
    return createElement(vnode);
  } else if (vtype === 2) {
    // 类组件
    return createClassComponent(vnode);
  } else if (vtype === 3) {
    // 函数组件
    return createFunComponent(vnode);
  }
}

/**
 * 创建原生元素标签
 * 函数组件和Class组件创建最终都会执行到 该原生....
 * @param {Object} vnode
 * @returns
 */
function createElement(vnode) {
  // 根据type创建元素
  const { type, props } = vnode;
  const node = document.createElement(type);

  // 处理属性,  原生自定义属性,特殊属性children
  const { key, children, ...rest } = props;
  Object.keys(rest).forEach((k) => {
    // 处理JSX里特殊属性名: className, htmlFor
    if (k === 'className') {
      node.setAttribute('class', rest[k]);
    } else if (k === 'htmlFor') {
      node.setAttribute('for', rest[k]);
    } else if (k === 'style' && typeof rest[k] === 'object') {
      // 内联 style用js写法的处理 ,这里就比较多了,这里就些了正常情况,如果font-size这样就不行
      const style = Object.keys(rest[k])
        .map((s) => `${s}:${rest[k][s]}`)
        .join(';');
      node.setAttribute('style', style);
    } else {
      node.setAttribute(k, rest[k]);
    }
  });
  // 递归子元素,// children父节点=> node
  children.forEach((c) => {
    // console.log('children',c)
      node.appendChild(initVNode(c));
  });
  return node;
}

/**
 * 创建Class组件
 *
 * @param {Object} vnode
 * @returns
 */
function createClassComponent(vnode) {
  //根据类组件看, type是class 组件声明
  const { type, props } = vnode;
  const component = new type(props);
  const vdom = component.render();
  return initVNode(vdom);
}
/**
 * 创建函数组件
 *
 * @param {Object} vnode
 * @returns
 */
function createFunComponent(vnode) {
  // type是函数
  const { type, props } = vnode;
  const vdom = type(props);
  return initVNode(vdom);
}

这里特别要注意下处理属性的时候有些特别的属性名,比如:对于 JSX 里classfor是保留字所以用classNamehtmlFor等;另外测试了style内联样式的简单处理。

JSX关于属性props

  • class属性需要写成classNamefor属性需要写成htmlFor,这是因为classforJavaScript的保留字。
  • 直接在标签上使用style属性时,要写成 style={{}} 是两个大括号,外层大括号是告知jsx这里是js语法,和真DOM不同的是,属性值不能是字符串而必须为对象,需要注意的是属性名同样需要驼峰命名法。即margin-top 要写成marginTop
  • this.props 下不要用children作为对象的属性名。因为this.props.children获取的该标签下的所有子标签。this.props.children的值有三种可能:
    • 如果当前组件没有子节点,它就是 undefined;
    • 如果有一个子节点,数据类型是 object
    • 如果有多个子节点,数据类型就是 array

所以,处理this.props.children的时候要小心。官方建议使用React.Children.map来遍历子节点,而不用担心数据类型引发的错误。

// class comp

class Comp2 extends Component {
  render() {
    return (
      <div>
        <h2>hi {this.props.name}</h2>
      </div>
    );
  }
}

// 测试 处理数组
const users = [
  { name: 'hank', age: 30 },
  { name: 'nimo', age: 7 },
];

// vdom
const jsx = (
  <div id="demo" style={{ color: 'red', border: '1px solid blue' }}>
    <span>hi</span>
    <Comp name="函数组件"></Comp>
    <Comp2 name="类组件"></Comp2>
    <ul>
      {users.map((user) => (
        <li key={user.name}>{user.name}</li>
      ))}
    </ul>
  </div>
);

运行结果:

为何li没有正常显示?,这种情况上面的createElement 里处理children 只针对单一虚拟 DOM 的处理,没考虑是多值的数组情况。下面是处理后的

  // 递归子元素,// children父节点=> node
  children.forEach(c => {
    console.log('children',c)
    // 如果子元素是个数组,改怎么处理 => 处理循环的
    if(Array.isArray(c)) {
      c.map(el => {
        node.appendChild(initVNode(el))
      })
    } else{
      node.appendChild(initVNode(c))
    }
  })

测试结果 OK

一个简单的粗略的实现,希望对你在学习React过程中有一定帮助,如任何问题和建议欢迎留言....

示例源码地址

参数考文献

React API