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

时间:2020-9-9 作者:admin

介绍

官网上这么解释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代码后进行了优化。
  • 它是类型安全,可防止注入攻击,在编译过程中就能发现错误。
  • 使用JSX编写模块更加简单快速。
  1. 怎么用JSX
    4.原理:babel-loader会预编译JSX为React.creaetElement(…)

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

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

大家知道,jsx最终就是普通的js对象,我在这直接声明一个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 核心API

错误提示说根本没有声明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输出是怎么样一个数据结构:

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

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

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

从上可看出,其实VDOM就是用来描述咱们DOM结构的JS对象,为什么需要这个虚拟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'))

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

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

从输出参数结果看,肯定有一个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系列:一个简化版react 核心API

为什么报错,因为本身React本身非常健壮,接收参数的时候会进行检查,我们这里支付返回了{ type, props}两参数, React认为是不足的,比如之前我们看到ref、key等;所有我们这里只能自己创建个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 };

页面输出结果

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

紧接着考虑,有没有办法能把render接受vnode转化成真正的node (DOM),这块逻辑有点多,所有我新建了一个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原生,另外考虑自定义的funtion组件和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里class和for是保留字所以用className、htmlFor等;另外测试了style内联样式的简单处理。

JSX关于属性props:

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

所以,处理 this.props.children 的时候要小心。官方4建议使用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>
);

运行结果:

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

为何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

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

示例源码地址

参数考文献

React API

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。