带你实现react源码的核心功能

  • React 的几种组件以及首次渲染实现
  • React 更新机制的实现以及 React diff 算法

React 的代码还是非常复杂的,虽然这里是一个简化版本。但是还是需要有不错的面向对象思维的。React 的核心主要有一下几点。

  • 虚拟 dom 对象(Virtual DOM)
  • 虚拟 dom 差异化算法(diff algorithm)
  • 单向数据流
  • 组件声明周期
  • 事件处理

本文代码仓库

  • 直接在游览器中打开 main.html 中查看效果
  • 更改代码请先执行执行npm i安装依赖(使用了部分 es6 代码)
  • 修改代码后请执行npm run dev重新编译代码

实现一个 hello React!的渲染

看如下代码:

// js
React.render('hello React!',document.getElementById("root"))

// html
<div id="root"></div>

// 生成代码
<div id="root">
    <span data-reactid="0">hello React!</span>
</div>

针对上面代码的具体实现

/** * component 类 * 文本类型 * @param {*} text 文本内容 */
function ReactDOMTextComponent(text) {
   
  // 存下当前的字符串
  this._currentElement = "" + text;
  // 用来标识当前component
  this._rootNodeID = null;
}

/** * component 类 装载方法,生成 dom 结构 * @param {number} rootID 元素id * @return {string} 返回dom */
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
   
  this._rootNodeID = rootID;
  return (
    '<span data-reactid="' + rootID + '">' + this._currentElement + "</span>"
  );
};

/** * 根据元素类型实例化一个具体的component * @param {*} node ReactElement * @return {*} 返回一个具体的component实例 */
function instantiateReactComponent(node) {
   
  //文本节点的情况
  if (typeof node === "string" || typeof node === "number") {
   
    return new ReactDOMTextComponent(node);
  }
}

const React = {
   
 nextReactRootIndex: 0,

 /**  * 接收一个React元素,和一个dom节点  * @param {*} element React元素  * @param {*} container 负责装载的dom  */
  render: function(element, container) {
   
    // 实例化组件
    var componentInstance = instantiateReactComponent(element);
    // 组件完成dom装载
    var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
    // 将装载好的 dom 放入 container 中
    $(container).html(markup);
    $(document).trigger("mountReady");
  }
};

这里代码分为三个部分:

  • 1 React.render 作为入口接受一个 React 元素和游览器中的 dom 负责调用渲染,nextReactRootIndex 为每个 component 的唯一标识
  • 2 引入 component 类的概念,ReactDOMTextComponent 是一个 component 类定义。ReactDOMTextComponent 针对于文本节点进行处理。并且在 ReactDOMTextComponent 的原型上实现了 mountComponent 方法,用于对组件的渲染,返回组件的 dom 结构。当然 component 还具有更新和删除操作,这里将在后续讲解。
  • 3 instantiateReactComponent 用来根据 element 的类型(现在只有一种 string 类型),返回一个 component 的实例。其实就是个类工厂。

在这里我们把逻辑分为几个部分,渲染逻辑则由 component 内部定义,React.render 负责调度整个流程,在调用 instantiateReactComponent 生成一个对应 component 类型的实例对象,再调用对象的 mountComponent 返回 dom,最后再写到 container 节点中

相关参考视频讲解:进入学习

虚拟 dom

虚拟 dom 无疑是 React 的核心概念,在代码中我们会使用 React.createElement 来创建一个虚拟 dom 元素。

虚拟 dom 分为两种一种是游览器自带的基本元素比如 div,还有一种是自定义元素(文本节点不算虚拟 dom)

虚拟节点的使用方式

// 绑定事件监听方法
function sayHello(){
   
    alert('hello!')
}
var element = React.createElement('div',{
   id:'jason',onclick:hello},'click me')
React.render(element,document.getElementById("root"))

// 最终生成的html

<div data-reactid="0" id="jason">
    <span data-reactid="0.0">click me</span>
</div>

我们使用 React.createElement 来创建一个虚拟 dom 元素,以下是简易实现

/**
 * ReactElement 就是虚拟节点的概念
 * @param {*} key 虚拟节点的唯一标识,后期可以进行优化
 * @param {*} type 虚拟节点类型,type可能是字符串('div', 'span'),也可能是一个function,function时为一个自定义组件
 * @param {*} props 虚拟节点的属性
 */
function ReactElement(type, key, props) {
   
  this.type = type;
  this.key = key;
  this.props = props;
}

const React = {
   
  nextReactRootIndex: 0,
  /**
   * @param {*} type 元素的 component 类型
   * @param {*} config 元素配置
   * @param {*} children 元素的子元素
   */
  createElement: function(type, config, children) {
   
    var props = {
   };
    var propName;
    config = config || {
   };

    var key = config.key || null;

    for (propName in config) {
   
      if (config.hasOwnProperty(propName) && propName !== "key") {
   
        props[propName] = config[propName];
      }
    }

    var childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
   
      props.children = Array.isArray(children) ? children : [children];
    } else if (childrenLength > 1) {
   
      var childArray = [];
      for (var i = 0; i < childrenLength; i++) {
   
        childArray[i] = arguments[i + 2];
      }
      props.children = childArray;
    }
    return new ReactElement(type, key, props);
  },

  /**
   * 自行添加上文中的render方法
   */
};

createElement 方法对传入的参数做了一些处理,最终会返回一个 ReactElement 虚拟元素实例,key 的定义可以提高更新时的效率

有了虚拟元素实例,我们需要改造一下 instantiateReactComponent 方法

/** * 根据元素类型实例化一个具体的component * @param {*} node ReactElement * @return {*} 返回一个具体的component实例 */
function instantiateReactComponent(node) {
   
  //文本节点的情况
  if (typeof node === "string" || typeof node === "number") {
   
    return new ReactDOMTextComponent(node);
  }
  //浏览器默认节点的情况
  if (typeof node === "object" && typeof node.type === "string") {
   
    //注意这里,使用了一种新的component
    return new ReactDOMComponent(node);
  }
}

我们增加了一个判断,这样当 render 的不是文本而是浏览器的基本元素时。我们使用另外一种 component 来处理它渲染时应该返回的内容。这里就体现了工厂方法 instantiateReactComponent 的好处了,不管来了什么类型的 node,都可以负责生产出一个负责渲染的 component 实例。这样 render 完全不需要做任何修改,只需要再做一种对应的 component 类型(这里是 ReactDOMComponent)就行了。

ReactDOMComponent的具体实现

/** * component 类 * react 基础标签类型,类似与html中的('div','span' 等) * @param {*} element 基础元素 */
function ReactDOMComponent(element) {
   
  // 存下当前的element对象引用
  this._currentElement = element;
  this._rootNodeID = null;
}

/** * component 类 装载方法 * @param {*} rootID 元素id * @param {string} 返回dom */
ReactDOMComponent.prototype.mountComponent = function(rootID) {
   
  this._rootNodeID = rootID;
  var props = this._currentElement.props;

  // 外层标签
  var tagOpen = "<" + this._currentElement.type;
  var tagClose = "</" + this._currentElement.type + ">";

  // 加上reactid标识
  tagOpen += " data-reactid=" + this._rootNodeID;

  // 拼接标签属性
  for (var propKey in props) {
   
    // 属性为绑定事件
    if (/^on[A-Za-z]/.test(propKey)) {
   
      var eventType = propKey.replace("on", "");
      // 对当前节点添加事件代理
      $(document).delegate(
        '[data-reactid=
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值