React学习---渲染机制

      在介绍React渲染机制之间先来说一说下面几个概念,对于新入手React的学员来说,经常会被搞蒙圈。
      React与ReactDOM区别
      在v0.14前,ReactDOM是React的函数,之所以在v0.14之后分成两个包是package是因为

      As we look at packages like react-native, react-art, react-canvas, and react-three, it has become clear that the beauty and essence of React has nothing to do with browsers or the DOM.
      To make this more clear and to make it easier to build more environments that React can render to, we’re splitting the main react package into two: react and react-dom. This paves the way to writing components that can be shared between the web version of React and React Native. We don’t expect all the code in an app to be shared, but we want to be able to share the components that do behave the same across platforms.

      可见,React分为react-dom和react的原因是React-Native的出现,它可以实现跨平台实现相同的组件。
      react包包含了React.createElement、 .createClass、 .Component、 .PropTypes、.Children以及其他的描述元素和组件的类。
      react-dom包包含了ReactDOM.render、.unmountComponentAtNode和.findDOMNode等。下面看一个创建组件的实例:

var React = require('react');
var ReactDOM = require('react-dom');
var MyComponent = React.createClass({
render: function() {
return <div>Hello World</div>;
}
});
ReactDOM.render(<MyComponent />, node);

      ReactDOM.render是React的最基本方法用于将模板转为HTML语言,并插入指定的DOM节点。它可以将一个React元素呈现在指定的DOM container中,并返回对组件的引用对象。

      Component、Element和Component

       Component
      Component组件就是JavaScript函数,可以接受任意参数。可以使用createClass和Component创建组件。createClass如其名就是创建React组件对应的类,描述你将要创建组件的各种行为,其中只有当组件被渲染时需要输出的内容的render接口是必须实现的,其他都是可选:

var SayHello = React.createClass({
      render: function() {
      return <div>Hello {this.props.name}</div>;
    }
});

从 React 0.13 开始,可以使用 ES6 Class代替 React.createClass了

class SayHello extends React.Component {
render() {
return <div>Hello {this.props.name}</a> </div>;
}
}

注意:(1)为了区分Component和DOM 元素,所有Component名字要首字母大写。
      (2)所有的React组件都不能修改它的prop属性的值
       Element
      Element是class的一个实例,它描述DOM节点或component实例的字面级对象。它包含一些信息,包括组件类型type和属性props。就像一个描述DOM节点的元素(虚拟节点)。可以使用createElement,创建React组件实例

ReactElement.createElement= function(type, config, children) {
...
}

在上一篇文章中我们已经讲到,当我们用JSX来描述< SayHello />时 ,编译后就是React.createElement()。
      Factory
      React.createFactory和ReactElement.createElement一样,但是
Factory返回的是给定元素类型或组件类的实例。React.createFactory的定义基本就是如下形式,Element 的类型被提前绑定了

ReactElement.createFactory = function (type) {
var factory = ReactElement.createElement.bind(null, type);
    factory.type = type;
    return factory;
};

官方鼓励使用JSX或者React.creatElement。下面进入今天的主题:React的渲染机制
      React渲染机制
      假设要实现的渲染代码如下:

  class Form extends React.Component {
    constructor() {
    super();
  }
  render() {
    return (
        <form>
          <input type="text"/>
        </form>
    );
  }
}

ReactDOM.render( (
  <div className="test">
    <p>请输入你的信息</p>
    <Form/>
  </div>
), document.getElementById('example'))

      从ReactDOM入口开始,找到ReactDOM.js文件

var ReactDOMComponentTree = require('./ReactDOMComponentTree');
var ReactDefaultInjection = require('./ReactDefaultInjection');
var ReactMount = require('./ReactMount');
var ReactReconciler = require('./ReactReconciler');
var ReactUpdates = require('./ReactUpdates');
var ReactVersion = require('./ReactVersion');

var findDOMNode = require('./findDOMNode');
var getHostComponentFromComposite = require('./getHostComponentFromComposite');
var renderSubtreeIntoContainer = require('./renderSubtreeIntoContainer');
var warning = require('fbjs/lib/warning');
ReactDefaultInjection.inject(); 
var ReactDOM = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion,

  /* eslint-disable camelcase */
  unstable_batchedUpdates: ReactUpdates.batchedUpdates,
  unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
  /* eslint-enable camelcase */
};

      ReactDOM.render()方法来自ReactMount文件的render方法:

render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },

       Render方法返回的是当前文件下的_renderSubtreeIntoContainer方法,顾名思义,它的作用是将子树nextElement注入到指定的container中,并调用其回调方法_renderSubtreeIntoContainer方法主要完成以下一个功能:

  1. 调用React.createElement生成待插入节点的虚拟DOM的实例对象(上篇文章中已经讲到,这里就不再重复叙述)
  2. 与之前的component比较,如果是初次渲染直接将虚拟DOM转换为真实DOM
  3. 将真实的组件写到对应的container节点中
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
/*
*将callback放入到React的更新队列中,判断nextElement有效性以及当前是发开环境还是生产环境。(代码省略)
*/
…

var nextWrappedElement = React.createElement(TopLevelWrapper, {child: nextElement});
// 返回一个ReactElement实例对象,也就是虚拟DOM的实例对象,下面就要把它渲染出来
var nextContext;
/*(1)判断是否有parentComponent,如果有则将其写到parentComponent*/
if (parentComponent) {
var parentInst = ReactInstanceMap.get(parentComponent);
nextContext = parentInst._processChildContext(parentInst._context);
} else {
nextContext = emptyObject;
}

var prevComponent =getTopLevelWrapperInContainer(container);
 /*
*(2)判断是否有prevComponent,如果有则取其child,利用Diff算法判断是否需要更新,如果需要则调用_updateRootComponen 方法,并return结果。对于初次渲染不需要该过程。 
*/

if (prevComponent) {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props.child;
 // shouldUpdateReactComponent方法判断是否需要更新,对同一DOM进行比较,type相同,key(如果有)相同的组件做DOM diff 

if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback = callback && function () {
callback.call(publicInst);
};
ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
return publicInst;
} else {
ReactMount.unmountComponentAtNode(container);
}
}
/*
*(3)对于prevElement为null直接跳到此步
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
var containerHasNonRootReactChild = hasNonRootReactChild(container);
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
/*
*3核心部分,调用_renderNewRootComponent,_renderedComponent,getPublicInstance三个方法,
*/
var component =ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
}

下面来看一看RenderNewRootComponent方法的源码

(1)_renderNewRootComponent:

_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
… …
 /*
*第一部分是调用instantiateReactComponen方法返回虚拟DOM对应的DOM,并将其返回结果作为_renderNewRootComponent的最终返回结果 
*/
var componentInstance = <a name="OLE_LINK14"></a> <a name="OLE_LINK13">instantiateReactComponent</a> (nextElement, false);
return componentInstance;
 /*
*第二部分是处理batchedMountComponentIntoNode方法,并将上面的结果componentInstance 结果插入到DOM中
*/*

ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
}

解析第一部分:将虚拟DOM生成DOM
       再来看一看instantiateReactComponent方法,它是从instantiateReactComponent文件require进来的,输入参数是虚拟DOM节点,主要实现的功能是将虚拟DOM生成DOM。根element的类型不同,分别实例化ReactDOMTextComponent, ReactDOMComponent, ReactCompositeComponent类。
下面来看一看instantiateReactComponent.js源码的伪代码

function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
/*
*判断node类型,不同的类型调用不同的渲染方法
*/
/*节点为空*/
instance = ReactEmptyComponent.create(instantiateReactComponent); 
/*如果节点是宿主内置节点,譬如浏览器的 的节点*/
instance = ReactHostComponent.createInternalComponent(element);

instance = new ReactCompositeComponentWrapper(element);
/*如果节点是字符串或数字*/
instance = ReactHostComponent.createInstanceForText(node);
return instance;
}

解析第二部分:将新的component分批插入到DOM中
       batchedUpdates是ReactUpdates的一个方法,使用batchingStrategy更新DOM

function batchedUpdates(callback, a, b, c, d, e) {
ensureInjected();
return batchingStrategy .batchedUpdates(callback, a, b, c, d, e);
}

       batchedUpdates 方法的回调函数是batchedMountComponentIntoNode方法

function batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
/* useCreateElement */
!shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement);
transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);
ReactUpdates.ReactReconcileTransaction.release(transaction);
}

batchedMountComponentIntoNode 方法通过
transaction.perform调用mountComponentIntoNode方法。

/*
* Mounts this component and inserts it into the DOM.
* @param {ReactComponent} componentInstance The instance to mount.
* @param {DOMElement} container DOM element to mount into.
* @param {ReactReconcileTransaction} transaction
* @param {boolean} shouldReuseMarkup If true, do not insert markup
*/
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
var wrappedElement = wrapperInstance._currentElement.props.child;
var type = wrappedElement.type;
markerName = 'React mount: ' + (typeof type === 'string' ? type : type.displayName || type.name);
console.time(markerName);
}
 **//*调用对应ReactReconciler中的mountComponent方法来渲染组件,返回React组件解析后的HTML** 

var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context, 0 /* parentDebugID */);
if (markerName) {
console.timeEnd(markerName);
}
wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
 /*将解析出来的HTML插入DOM中 ReactMount._mountImageIntoNode</a> (markup, container, wrapperInstance, shouldReuseMarkup, transaction);

}

_mountImageIntoNode 可以实现将HTML插入DOM中的操作方法是:

_mountImageIntoNode: function (markup, container, instance, shouldReuseMarkup, transaction) {
/*是否复用markup
if (shouldReuseMarkup) {
/*如果可以复用,直接将instance插入到根元素** 
var rootElement = getReactRootElementInContainer(container);
if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
ReactDOMComponentTree.precacheNode(instance, rootElement);
return;
} else {
/*创建新的normalizedMarkup
var checksum = rootElement.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
var rootMarkup = rootElement.outerHTML;
rootElement.setAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME, checksum);
var normalizedMarkup = markup;
if (process.env.NODE_ENV !== 'production') {
var normalizer;
if (container.nodeType === ELEMENT_NODE_TYPE) {
normalizer = document.createElement('div');
normalizer.innerHTML = markup;
normalizedMarkup = normalizer.innerHTML;
} else {
normalizer = document.createElement('iframe');
document.body.appendChild(normalizer);
normalizer.contentDocument.write(markup);
normalizedMarkup = normalizer.contentDocument.documentElement.outerHTML;
document.body.removeChild(normalizer);
}
}
if (transaction.
while (container.lastChild) {
container.removeChild(container.lastChild);
}
DOMLazyTree.insertTreeBefore(container, markup, null);
} else {
/*利用innerHTML将markup插入到container这个DOM元素上** 
setInnerHTML(container, markup);
 /*将instance保存到container的原生节点firstChild上*/
ReactDOMComponentTree.precacheNode(instance, container.firstChild);
}
if (process.env.NODE_ENV !== 'production') {
var hostNode = ReactDOMComponentTree.getInstanceFromNode(container.firstChild);
if (hostNode._debugID !== 0) {
ReactInstrumentation.debugTool.onHostOperation({
instanceID: hostNode._debugID,
type: 'mount',
payload: markup.toString()
});
}
}
}
};

这么多的内容有点懵懵的,下面我们通过简单的流程图总结:


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

推荐阅读更多精彩内容

  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,809评论 1 18
  • 自己最近的项目是基于react的,于是读了一遍react的文档,做了一些记录(除了REFERENCE部分还没开始读...
    潘逸飞阅读 3,333评论 1 10
  • React Native是基于React的,在开发React Native过程中少不了的需要用到React方面的知...
    亓凡阅读 1,450评论 1 4
  • 现在最热门的前端框架,毫无疑问是 React 。上周,基于 React 的 React Native 发布,结果一...
    sakura_L阅读 419评论 0 0
  • 杨小莲,白族女孩,前不久已成为女孩她妈了。初见小莲,是大学报道第一天,似乎什么都变得不顺利了当爸爸说他要回家的那一...
    燊儿阅读 255评论 0 0