深入理解React Native页面构建渲染原理

时间:2022-04-24
本文章向大家介绍深入理解React Native页面构建渲染原理,主要内容包括前言、React Native源码剖析、ReactClass、React Native更新机制、总结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

前言

React Native 是最近非常火的一个话题,因为它的语法简介,跨平台等特性,赢得了各大平台的青睐,虽然前期是有一些坑。

基本概念解释

React 是一套可以用简洁的语法高效绘制 DOM 的框架,所谓的“高效”,是因为 React 独创了 Virtual DOM 机制。Virtual DOM 是一个存在于内存中的 JavaScript 对象,它与 DOM 是一一对应的关系,也就是说只要有 Virtual DOM,我们就能渲染出 DOM。当界面发生变化时,得益于高效的 DOM Diff 算法,我们能够知道 Virtual DOM 的变化,从而高效的改动 DOM,避免了重新绘制 DOM。

我们知道React是一个专注于 UI 部分框架,对应到 MVC 结构中就是 View 层。要想实现完整的 MVC 架构,还需要 Model 和 Controller 的结构。在前端开发时,我们可以采用 Flux 和 Redux 架构,它们并非框架(Library),而是和 MVC 一样都是一种架构设计(Architecture)。

我们知道React Native之所以能再Android/ios等移动设备上运行起来,是因为react native和原生设备之间有一种交互,以ios为例,JavaScript 的形式告诉 Objective-C需要执行什么,然后ios自己去调用 UIKit 等框架绘制界面。所以,React Native 能够运行起来,全靠 Objective-C 和 JavaScript 的交互。

我们知道 C 系列的语言,经过编译,链接等操作后,会得到一个二进制格式的可执行文,所谓的运行程序,其实是运行这个二进制程序。而 JavaScript 是一种脚本语言,它不会经过编译、链接等操作,而是在运行时才动态的进行词法、语法分析,生成抽象语法树(AST)和字节码,然后由解释器负责执行或者使用 JIT 将字节码转化为机器码再执行。整个流程由 JavaScript 引擎负责完成。

苹果提供了一个叫做 JavaScript Core 的框架,这是一个 JavaScript 引擎。通过下面这段代码可以简单的感受一下 Objective-C 如何调用 JavaScript 代码:

JSContext *context = [[JSContext alloc] init];  
JSValue *jsVal = [context evaluateScript:@"21+7"];  
int iVal = [jsVal toInt32];

JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。 要完全理解JavaScript和Objective-C之前的交互,可以看我之前关于这方面吗的介绍React native和原生之间的通信

React Native源码剖析

在解释React Native的也没渲染原理之前,我们先看几个概念。

ReactElement和ReactClass

ReactElement 一个描述DOM节点或component实例的字面级对象。它包含一些信息,包括组件类型 type 和属性 props 。就像一个描述DOM节点的元素(虚拟节点)。它们可以被创建通过 React.createElement 方法或 jsx 写法。 ReactElement分为 DOM Element 和 Component Elements 两类: DOM Elements实例

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

Component Elements 当节点的type属性为一个函数或一个类时,它代表自定义的节点。 Component Elements实例

class Button extends React.Component {
  render() {
    const { children, color } = this.props;
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
}

// Component Elements
{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

ReactClass

ReactClass是平时我们写的Component组件(类或函数),例如上面的 Button 类。ReactClass实例化后调用render方法可返回 DOM Element 。

如上图所示:

  1. 调用 React.render 方法,将我们的 element 根虚拟节点渲染到 container 元素中。element 可以是一个字符串文本元素,也可以是如上介绍的 ReactElement 。
  2. 根据 element 的类型不同,分别实例化 ReactDOMTextComponent , ReactDOMComponent , ReactCompositeComponent 类。这些类用来管理 ReactElement ,负责将不同的 ReactElement 转化成DOM,并更新DOM。
  3. ReactCompositeComponent 实例调用 mountComponent 方法后内部调用 render 方法,返回了 DOM Elements 。再对如图的步骤:two:递归。React Native工作原理介绍 要想深入理解 React Native 的工作原理,有两个阶段必须了解:初始化阶段和方法调用阶段。初始化 React Native 每个项目都有一个入口,然后进行初始化操作,React Native 也不例外。一个不含 Objective-C 代码的项目留给我们的唯一线索就是位于 AppDelegate 文件中,用户能看到的一切内容都来源于这个 RootView ,所有的初始化工作也都在这个方法内完成。

在这个方法内部,在创建 RootView 之前,React Native 实际上先创建了一个 Bridge 对象。它是 Objective-C 与 JavaScript 交互的桥梁,后续的方法交互完全依赖于它,而整个初始化过程的最终目的其实也就是创建这个桥梁对象。 初始化方法的核心是 setUp 方法,而 setUp 方法的主要任务则是创建 BatchedBridge 。BatchedBridge 的作用是批量读取 JavaScript 对 Objective-C 的方法调用,同时它内部持有一个 JavaScriptExecutor 。创建 BatchedBridge 的关键是 start 方法,它可以分为五个步骤:

  • 读取 JavaScript 源码
  • 初始化模块信息
  • 初始化 JavaScript 代码的执行器,即 RCTJSCExecutor 对象
  • 生成模块列表并写入 JavaScript 端
  • 执行 JavaScript 源码JavaScript 调用 Objective-C 在调用 Objective-C 代码时,如前文所述,JavaScript 会解析出方法的 ModuleId 、 MethodId 和 Arguments 并放入到 MessageQueue 中,等待 Objective-C 主动拿走,或者超时后主动发送给 Objective-C。

Objective-C 负责处理调用的方法是 handleBuffer ,它的参数是一个含有四个元素的数组,每个元素也都是一个数组,分别存放了 ModuleId 、 MethodId 、 Params ,第四个元素目测用处不大。 函数内部在每一次方调用中调用 _handleRequestNumber:moduleID:methodID:params 方法。,通过查找模块配置表找出要调用的方法,并通过 runtime 动态的调用:

[method invokeWithBridge:self module:moduleData.instance arguments:params];

processMethodSignature ,它会根据 JavaScript 的 CallbackId 创建一个 Block,并且在调用完函数后执行这个 Block。

React Native更新机制

之前我们说过,React是有个状态机这么一说的,就是不行的去检查当前的状态,是否需要刷新。

调用this.setState

ReactClass.prototype.setState = function(newState) {
    this._reactInternalInstance.receiveComponent(null, newState);
}

调用内部receiveComponent方法

这里在接受元素的时候主要分三种情况,文本元素,基本元素,自定义元素。 自定义元素

ReactCompositeComponent.prototype.receiveComponent = function(nextElement, transaction, nextContext) {
    var prevElement = this._currentElement;
    var prevContext = this._context;

    this._pendingElement = null;

    this.updateComponent(
      transaction,
      prevElement,
      nextElement,
      prevContext,
      nextContext
    );
}

updateComponent

ReactCompositeComponent.prototype.updateComponent = function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext
){
//省略
}

调用内部_performComponentUpdate方法

ReactCompositeComponent.prototype._updateRenderedComponentWithNextElement = function() {
   
   // 判定两个element需不需要更新
   if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
     // 如果需要更新,就继续调用子节点的receiveComponent的方法,传入新的element更新子节点。
     ReactReconciler.receiveComponent(
       prevComponentInstance,
       nextRenderedElement,
       transaction,
       this._processChildContext(context)
     );
   } else {
     // 卸载之前的子节点,安装新的子节点
     var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
     ReactReconciler.unmountComponent(
       prevComponentInstance,
       safely,
       false /* skipLifecycle */
     );

     var nodeType = ReactNodeTypes.getType(nextRenderedElement);
     this._renderedNodeType = nodeType;
     var child = this._instantiateReactComponent(
       nextRenderedElement,
       nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
     );
     this._renderedComponent = child;

     var nextMarkup = ReactReconciler.mountComponent(
       child,
       transaction,
       this._hostParent,
       this._hostContainerInfo,
       this._processChildContext(context),
       debugID
     );
   
   }

文本元素

ReactDOMTextComponent.prototype.receiveComponent(nextText, transaction) {
     //跟以前保存的字符串比较
    if (nextText !== this._currentElement) {
      this._currentElement = nextText;
      var nextStringText = '' + nextText;
      if (nextStringText !== this._stringText) {
        this._stringText = nextStringText;
        var commentNodes = this.getHostNode();
        // 替换文本元素
        DOMChildrenOperations.replaceDelimitedText(
          commentNodes[0],
          commentNodes[1],
          nextStringText
        );
      }
    }
  }

基本元素

ReactDOMComponent.prototype.receiveComponent = function(nextElement, transaction, context) {
    var prevElement = this._currentElement;
    this._currentElement = nextElement;
    this.updateComponent(transaction, prevElement, nextElement, context);
}

updateComponent

ReactDOMComponent.prototype.updateComponent = function(transaction, prevElement, nextElement, context) {
    // 略.....
    //需要单独的更新属性
    this._updateDOMProperties(lastProps, nextProps, transaction, isCustomComponentTag);
    //再更新子节点
    this._updateDOMChildren(
      lastProps,
      nextProps,
      transaction,
      context
    );

    // ......
}

this._updateDOMChildren 方法内部调用diff算法。

_updateChildren: function(nextNestedChildrenElements, transaction, context) {
    var prevChildren = this._renderedChildren;
    var removedNodes = {};
    var mountImages = [];
    
    // 获取新的子元素数组
    var nextChildren = this._reconcilerUpdateChildren(
      prevChildren,
      nextNestedChildrenElements,
      mountImages,
      removedNodes,
      transaction,
      context
    );
    
    if (!nextChildren && !prevChildren) {
      return;
    }
    
    var updates = null;
    var name;
    var nextIndex = 0;
    var lastIndex = 0;
    var nextMountIndex = 0;
    var lastPlacedNode = null;

    for (name in nextChildren) {
      if (!nextChildren.hasOwnProperty(name)) {
        continue;
      }
      var prevChild = prevChildren && prevChildren[name];
      var nextChild = nextChildren[name];
      if (prevChild === nextChild) {
          // 同一个引用,说明是使用的同一个component,所以我们需要做移动的操作
          // 移动已有的子节点
          // NOTICE:这里根据nextIndex, lastIndex决定是否移动
        updates = enqueue(
          updates,
          this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
        );
        
        // 更新lastIndex
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 更新component的.mountIndex属性
        prevChild._mountIndex = nextIndex;
        
      } else {
        if (prevChild) {
          // 更新lastIndex
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        }
        
        // 添加新的子节点在指定的位置上
        updates = enqueue(
          updates,
          this._mountChildAtIndex(
            nextChild,
            mountImages[nextMountIndex],
            lastPlacedNode,
            nextIndex,
            transaction,
            context
          )
        );
        
        
        nextMountIndex++;
      }
      
      // 更新nextIndex
      nextIndex++;
      lastPlacedNode = ReactReconciler.getHostNode(nextChild);
    }
    
    // 移除掉不存在的旧子节点,和旧子节点和新子节点不同的旧子节点
    for (name in removedNodes) {
      if (removedNodes.hasOwnProperty(name)) {
        updates = enqueue(
          updates,
          this._unmountChild(prevChildren[name], removedNodes[name])
        );
      }
    }
  }

总结

react的优点总结:

  • 虚拟节点。在UI方面,不需要立刻更新视图,而是生成虚拟DOM后统一渲染。
  • 组件机制。各个组件独立管理,层层嵌套,互不影响,react内部实现的渲染功能。
  • 差异算法。根据基本元素的key值,判断是否递归更新子节点,还是删除旧节点,添加新节点。