手写一个react系列(一):实现react和react-dom

815 阅读4分钟

开篇

如今前端开发框架里,React已经是前端开发中最受欢迎的框架之一,对于前端的重要性不言而喻。从今天开始我将用几个篇幅来实现一个简单的React框架, 带大家一起来了解React的基本机制。

React

用过React的童鞋们应该都知道React的 JSX 语法,其实 JSX 的写法就类似于HTML标签,经过解析后,实际上执行的是React.createElement,如下:

// jsx语法
function render() {
  return <div>hello react</div>;
}

// 经过解析后,实际上执行的是
function render() {
  return React.createElement((type: 'div'), {}, 'hello react'); // 返回一个js对象:{type: 'div',attrs: {},children: ['hello react']}
}

因此我们可以开始模拟第一个函数,React.createElement

// 创建一个React对象,后面导出
const React = {};

/*
 * createElement方法
 * 3个参数type,attrs,children(多个,所以会返回数组)
 *
 */
React.createElement = function (type, attrs, ...children) {
  return {
    type,
    attrs,
    children,
  };
};

export default React;

ReactDOM

以上就是React.createElement的基本核心代码,接下来我们来实现另外一个对象 --- ReactDOM,细节我就不多说了,就是来实现创建dom节点的方法,直接看代码:

// 创建一个ReactDOM对象,后面导出
const ReactDOM = {};

/*
 * render方法
 * 2个参数:vnode 就是createElement返回的对象;container就是dom节点,如:document.getELementById('app')
 *
 */
ReactDOM.render = function (vnode, container) {};

export default ReactDOM;

定义好了render方法后,我们来具体分析下他的参数,vnode 和 container,我们说了vnode实际上就是React.createElement返回的对象,containerdom节点,我们来修改一下ReactDOM.render方法:

// 创建一个ReactDOM对象,后面导出
const ReactDOM = {};

/*
 * render方法
 * 2个参数:vnode 就是createElement返回的对象;container就是dom节点,如:document.getELementById('app')
 *
 */
ReactDOM.render = function (vnode, container) {
  let element;
  // 判断传入的vnode是不是string,如果是直接创建文本节点
  if (typeof vnode === 'string') {
    element = document.createTextNode(vnode);
  } else {
    // 如果是vnode对象,则直接创建html节点
    element = document.createElement(vnode.type);
  }

  // 将节点元素插入html中
  container.appendChild(element);
};

export default ReactDOM;

至此一个最基本的 React 库就可以执行了,我们可以先创建一个index.html来测试下: index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>

  <!-- react.js 包含React和ReactDOM两个对象 -->
  <script src="./react.js"></script>
  <!-- index.js 引入并执行我们的React方法 -->
  <script src="./index.js"></script>
</html>

我们来看下index.js文件: index.js

// JSX会被解析并执行React.createElement,返回一个vnode对象
const element = <div>a single react</div>;
// element实际上执行的是
// const element = React.createElement(type: 'div', {}, ['a single react'])

ReactDOM.render(element, document.getElementById('root'));

html文件在浏览器上运行下就可以看到 a single react 字样,说明React运行成功。我们再改动一下index.js

// JSX会被解析并执行React.createElement,返回一个vnode对象
// 多嵌套一层span
const element = (
  <div>
    <span>a single react</span>
  </div>
);
// element实际上执行的是
// const element = React.createElement(type: 'div', {}, [React.createElement(type: 'span',{}, ['a single react'])])

ReactDOM.render(element, document.getElementById('root'));

递归

再刷新一下浏览器,我们会发现并没有任何的内容,这是为什么呢?看到index.js应该知道,我们给原来的 a single react 多套了一层span,所以我们应该把ReactDOM.render再改动下, 改为递归的方式执行,就可以解决掉这个问题:

// 创建一个ReactDOM对象,后面导出
const ReactDOM = {};

/*
 * render方法
 * 2个参数:vnode 就是createElement返回的对象;container就是dom节点,如:document.getELementById('app')
 *
 */
ReactDOM.render = function (vnode, container) {
  // 做一下容错判断,如果没有传入container则退出
  if (!container) {
    return;
  }

  let element;
  // 判断传入的vnode是不是string,如果是直接创建文本节点
  if (typeof vnode === 'string') {
    element = document.createTextNode(vnode);
  } else {
    // 如果是vnode对象,则直接创建html节点
    element = document.createElement(vnode.type);
  }

  // 将节点元素插入html中
  container.appendChild(element);

  /*
   * 判断是否有children
   * 有的话,遍历children并递归执行下ReactDOM.render
   *
   */
  if (vnode.children && vnode.children.length) {
    vnode.children.forEach((child) => {
      /*
       * 传入每个child
       * 同时传入当前element作为dom节点
       *
       */
      ReactDOM.render(child, element);
    });
  }
};

export default ReactDOM;

总结

此时,我们再刷新一下浏览器,就会发现运行正常显示了 a single react ,一个简单版的React第一步就算实现了。下一篇,我会带大家来实现React属性功能,有错误之处欢迎在评论中指出。