React 渲染过程

  1. 程序假设有如下 jsx
class Form extends React.Component {
  constructor() {
    super();
  }
  render() {
    return (
        <form>
          <input type="text"/>
        </form>
    );
  }
}

ReactDOM.render( (
  <div className="test">
    <span onClick={function(){}}>CLICK ME</span>
    <Form/>
  </div>
), document.getElementById('main'))
  1. 拿 ReactDOM render 的部分(<div className="test">...</div>)为例,用 babel 把 jsx 转成 js 后得到如下代码:
React.createElement( 'div', {
  className: 'test'
  },
  React.createElement( 'span',
    { onClick: function(){} },
    'CLICK ME'
  ),
  React.createElement(Form, null)
)
  1. 这里看下 API: React.createElement(component, props, ...children)。它生成一个 js 的对象,这个对象是用来代表一个真实的 dom node,这个 js 的对象就是我们俗称的虚拟dom。(虚拟 dom 的意思是用 js 对象结构模拟出 html 中 dom 结构,批量的增删改查先直接操作 js 对象,最后更新到真正的 dom 树上。因为直接操作 js 对象的速度要比操作 dom 的那些 api 要快。) 譬如根元素 <div className="test"></div> 生成对应出来的虚拟 dom 是:
{
  type: 'div',
  props: {
    className: 'test',
    children: []
  }
}

除了一些 dom 相关的属性,虚拟 dom 对象还包括些 React 自身需要的属性,譬如:ref,key。最终示例中的 jsx 生成出来的虚拟 dom,大致如下:

{
  type: 'div',
  props: {
    className: 'xxx',
    children: [ {
      type: 'span',
      props: {
        children: [ 'CLICK ME' ]
      },
      ref:
      key:
    }, {
      type: Form,
      props: {
        children: []
      },
      ref:
      key:
    } ] | Element
  }
  ref: 'xxx',
  key: 'xxx'
}
  1. 有了虚拟 dom,接下来的工作就是把这个虚拟 dom 树真正渲染成一个 dom 树。React 的做法是针对不同的 type 构造相应的渲染对象,渲染对象提供一个 mountComponent 方法(负责把对应的某个虚拟 dom 的节点生成成具体的 dom node),然后循环迭代整个 vdom tree 生成一个完整的 dom node tree,最终插入容器节点。查看源码你会发现如下代码:
// vdom 是第3步生成出来的虚拟 dom 对象
var renderedComponent = instantiateReactComponent( vdom );
// dom node
var markup = renderedComponent.mountComponent();
// 把生成的 dom node 插入到容器 node 里面,真正在页面上显示出来
// 下面是伪代码,React 的 dom 操作封装在 DOMLazyTree 里面
containerNode.appendChild( markup );

instantiateReactComponent 传入的是虚拟 dom 节点,这个方法做的就是根据不同的 type 调用如下方法生成渲染对象:

// 如果节点是字符串或者数字
return ReactHostComponent.createInstanceForText( vdom(string|number) );
// 如果节点是宿主内置节点,譬如浏览器的 html 的节点
return ReactHostComponent.createInternalComponent( vdom );
// 如果是 React component 节点
return new ReactCompositeComponentWrapper( vdom );

ReactHostComponent.createXXX 也只是一层抽象,不是最终的的渲染对象,这层抽象屏蔽了宿主。譬如手机端(React native)和浏览器中同样调用 ReactHostComponent.createInternalComponent( vdom ); 他生成的最终的渲染对象是不同的,我们当前只讨论浏览器环境。字符串和数字没有什么悬念,在这里我们就不深入探讨了,再进一步看,div 等 html 的原生 dom 节点对应的渲染对象是 ReactDOMComponent 的实例。如何把 { type:'div', ... } 生成一个 dom node 就在这个类(的 mountComponent 方法)里面。(对如何生成 div、span、input、select 等 dom node 感兴趣的可以去探究 ReactDOMComponent,这里不做具体的讨论,本文只是想总结下 React 整个渲染过程。下面只给出一个最简示例代码:

class ReactDOMComponent {
  constructor( vdom ) {
    this._currentElement = vdom;
  }
  mountComponent() {
    var result;
    var props = this._currentElement.props;
    if ( this._currentElement.type === 'div' ) {
      result = document.createElement( 'div' );
      
      for(var key in props ) {
        result.setAttribute( key, props[ key ] );
      }
    } else {
      // 其他类型
    }
    // 迭代子节点
    props.children.forEach( child=>{
      var childRenderedComponent =  = instantiateReactComponent( child );
      var childMarkup = childRenderedComponent.mountComponent();
      result.appendChild( childMarkup );
    } )
    return result;
  }
}

我们再看下 React component 的渲染对象 ReactCompositeComponentWrapper(主要实现在 ReactCompositeComponent 里面,ReactCompositeComponentWrapper 只是一个防止循环引用的 wrapper

// 以下是伪代码
class ReactCompositeComponent {
  _currentElement: vdom,
  _rootNodeID: 0,
  _compositeType:
  _instance: 
  _hostParent:
  _hostContainerInfo: 
  // See ReactUpdateQueue
  _updateBatchNumber:
  _pendingElement:
  _pendingStateQueue:
  _pendingReplaceState:
  _pendingForceUpdate:
  _renderedNodeType:
  _renderedComponent:
  _context:
  _mountOrder:
  _topLevelWrapper:
  // See ReactUpdates and ReactUpdateQueue.
  _pendingCallbacks:
  // ComponentWillUnmount shall only be called once
  _calledComponentWillUnmount:

  // render to dom node
  mountComponent( transaction, hostParent, hostContainerInfo, context ) {
    // ---------- 初始化 React.Component --------------
    var Component = this._currentElement.type;
    var publicProps = this._currentElement.props;
    /*
      React.Component 组件有2种:
      new Component(publicProps, publicContext, updateQueue);
      new StatelessComponent(Component);
      对应的 compositeType 有三种
      this._compositeType = StatelessFunctional | PureClass | ImpureClass,
      组件种类和 compositeType 在源码中都有区分,但是这里为了简单,只示例最常用的一种组件的代码
    */
    var inst = new Component(publicProps, publicContext, updateQueue);
    
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;
    
    // 渲染对象存储组件对象
    this._instance = inst;

    // 通过 map 又把组件对象和渲染对象联系起来
    ReactInstanceMap.set(inst, this);
    /*
      ReactInstanceMap: {
              -----------------------------------------------
              |                                              |
              v                                              |
        React.Component: ReactCompositeComponentWrapper {    |
          _instance:  <-------------------------------------
        }
      }
      这样双方都在需要对方的时候可以获得彼此的引用
    */

    // ---------- 生成 React.Component 的 dom  --------------
    // 组件生命周期函数 componentWillMount 被调用
    inst.componentWillMount();
    // 调用 render 方法返回组件的虚拟 dom
    var renderedElement = inst.render();
    // save nodeType  
    var nodeType = ReactNodeTypes.getType(renderedElement);
    this._renderedNodeType = nodeType;
    // 根据组件的虚拟 dom 生成渲染对象
    var child = instantiateReactComponent(renderedElement)
    this._renderedComponent = child;
    // 生成真正的 dom node
    // 其实源码中的真正代码应该是 var markup = ReactReconciler.mountComponent( child, ... ),
    // 这里为了简化说明,先不深究 ReactReconciler.mountComponent 还做了点什么
    var markup = child.mountComponent(); 
    // 把组件生命周期函数 componentDidMount 注册到回调函数中,当整个 dom node tree 被添加到容器节点后触发。
    transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    return markup;
  }
}
// static member
ReactCompositeComponentWrapper._instantiateReactComponent = instantiateReactComponent
  1. 最终的过程是:
<div className="test">
  <span onClick={this.click}>CLICK ME</span>
  <Form/>
</div>

|
babel and React.createElement
|
v

{
  type: 'div',
  props: {
    className: 'xxx',
    children: [ {
      type: 'span',
      props: {
        children:
      },
      ref:
      key:
    }, {
      type: Form,
      props: {
        children:
      },
      ref:
      key:
    } ] | Element
  }
  ref: 'xxx',
  key: 'xxx'
}

|
var domNode = new ReactDOMComponent( vdom ).mountComponent();
|
v

domNode = {
  ReactDOMComponent -> <div/>
  props: {
    children: [
      ReactDOMComponent -> <span/>
      ReactCompositeComponentWrapper.render() -> vdom -> instantiateReactComponent(vdom) -> <form><input/></from>
    ]
  }
}

|
|
v
containerDomNode.appendChild( domNode );

以上是 React 渲染 dom 的一个基本流程,下一篇计划总结下更新 dom 的流程,即 setState 后发生了什么。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容

  • 自己最近的项目是基于react的,于是读了一遍react的文档,做了一些记录(除了REFERENCE部分还没开始读...
    潘逸飞阅读 3,309评论 1 10
  • It's a common pattern in React to wrap a component in an ...
    jplyue阅读 3,230评论 0 2
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,807评论 1 18
  • 以下内容是我在学习和研究React时,对React的特性、重点和注意事项的提取、精练和总结,可以做为React特性...
    科研者阅读 8,198评论 2 21
  • 深入JSX date:20170412笔记原文其实JSX是React.createElement(componen...
    gaoer1938阅读 8,032评论 2 35