深入理解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值,判断是否递归更新子节点,还是删除旧节点,添加新节点。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏coder修行路

Go基础--终端操作和文件操作

终端操作 操作终端相关的文件句柄常量 os.Stdin:标准输入 os.Stdout:标准输出 os.Stderr:标准错误输出 关于终端操作的代码例子: pa...

2996
来自专栏吴裕超

认识createDocumentFragment

今天在看vue源码解析时候发现一个api没有见过,一查是原生的,赶紧补漏。 DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的...

2737
来自专栏老九学堂

【干货】20K以上的高薪Java必掌握的基础知识点(二)

怎么样!上一期的知识点小伙伴都掌握了多少呢?复习的同时有没有查漏补缺的巩固自己的基础知识呢?今天我们来复习Java基础知识第二期! 61、Math 类提供了许多...

3827
来自专栏Golang语言社区

Go语言的os包中常用函数初步归纳

1)os.Getwd函数原型是func Getwd() (pwd string, err error) 返回的是路径的字符串和一个err信息,为什么先开这个呢?...

3499
来自专栏柠檬先生

Angularjs基础(十一)

ng-csp       描述:修改内容的安全策略       实例: 修改AngularJS 中关于"eval"的行为方式及内联样式;         ...

2635
来自专栏超然的博客

react入门——慕课网笔记

  1. 被称为语法糖:糖衣语法,计算机语言中添加的某种语法,对语言的功能没有影响,更方便程序员使用,增加程序的可读性,降低出错的可能性

1022
来自专栏web前端-

rem和em小插曲

1.对em来说,它的大小是相对于父层font-size来改变,但是如果其自身有font-size属性的话,em会优先考虑自身的font-size;

932
来自专栏Golang语言社区

Go 语言简介(下) - 特性

goroutine GoRoutine主要是使用go关键字来调用函数,你还可以使用匿名函数,如下所示: package main import "fmt" f...

2805
来自专栏禹都一只猫博客

Go语言简介 — 特性

1191
来自专栏Golang语言社区

Go语言的os包中常用函数初步归纳

1)os.Getwd函数原型是func Getwd() (pwd string, err error) 返回的是路径的字符串和一个err信息,为什么先开这个呢?...

3987

扫码关注云+社区

领取腾讯云代金券