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

news2024/11/17 1:43:49
  • 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="' + this._rootNodeID + '"]',
        eventType + "." + this._rootNodeID,
        props[propKey]
      );
    }

    // 对于props 上的children和事件属性不做处理
    if (
      props[propKey] &&
      propKey != "children" &&
      !/^on[A-Za-z]/.test(propKey)
    ) {
      tagOpen += " " + propKey + "=" + props[propKey];
    }
  }
  // 渲染子节点dom
  var content = "";
  var children = props.children || [];

  var childrenInstances = []; // 保存子节点component 实例
  var that = this;

  children.forEach((child, key) => {
    var childComponentInstance = instantiateReactComponent(child);
    // 为子节点添加标记
    childComponentInstance._mountIndex = key;
    childrenInstances.push(childComponentInstance);
    var curRootId = that._rootNodeID + "." + key;

    // 得到子节点的渲染内容
    var childMarkup = childComponentInstance.mountComponent(curRootId);

    // 拼接在一起
    content += " " + childMarkup;
  });

  // 保存component 实例
  this._renderedChildren = childrenInstances;

  // 拼出整个html内容
  return tagOpen + ">" + content + tagClose;
};

对于虚拟 dom 的渲染逻辑,本质上还是个递归渲染的东西,reactElement 会递归渲染自己的子节点。可以看到我们通过 instantiateReactComponent 屏蔽了子节点的差异,只需要使用不同的 component 类,这样都能保证通过 mountComponent 最终拿到渲染后的内容。

另外这边的事件也要说下,可以在传递 props 的时候传入{onClick:function(){}}这样的参数,这样就会在当前元素上添加事件,代理到 document。由于 React 本身全是在写 js,所以监听的函数的传递变得特别简单。

这里很多东西没有考虑,这里为了保持简单就不再扩展了,另外 React 的事件处理其实很复杂,实现了一套标准的 w3c 事件。这里偷懒直接使用 jQuery 的事件代理到 document 上了。

自定义元素的实现
随着前端技术的发展浏览器的那些基本元素已经满足不了我们的需求了,如果你对 web components 有一定的了解,就会知道人们一直在尝试扩展一些自己的标记。

React 通过虚拟 dom 做到了类似的功能,还记得我们上面 node.type 只是个简单的字符串,如果是个类呢?如果这个类恰好还有自己的生命周期管理,那扩展性就很高了。

在 React 中使用自定义元素

var CompositeComponent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  componentWillMount: function() {
    console.log("声明周期: " + "componentWillMount");
  },
  componentDidMount: function() {
    console.log("声明周期: " + "componentDidMount");
  },
  onChange: function(e) {
    var count = ++this.state.count;
    this.setState({
      count: count
    });
  },
  render: function() {
    const count = this.state.count;
    var h3 = React.createElement(
      "h3",
      { onclick: this.onChange.bind(this), class: "h3" },
      `click me ${count}`
    );
    var children = [h3];

    return React.createElement("div", null, children);
  }
});

var CompositeElement = React.createElement(CompositeComponent);

var root = document.getElementById("container");

React.render(CompositeElement, root);

React.createElement接受的不再是字符串,而是一个 class。
React.createClass 生成一个自定义标记类,带有基本的生命周期:

  • getInitialState 获取最初的属性值 this.state
  • componentWillmount 在组件准备渲染时调用
  • componentDidMount 在组件渲染完成后调用

React.createClass 的实现

/** * 所有自定义组件的超类 * @function render所有自定义组件都有该方法 */
function ReactClass() {}

ReactClass.prototype.render = function() {};

/** * 更新 * @param {*} newState 新状态 */
ReactClass.prototype.setState = function(newState) {
  // 拿到ReactCompositeComponent的实例
  this._reactInternalInstance.receiveComponent(null, newState);
};

const React = {
  nextReactRootIndex: 0,

  /**   * 创建 ReactClass   * @param {*} spec 传入的对象   */
  createClass: function(spec) {
    var Constructor = function(props) {
      this.props = props;
      this.state = this.getInitialState ? this.getInitialState() : null;
    };

    Constructor.prototype = new ReactClass();
    Constructor.prototype.constructor = Constructor;

    Object.assign(Constructor.prototype, spec);
    return Constructor;
  },

  /**   * 自己上文的createElement方法   */

  /**   * 自己上文的render方法   */
};

这里 createClass 生成了一个继承 ReactClass 的子类,在构造函数里调用 this.getInitialState 获得最初的 state。

为了演示方便,我们这边的 ReactClass 相当简单,实际上原始的代码处理了很多东西,比如类的 mixin 的组合继承支持,比如 componentDidMount 等可以定义多次,需要合并调用等等,有兴趣的去翻源码吧,不是本文的主要目的,这里就不详细展开了。

看看我们上面的两种类型就知道,我们是时候为自定义元素也提供一个 component 类了,在那个类里我们会实例化 ReactClass,并且管理生命周期,还有父子组件依赖。

首先改造 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);
  }
  // 自定义的元素节点
  if (typeof node === "object" && typeof node.type === "function") {
    // 注意这里,使用新的component,专门针对自定义元素
    return new ReactCompositeComponent(node);
  }
}

这里我们添加了一个判断,处理自定义类型的 component

ReactCompositeComponent 的具体实现如下

/** * component 类 * 复合组件类型 * @param {*} element 元素 */
function ReactCompositeComponent(element) {
  // 存放元素element对象
  this._currentElement = element;
  // 存放唯一标识
  this._rootNodeID = null;
  // 存放对应的ReactClass的实例
  this._instance = null;
}

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

  // 当前元素属性
  var publicProps = this._currentElement.props;
  // 对应的ReactClass
  var ReactClass = this._currentElement.type;

  var inst = new ReactClass(publicProps);
  this._instance = inst;

  // 保留对当前 component的引用
  inst._reactInternalInstance = this;

  if (inst.componentWillMount) {
    // 生命周期
    inst.componentWillMount();
    //这里在原始的 reactjs 其实还有一层处理,就是  componentWillMount 调用 setstate,不会触发 rerender 而是自动提前合并,这里为了保持简单,就略去了
  }

  // 调用 ReactClass 实例的render 方法,返回一个element或者文本节点
  var renderedElement = this._instance.render();
  var renderedComponentInstance = instantiateReactComponent(renderedElement);
  this._renderedComponent = renderedComponentInstance; //存起来留作后用

  var renderedMarkup = renderedComponentInstance.mountComponent(
    this._rootNodeID
  );

  // dom 装载到html 后调用生命周期
  $(document).on("mountReady", function() {
    inst.componentDidMount && inst.componentDidMount();
  });

  return renderedMarkup;
};

自定义元素本身不负责具体的内容,他更多的是负责生命周期。具体的内容是由它的 render 方法返回的虚拟节点来负责渲染的。

本质上也是递归的去渲染内容的过程。同时因为这种递归的特性,父组件的 componentWillMount 一定在某个子组件的 componentWillMount 之前调用,而父组件的 componentDidMount 肯定在子组件之后,因为监听 mountReady 事件,肯定是子组件先监听的。

需要注意的是自定义元素并不会处理我们 createElement 时传入的子节点,它只会处理自己 render 返回的节点作为自己的子节点。不过我们在 render 时可以使用 this.props.children 拿到那些传入的子节点,可以自己处理。其实有点类似 web components 里面的 shadow dom 的作用。

初始化渲染的大致流程如下:

实现一个简单的更新机制

一般在 React 中我们需要更新时都是调用的 setState 方法。所以本文的更新就基于 setState 实现。看下面的调用方式:

/** * ReactCompositeComponent组件 */
var CompositeComponent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  componentWillMount: function() {
    console.log("声明周期: " + "componentWillMount");
  },
  componentDidMount: function() {
    console.log("声明周期: " + "componentDidMount");
  },
  onChange: function(e) {
    var count = ++this.state.count;
    this.setState({
      count: count
    });
  },
  render: function() {
    const count = this.state.count;
    var h3 = React.createElement(
      "h3",
      { onclick: this.onChange.bind(this), class: "h3" },
      `click me ${count}`
    );
    var children = [h3];

    return React.createElement("div", null, children);
  }
});
var CompositeElement = React.createElement(CompositeComponent);
var root = document.getElementById("root");

React.render(CompositeElement, root);

// 生成html
<div id="root">
  <div data-reactid="0">
    <h3 data-reactid="0.0" class="h3">
      <span data-reactid="0.0.0">click me 0</span>
    </h3>
  </div>
</div>

// 点击click me 计数会递增

点击文字就会调用 setState 走更新流程,我们回顾一下 ReactClass,看一下 setState 的实现

/** * 更新 * @param {*} newState 新状态 */
ReactClass.prototype.setState = function(newState) {
  // 拿到ReactCompositeComponent的实例
  // 在装载的时候保存
  // 代码:this._reactInternalInstance = this
  this._reactInternalInstance.receiveComponent(null, newState);
};

可以看到 setState 主要调用了对应的 component 的 receiveComponent 来实现更新。所有的挂载,更新都应该交给对应的 component 来管理。所以就像所有的 component 都实现了 mountComponent 来处理第一次渲染,所有的 component 类都应该实现 receiveComponent 用来处理自己的更新。

文本节点的 receiveComponent

文本节点的更新比较简单,拿到新的文本进行比较,不同则直接替换整个节点

/** * component 类 更新 * @param {*} newText
 */
ReactDOMTextComponent.prototype.receiveComponent = function(nextText) {
  var nextStringText = "" + nextText;
  // 跟以前保存的字符串比较
  if (nextStringText !== this._currentElement) {
    this._currentElement = nextStringText;
    // 替换整个节点
    $('[data-reactid="' + this._rootNodeID + '"]').html(this._currentElement);
  }
};

自定义元素的 receiveComponent

先来看自定义元素的 receiveComponent 的实现

/** * component 类 更新 * @param {*} nextElement * @param {*} newState */
ReactCompositeComponent.prototype.receiveComponent = function(
  nextElement,
  newState
) {
  // 如果接受了新的element,则直接使用最新的element
  this._currentElement = nextElement || this._currentElement;

  var inst = this._instance;
  // 合并state
  var nextState = Object.assign(inst.state, newState);
  var nextProps = this._currentElement.props;

  // 更新state
  inst.state = nextState;

  // 生命周期方法
  if (
    inst.shouldComponentUpdate &&
    inst.shouldComponentUpdate(nextProps, nextState) === false
  ) {
    // 如果实例的 shouldComponentUpdate 返回 false,则不需要继续往下执行更新
    return;
  }

  // 生命周期方法
  if (inst.componentWillUpdate) inst.componentWillUpdate(nextProps, nextState);

  // 获取老的element
  var prevComponentInstance = this._renderedComponent;
  var prevRenderedElement = prevComponentInstance._currentElement;

  // 通过重新render 获取新的element
  var nextRenderedElement = this._instance.render();

  // 比较新旧元素
  if (_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
    // 两种元素为相同,需要更新,执行字节点更新
    prevComponentInstance.receiveComponent(nextRenderedElement);
    // 生命周期方法
    inst.componentDidUpdate && inst.componentDidUpdate();
  } else {
    // 两种元素的类型不同,直接重新装载dom
    var thisID = this._rootNodeID;

    this._renderedComponent = this._instantiateReactComponent(
      nextRenderedElement
    );

    var nextMarkup = _renderedComponent.mountComponent(thisID);
    // 替换整个节点
    $('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup);
  }
};

/** * 通过比较两个元素,判断是否需要更新 * @param {*} preElement  旧的元素 * @param {*} nextElement 新的元素 * @return {boolean} */
function _shouldUpdateReactComponent(prevElement, nextElement) {
  if (prevElement != null && nextElement != null) {
    var prevType = typeof prevElement;
    var nextType = typeof nextElement;
    if (prevType === "string" || prevType === "number") {
      // 文本节点比较是否为相同类型节点
      return nextType === "string" || nextType === "number";
    } else {
      // 通过type 和 key 判断是否为同类型节点和同一个节点
      return (
        nextType === "object" &&
        prevElement.type === nextElement.type &&
        prevElement.key === nextElement.key
      );
    }
  }
  return false;
}

上述代码的大致流程是:

  • 合并 state
  • 更新 state
  • 然后看业务代码中是否实现生命周期方法 shouldComponentUpdate 有则调用,如果返回值为 false 则停止往下执行
  • 然后是生命周期方法 componentWillUpdate
  • 然后通过拿到新 state 的 instance 调用 render 方法拿到新的 element 和之旧的 element 进行比较
  • 如果要更新就继续调用对应的 component 类对应的 receiveComponent 就好啦,其实就是直接当甩手掌柜,事情直接丢给手下去办了。当然还有种情况是,两次生成的 element 差别太大,就不是一个类型的,那好办直接重新生成一份新的代码重新渲染一次就 o 了

_shouldUpdateReactComponent 是一个全局方法,这个是一种 React 的优化机制。用来决定是直接全部替换,还是使用很细微的改动。当两次 render 出来的子节点 key 不同,直接全部重新渲染一遍,替换就好了。否则,我们就得来个递归的更新,保证最小化的更新机制,这样可以不会有太大的闪烁。

在这里本质上还是递归调用 receiveComponent 的过程。

基本元素的 receiveComponent

基础元素的更新包括两方面

  • 属性的更新,包括对特殊属性比如事件的处理
  • 子节点的更新

子节点的更新比较复杂,是提升效率的关键,所以需要处理以下问题:

  • diff - 拿新的子节点树跟以前老的子节点树对比,找出他们之间的差别。
  • patch - 所有差别找出后,再一次性的去更新。

下面是基础元素更新的基本结构

/**
 * component 类 更新
 * @param {*} nextElement
 */
ReactDOMComponent.prototype.receiveComponent = function(nextElement) {
  var lastProps = this._currentElement.props;
  var nextProps = nextElement.props;
  this._currentElement = nextElement;
  // 处理当前节点的属性
  this._updateDOMProperties(lastProps, nextProps);
  // 处理当前节点的子节点变动
  this._updateDOMChildren(nextElement.props.children);
};

先看看,更新属性怎么变更:

/** * 更新属性 * @param {*} lastProps
 * @param {*} nextProps
 */
ReactDOMComponent.prototype._updateDOMProperties = function(
  lastProps,  nextProps
) {
  // 当老属性不在新属性的集合里时,需要删除属性
  var propKey;
  for (propKey in lastProps) {
    if (
      nextProps.hasOwnProperty(propKey) ||
      !lastProps.hasOwnProperty(propKey)
    ) {
      // 新属性中有,且不再老属性的原型中
      continue;
    }
    if (/^on[A-Za-z]/.test(propKey)) {
      var eventType = propKey.replace("on", "");
      // 特殊事件,需要去掉事件监听
      $(document).undelegate(
        '[data-reactid="' + this._rootNodeID + '"]',
        eventType,
        lastProps[propKey]
      );
      continue;
    }

    // 删除不需要的属性
    $('[data-reactid="' + this._rootNodeID + '"]').removeAttr(propKey);
  }

  // 对于新的事件,需要写到dom上
  for (propKey in nextProps) {
    if (/^on[A-Za-z]/.test(propKey)) {
      var eventType = propKey.replace("on", "");
      // 删除老的事件绑定
      lastProps[propKey] &&
        $(document).undelegate(
          '[data-reactid="' + this._rootNodeID + '"]',
          eventType,
          lastProps[propKey]
        );
      // 针对当前的节点添加事件代理,以_rootNodeID为命名空间
      $(document).delegate(
        '[data-reactid="' + this._rootNodeID + '"]',
        eventType + "." + this._rootNodeID,
        nextProps[propKey]
      );
      continue;
    }

    if (propKey == "children") continue;

    // 添加新的属性,重写同名属性
    $('[data-reactid="' + this._rootNodeID + '"]').prop(
      propKey,
      nextProps[propKey]
    );
  }
};

属性的变更并不是特别复杂,主要就是找到以前老的不用的属性直接去掉,新的属性赋值,并且注意其中特殊的事件属性做出特殊处理就行了。

子节点更新,也是最复杂的部分:

// 全局的更新深度标识
var updateDepth = 0;
// 全局的更新队列,所有的差异都存在这里
var diffQueue = [];

ReactDOMComponent.prototype._updateDOMChildren = function(
  nextChildrenElements
) {
  updateDepth++;
  // _diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
  this._diff(diffQueue, nextChildrenElements);
  updateDepth--;
  if (updateDepth == 0) {
    // 在需要的时候调用patch,执行具体的dom操作
    this._patch(diffQueue);
    diffQueue = [];
  }
};

就像我们之前说的一样,更新子节点包含两个部分,一个是递归的分析差异,把差异添加到队列中。然后在合适的时机调用_patch 把差异应用到 dom 上。那么什么是合适的时机,updateDepth 又是干嘛的?这里需要注意的是,_diff 内部也会递归调用子节点的 receiveComponent 于是当某个子节点也是浏览器普通节点,就也会走_updateDOMChildren 这一步。所以这里使用了 updateDepth 来记录递归的过程,只有等递归回来 updateDepth 为 0 时,代表整个差异已经分析完毕,可以开始使用 patch 来处理差异队列了。

diff 实现

// 差异更新的几种类型
var UPDATE_TYPES = {
  MOVE_EXISTING: 1,
  REMOVE_NODE: 2,
  INSERT_MARKUP: 3
};

/**
 * 生成子节点 elements 的 component 集合
 * @param {object} prevChildren 前一个 component 集合
 * @param {Array} nextChildrenElements 新传入的子节点element数组
 * @return {object} 返回一个映射
 */
function generateComponentChildren(prevChildren, nextChildrenElements) {
  var nextChildren = {};
  nextChildrenElements = nextChildrenElements || [];
  $.each(nextChildrenElements, function(index, element) {
    var name = element.key ? element.key : index;
    var prevChild = prevChildren && prevChildren[name];
    var prevElement = prevChild && prevChild._currentElement;
    var nextElement = element;

    // 调用_shouldUpdateReactComponent判断是否是更新
    if (_shouldUpdateReactComponent(prevElement, nextElement)) {
      // 更新的话直接递归调用子节点的receiveComponent就好了
      prevChild.receiveComponent(nextElement);
      // 然后继续使用老的component
      nextChildren[name] = prevChild;
    } else {
      // 对于没有老的,那就重新新增一个,重新生成一个component
      var nextChildInstance = instantiateReactComponent(nextElement, null);
      // 使用新的component
      nextChildren[name] = nextChildInstance;
    }
  });

  return nextChildren;
}

/**
 * 将数组转换为映射
 * @param {Array} componentChildren
 * @return {object} 返回一个映射
 */
function flattenChildren(componentChildren) {
  var child;
  var name;
  var childrenMap = {};
  for (var i = 0; i < componentChildren.length; i++) {
    child = componentChildren[i];
    name =
      child && child._currentelement && child._currentelement.key
        ? child._currentelement.key
        : i.toString(36);
    childrenMap[name] = child;
  }
  return childrenMap;
}

/**
 * _diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
 * @param {*} diffQueue
 * @param {*} nextChildrenElements
 */
ReactDOMComponent.prototype._diff = function(diffQueue, nextChildrenElements) {
  var self = this;
  // 拿到之前的子节点的 component类型对象的集合,这个是在刚开始渲染时赋值的,记不得的可以翻上面
  // _renderedChildren 本来是数组,我们搞成map
  var prevChildren = flattenChildren(self._renderedChildren);
  // 生成新的子节点的component对象集合,这里注意,会复用老的component对象
  var nextChildren = generateComponentChildren(
    prevChildren,
    nextChildrenElements
  );
  // 重新赋值_renderedChildren,使用最新的。
  self._renderedChildren = [];
  $.each(nextChildren, function(key, instance) {
    self._renderedChildren.push(instance);
  });

  /**注意新增代码**/
  var lastIndex = 0; // 代表访问的最后一次的老的集合的位置

  var nextIndex = 0; // 代表到达的新的节点的index
  // 通过对比两个集合的差异,组装差异节点添加到队列中
  for (name in nextChildren) {
    if (!nextChildren.hasOwnProperty(name)) {
      continue;
    }
    var prevChild = prevChildren && prevChildren[name];
    var nextChild = nextChildren[name];
    // 相同的话,说明是使用的同一个component,所以我们需要做移动的操作
    if (prevChild === nextChild) {
      // 添加差异对象,类型:MOVE_EXISTING
      /**注意新增代码**/
      prevChild._mountIndex < lastIndex &&
        diffQueue.push({
          parentId: self._rootNodeID,
          parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
          type: UPDATE_TYPES.MOVE_EXISTING,
          fromIndex: prevChild._mountIndex,
          toIndex: nextIndex
        });
      /**注意新增代码**/
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);
    } else {
      // 如果不相同,说明是新增加的节点
      // 但是如果老的还存在,就是element不同,但是component一样。我们需要把它对应的老的element删除。
      if (prevChild) {
        // 添加差异对象,类型:REMOVE_NODE
        diffQueue.push({
          parentId: self._rootNodeID,
          parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
          type: UPDATE_TYPES.REMOVE_NODE,
          fromIndex: prevChild._mountIndex,
          toIndex: null
        });

        // 如果以前已经渲染过了,记得先去掉以前所有的事件监听,通过命名空间全部清空
        if (prevChild._rootNodeID) {
          $(document).undelegate("." + prevChild._rootNodeID);
        }

        /**注意新增代码**/
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
      }
      // 新增加的节点,也组装差异对象放到队列里
      // 添加差异对象,类型:INSERT_MARKUP
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
        type: UPDATE_TYPES.INSERT_MARKUP,
        fromIndex: null,
        toIndex: nextIndex,
        markup: nextChild.mountComponent(self._rootNodeID + "." + name) //新增的节点,多一个此属性,表示新节点的dom内容
      });
    }
    // 更新mount的index
    nextChild._mountIndex = nextIndex;
    nextIndex++;
  }

  // 对于老的节点里有,新的节点里没有的那些,也全都删除掉
  for (name in prevChildren) {
    if (
      prevChildren.hasOwnProperty(name) &&
      !(nextChildren && nextChildren.hasOwnProperty(name))
    ) {
      // 添加差异对象,类型:REMOVE_NODE
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
        type: UPDATE_TYPES.REMOVE_NODE,
        fromIndex: prevChildren[name]._mountIndex,
        toIndex: null
      });
      // 如果以前已经渲染过了,记得先去掉以前所有的事件监听
      if (prevChildren[name]._rootNodeID) {
        $(document).undelegate("." + prevChildren[name]._rootNodeID);
      }
    }
  }
};

注意 flattenChildren 我们这里把数组集合转成了对象 map,以 element 的 key 作为标识,当然对于 text 文本或者没有传入 key 的 element,直接用 index 作为标识。通过这些标识,我们可以从类型的角度来判断两个 component 是否是一样的。

generateComponentChildren 会尽量的复用以前的 component,也就是那些坑,当发现可以复用 component(也就是 key 一致)时,就还用以前的,只需要调用他对应的更新方法 receiveComponent 就行了,这样就会递归的去获取子节点的差异对象然后放到队列了。如果发现不能复用那就是新的节点,我们就需要 instantiateReactComponent 重新生成一个新的 component。

lastIndex,这个代表最后一次访问的老集合节点的最大的位置。
而我们加了个判断,只有_mountIndex 小于这个 lastIndex 的才会需要加入差异队列。有了这个判断上面的例子 2 就不需要 move。而程序也可以好好的运行,实际上大部分都是 2 这种情况。

这是一种顺序优化,lastIndex 一直在更新,代表了当前访问的最右的老的集合的元素。
我们假设上一个元素是 A,添加后更新了 lastIndex。
如果我们这时候来个新元素 B,比 lastIndex 还大说明当前元素在老的集合里面就比上一个 A 靠后。所以这个元素就算不加入差异队列,也不会影响到其他人,不会影响到后面的 path 插入节点。因为我们从 patch 里面知道,新的集合都是按顺序从头开始插入元素的,只有当新元素比 lastIndex 小时才需要变更。其实只要仔细推敲下上面那个例子,就可以理解这种优化手段了。
查看React diff 策略

_patch 的实现

/**
 *
 * @param {*} parentNode
 * @param {*} childNode
 * @param {*} index
 */ function insertChildAt(parentNode, childNode, index) {
  var beforeChild = parentNode.children().get(index);
  beforeChild
    ? childNode.insertBefore(beforeChild)
    : childNode.appendTo(parentNode);
}

/**
 *
 * @param {*} diffQueue
 */
ReactDOMComponent.prototype._patch = function(diffQueue) {
  var update;
  var initialChildren = {};
  var deleteChildren = [];
  for (var i = 0; i < updates.length; i++) {
    update = updates[i];
    if (
      update.type === UPDATE_TYPES.MOVE_EXISTING ||
      update.type === UPDATE_TYPES.REMOVE_NODE
    ) {
      var updatedIndex = update.fromIndex;
      var updatedChild = $(update.parentNode.children().get(updatedIndex));
      var parentID = update.parentID;

      // 所有需要更新的节点都保存下来,方便后面使用
      initialChildren[parentID] = initialChildren[parentID] || [];
      // 使用parentID作为简易命名空间
      initialChildren[parentID][updatedIndex] = updatedChild;

      // 所有需要修改的节点先删除,对于move的,后面再重新插入到正确的位置即可
      deleteChildren.push(updatedChild);
    }
  }

  // 删除所有需要先删除的
  $.each(deleteChildren, function(index, child) {
    $(child).remove();
  });

  // 再遍历一次,这次处理新增的节点,还有修改的节点这里也要重新插入
  for (var k = 0; k < updates.length; k++) {
    update = updates[k];
    switch (update.type) {
      case UPDATE_TYPES.INSERT_MARKUP:
        insertChildAt(update.parentNode, $(update.markup), update.toIndex);
        break;
      case UPDATE_TYPES.MOVE_EXISTING:
        insertChildAt(
          update.parentNode,
          initialChildren[update.parentID][update.fromIndex],
          update.toIndex
        );
        break;
      case UPDATE_TYPES.REMOVE_NODE:
        // 什么都不需要做,因为上面已经帮忙删除掉了
        break;
    }
  }
};

_patch 主要就是挨个遍历差异队列,遍历两次,第一次删除掉所有需要变动的节点,然后第二次插入新的节点还有修改的节点。这里为什么可以直接挨个的插入呢?原因就是我们在 diff 阶段添加差异节点到差异队列时,本身就是有序的,也就是说对于新增节点(包括 move 和 insert 的)在队列里的顺序就是最终 dom 的顺序,所以我们才可以挨个的直接根据 index 去塞入节点。

这样整个的更新机制就完成了。我们再来简单回顾下 React 的差异算法:

首先是所有的 component 都实现了 receiveComponent 来负责自己的更新,而浏览器默认元素的更新最为复杂,也就是经常说的 diff algorithm。

react 有一个全局_shouldUpdateReactComponent 用来根据 element 的 key 来判断是更新还是重新渲染,这是第一个差异判断。比如自定义元素里,就使用这个判断,通过这种标识判断,会变得特别高效。

每个类型的元素都要处理好自己的更新:

  • 自定义元素的更新,主要是更新 render 出的节点,做甩手掌柜交给 render 出的节点的对应 component 去管理更新。

  • text 节点的更新很简单,直接更新文案。

  • 浏览器基本元素的更新,分为两块:

    • 先是更新属性,对比出前后属性的不同,局部更新。并且处理特殊属性,比如事件绑定。
    • 然后是子节点的更新,子节点更新主要是找出差异对象,找差异对象的时候也会使用上面的_shouldUpdateReactComponent 来判断,如果是可以直接更新的就会递归调用子节点的更新,这样也会递归查找差异对象,这里还会使用 lastIndex 这种做一种优化,使一些节点保留位置,之后根据差异对象操作 dom 元素(位置变动,删除,

end

这只是个玩具,但实现了 React 最核心的功能,虚拟节点,差异算法,单向数据更新都在这里了。还有很多 React 优秀的东西没有实现,比如对象生成时内存的线程池管理,批量更新机制,事件的优化,服务端的渲染,immutable data 等等。这些东西受限于篇幅就不具体展开了。

React 作为一种解决方案,虚拟节点的想法比较新奇,不过个人还是不能接受这种别扭的写法。使用 React,就要使用他那一整套的开发方式,而他核心的功能其实只是一个差异算法,而这种其实已经有相关的库实现了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/63683.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ_消息确认机制

消息确认机制分为消息发送确认机制与消息消费确认机制 消息发送确认机制 消息发送确认机制&#xff1a;消息由producer发送后&#xff0c;确认其是否到达broker&#xff0c;又是否被exchange转发至对应queue的机制 该机制分为两部分&#xff1a;producer---broker&#xff0c…

Android 性能优化之内存优化——重识内存

我们知道&#xff0c;手机的内存是有限的&#xff0c;如果应用内存占用过大&#xff0c;轻则引起卡顿&#xff0c;重则导致应用崩溃或被系统强制杀掉&#xff0c;更严重的情况下会影响应用的留存率。因此&#xff0c;内存优化是性能优化中非常重要的一部分。但是&#xff0c;很…

66-86-javajvm-堆

66-javajvm-堆&#xff1a; 堆的核心概述 堆与进程、线程 一个进程对应一个JVM实例一个JVM实例对应一个堆空间进程包含多个线程&#xff0c;所以线程之间共享同一个堆空间 对堆的认识 一个JVM实例只存在一个堆内存&#xff0c;堆也是Java内存管理的核心区域。Java堆区在JVM启动…

HashMap原理

在Java编程语言中&#xff0c;最基本的结构就是两种&#xff0c;一种是数组&#xff0c;一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造&#xff0c;HashMap也一样。当程序试图将多个 key-value 放入 HashMap 中时&#xff0c;以如下代码片段为例&#xff1a;…

P1182 数列分段 Section II——二分答案

数列分段 Section II 题目描述 对于给定的一个长度为N的正整数数列 A1∼NA_{1\sim N}A1∼N​&#xff0c;现要将其分成 MMM&#xff08;M≤NM\leq NM≤N&#xff09;段&#xff0c;并要求每段连续&#xff0c;且每段和的最大值最小。 关于最大值最小&#xff1a; 例如一数列…

NCTF web总结与复现

前言 打完NCTF休息了一下&#xff0c;总体感觉还行&#xff0c;学到了很多。 calc 这一题也卡了我很久&#xff0c;因为复现过DASCTF三月赛&#xff0c;一直在想着有没有可以替代反引号或绕过的方法&#xff0c;搞了好久都没出&#xff0c;在学长的提示下学到了一个方法&…

最新出炉的阿里巴巴面试题及答案汇总(513页)

前言 秋招已经结束了&#xff0c;不知道各位有没有拿到自己心仪的offer&#xff1f;最近有不少粉丝去阿里巴巴面试了&#xff0c;回来之后我整理成了一份手册java面试时常用到的面试题&#xff08;附答案&#xff09;那么今天分享给大家&#xff0c;祝愿大家都能找到满意的工作…

HTML期末作业课程设计期末大作业——我的美丽家乡湛江 海鲜之都HTML+CSS+JavaScript

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

python爬虫实战之逆向分析酷狗音乐

文章目录前言一、请求分析二、逆向思路三、全部代码总结前言 声明&#xff1a;本文章只是用于学习逆向知识&#xff0c;仅供学习&#xff0c;未经作者同意禁止转载 对于爬虫而言&#xff0c;不管是什么类型的都会遵循这几个步骤 获取目标url分析请求数据逆向解密数据伪造请求清…

算法日常训练12.5

首先有个很大的进步&#xff0c;看见困难题我没选择做逃兵跑路&#xff0c;这点起码是进步了&#xff0c;虽然算法能力还是那么拉&#xff0c;但是起码敢不自量力地分析一下。。。还能看题解理解下。 先找题解中最简单地一种超时方法开始理解&#xff0c;使用动态规划&#xff…

线程基础概念

1.线程基础 现代软件系统中&#xff0c;除了进程之外&#xff0c;线程也是一个十分重要的概念。特别是随着CPU频率增长开始出现停滞&#xff0c;而开始向多核方向发展。多线程&#xff0c;作为实现软件并发执行的一个重要的方法&#xff0c;也开始具有越来越重要的地位。 什么…

[本人毕业设计] 别踩白块_计算机科学与技术_前端H5游戏毕设

摘 要 本文详细介绍了网页版躲避白色钢琴块音乐游戏的设计和实现。由于游戏软件安装占据较大的空间与安装时间&#xff0c;而且步骤繁琐&#xff0c;用常规的游戏安装方法不能取得便捷的游戏安装体验。网页游戏是一种基于在网络游戏中被广泛应用&#xff0c;网页游戏更具有便捷…

【Tensorflow深度学习】实现手写字体识别、预测实战(附源码和数据集 超详细)

需要源码和数据集请点赞关注收藏后评论区留言私信~~~ 一、数据集简介 下面用到的数据集基于IAM数据集的英文手写字体自动识别应用&#xff0c;IAM数据库主要包含手写的英文文本&#xff0c;可用于训练和测试手写文本识别以及执行作者的识别和验证&#xff0c;该数据库在ICDAR1…

对副业的选择无论是自媒体还是 Python接单 ,始终绕不开IT行业。

前言 这个年代&#xff0c;成年人的日子活成了一部苦情戏。十年前&#xff0c;5000块钱工资还能过的自由自在&#xff1b;今天&#xff0c;估计连车贷&#xff0c;房贷&#xff0c;信用卡都不够还。所以一些想要改变现状的朋友&#xff0c;选择了副业这种形式&#xff0c;副业…

【Linux】Shell脚本详解

目录一.概述二.Linux提供的Shell解析器三.Shell入门1.执行一个简单的shell脚本2.脚本常用的执行方法四.变量1.系统预定义变量2.自定义变量3.特殊变量五.运算符六.条件判断1.单条件判断2.多条件判断七.流程控制(重点)1.if判断2.case语句3.for循环4.while循环八.read读取控制台输…

【论文简述】 Point-MVSNet:Point-Based Multi-View Stereo Network(ICCV 2019)

一、论文简述 1. 第一作者&#xff1a;Rui Chen、Songfang Han 2. 发表年份&#xff1a;2019 3. 发表期刊&#xff1a;ICCV 4. 关键词&#xff1a;MVS、深度学习、点云、迭代改进 5. 探索动机&#xff1a;很多传统方法通过多视图光度一致性和正则化优化迭代更新&#xff…

C语言实例|使用C程序优雅地杀掉其它程序进程

C语言文章更新目录 C语言学习资源汇总&#xff0c;史上最全面总结&#xff0c;没有之一 C/C学习资源&#xff08;百度云盘链接&#xff09; 计算机二级资料&#xff08;过级专用&#xff09; C语言学习路线&#xff08;从入门到实战&#xff09; 编写C语言程序的7个步骤和编程…

FPGA 20个例程篇:18.SD卡存放音频WAV播放(中)

第七章 实战项目提升&#xff0c;完善简历 18.SD卡存放音频WAV播放&#xff08;中&#xff09; 如图1所示是WM8731中11个寄存器功能说明概况图&#xff0c;我们需要对照手册&#xff0c;再去深入了解WM8731中的11个寄存器&#xff0c;怎么去配置这些寄存器达到预期的效果&…

了解3dmax坐标系

3dmax具有多种坐标系&#xff0c;其类别如下&#xff1b;默认的是View坐标系&#xff1b; 新建一个茶壶&#xff0c;此时默认是View坐标系&#xff1b; 切换到屏幕坐标系&#xff0c;看一下如下图&#xff1b;要保持视口区域激活&#xff1b; 根据资料&#xff0c;屏幕坐标系&a…

园区如何快速实现数据可视化分析?

对于园区运营方来说&#xff0c;如果没有专业针对性的管理方案以及管理系统辅助的话&#xff0c;实现园区可视化管理的难度非常大&#xff0c;而且操作成本会很高。但如果园区运营方选择引进快鲸智慧楼宇推出的园区数据孪生可视化管理系统的话就会简单很多。 快鲸智慧楼宇数据孪…