react源码阅读笔记(2)组件的渲染

本文使用的是react 15.6.1的代码

ReactDOM.render


class App extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        let {text} = this.props;
        return <div id="app">
            <span>{text}</span>
        </div>
    }
}
App.defaultProps = {
    text: 'hello react'
};



ReactDOM.render((
    <App/>
), document.getElementById('root'));

相信大家在初学react的时候都写过类似上面的代码,上篇文章中,已经介绍了 extends React.Component做了些什么,这次,我们就来看看ReactDOM.render中react去做了些什么,ReactDOM.render实际调用的是ReactMount.js中的render
代码地址:[ReactMount]

// nextElement即ReactElement,
// container 具体容器,将由virtualDOM生成的真实dom映射的位置
// callback 渲染成功后的回调
  render: function(nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement,
      container,
      callback,
    );

进一步得知,其主要逻辑位于_renderSubtreeIntoContainer下
代码地址:[ReactMount]

// ReactDom.render调用后 第一个参数默认传递null,因为该方法第一个参数是父组件,剩下三个参数和render函数一致
  _renderSubtreeIntoContainer: function(
    parentComponent,
    nextElement,
    container,
    callback,
  ) {
    //判断callback是否为函数;
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');


    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement,
    });
//通过TopLevelWrapper创建一个ReactElement节点,并且设置其this.props.child =     render传入的ReactElement    
     /** 上文TopLevelWrapper代码
     var topLevelRootCounter = 1;z
     var TopLevelWrapper = function() {
      this.rootID = topLevelRootCounter++;
    };
     TopLevelWrapper.prototype.isReactComponent = {};
     TopLevelWrapper.prototype.render = function() {
      return this.props.child;
    };
     TopLevelWrapper.isReactTopLevelWrapper = true;
     */
      // 可以看出TopLevelWrapper代码就是一个简单的ReactComponent,类似于 extend React.Component, 并重写了方法render
    

    var nextContext;
    // 如果存在父组件,即不是顶级组件的情况下(在ReactDOM.render时,parentComponent为null)
    if (parentComponent) {
      var parentInst = ReactInstanceMap.get(parentComponent);
      nextContext = parentInst._processChildContext(parentInst._context);
    } else {
      nextContext = emptyObject;
    }
    // 这时候preComponent = null;
    var prevComponent = getTopLevelWrapperInContainer(container);
    if (prevComponent) {
      //
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      // diff 简单概括就是如果渲染的节点和原节点type和key(所以像listview可以通过设置key来进行优化)都不变的时候,直接更新就好,不用在去重新渲染一遍
      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);
      }
    }

    // 获取container的跟元素
    var reactRootElement = getReactRootElementInContainer(container);
    // 确定container是否被markup,即添加了data-reactid,第一次渲染肯定是false
    var containerHasReactMarkup =
      reactRootElement && !!internalGetID(reactRootElement);
    // 目前为false,因为ReactDOM.render调用时还没有实例化任何组件
    var containerHasNonRootReactChild = hasNonRootReactChild(container);


    // 目前为false
    var shouldReuseMarkup =
      containerHasReactMarkup &&
      !prevComponent &&
      !containerHasNonRootReactChild;
    // 关键代码,渲染,插入都在这里面
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      nextContext,
    )._renderedComponent.getPublicInstance();
    if (callback) {
      callback.call(component);
    }
    return component;
  }

根据代码我们知道,在_renderSubtreeIntoContainer方法的时候,他会优先判断渲染节点和原节点的type和key是否一致,如果一致,直接调用_updateRootComponent更新,否则才会去重新render新的组件,因此在渲染listview等需要大量刷新的组件时,可以通过设置key去优化显示,减少重新渲染

在看看_renderNewRootComponent的代码

/**
   * Render a new component into the DOM. Hooked by hooks!
   *
   * @param {ReactElement} nextElement 即将渲染的组件
   * @param {DOMElement} container 容器元素
   * @param {boolean} shouldReuseMarkup 是否需要重新标记元素
   * @return {ReactComponent} nextComponent 返回一个ReactComponent
   */
  _renderNewRootComponent: function(
    nextElement,
    container,
    shouldReuseMarkup,
    context,
  ) {
     //主要和滚动条有关,目前不需要太关心
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    //实例化React Component,
    var componentInstance = instantiateReactComponent(nextElement, false);
    /*
     上文instantiateReactComponent
     function instantiateReactComponent(node, shouldHaveDebugID) {
     var instance;

     if (node === null || node === false) {
     // 如果是空对象
     instance = ReactEmptyComponent.create(instantiateReactComponent);
     } else if (typeof node === 'object') {
     // 如果是Node,(包括dom节点以及reactElement)
     var element = node;

     // 原生对象
     if (typeof element.type === 'string') {
     instance = ReactHostComponent.createInternalComponent(element);
     } else {
     // react组件
     instance = new ReactCompositeComponentWrapper(element);
     }
     //如果元素本来就是一个string或者number,如 <div>111</div>中的111
     } else if (typeof node === 'string' || typeof node === 'number') {
     //创建一个
     instance = ReactHostComponent.createInstanceForText(node);
     }

     //这两个参数用于dom 和 art diff算法
     instance._mountIndex = 0;
     instance._mountImage = null;
     return instance;
     }
     */


    /* 批量更新方法,具体实现可以见 ReactDefaultBatchingStrategy.js中 batchedUpdate方法,实际就是执行
     * batchedMountComponentIntoNode方法,将后面的参数传入batchedMountComponentIntoNode中

     */
    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },

batchedMountComponentIntoNode中,使用了React中大量使用的事务机制,调用了mountComponentIntoNode方法,事务机制后面有空在来研究,我们直接看mountComponentIntoNode


function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context,
) {
 /*调用刚才ReactComponent instance中mountComponent方法,将React组件解析成对应的html(对应不同ReactComponent instance)mountComponent也是不同的
   <div>hello react</div>, 对应的是ReactDOMTextComponent,最终解析成的HTML为<div data-reactroot="x.x.x">hello react</div>
   */
  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
    0 /* parentDebugID */,
  );

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  /**
   * 将解析出来的html插入到dom中
   */
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  );
}

看看ReactReconciler中的mountComponent代码

mountComponent: function(
    internalInstance,
    transaction,
    hostParent,
    hostContainerInfo,
    context,
    parentDebugID, // 0 in production and for roots
  ) {
    var markup = internalInstance.mountComponent(
      transaction,
      hostParent,
      hostContainerInfo,
      context,
      parentDebugID,
    );
    if (
      internalInstance._currentElement &&
      internalInstance._currentElement.ref != null
    ) {
      transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
    }
    return markup;
  },

由代码可以知道,其实本质也是调用了ReactComponent实例的mountComponent,刚才我们说到,不同的ReactComponent实例会有不同的mountComponent方法
大体有这三种实例:

  1. ReactHostComponent.createInternalComponent(element); 原生组件
  2. new ReactCompositeComponentWrapper(element); 自定义React组件
  3. ReactHostComponent.createInstanceForText(node); 字符串/数字

我们看一下最复杂的自定义React组件的mountComponent方法

mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
    this._context = context;
    this._mountOrder = nextMountID++;
    this._hostParent = hostParent;
    this._hostContainerInfo = hostContainerInfo;

    var publicProps = this._currentElement.props;
    var publicContext = this._processContext(context);

    var Component = this._currentElement.type;

    var updateQueue = transaction.getUpdateQueue();

    // Initialize the public class a
    var doConstruct = shouldConstruct(Component);
    // 本质上就是new 了一个React.createClass(), 即Component组件,代码可以自己看看,比较简单
    var inst = this._constructComponent(
      doConstruct,
      publicProps,
      publicContext,
      updateQueue,
    );
    var renderedElement;

    // 当不存在构造函数,并且没有new出来的组件实例是null或者组件实例没有render方法,那么可以认为这是一个无状态组件
    if (!doConstruct && (inst == null || inst.render == null)) {
      renderedElement = inst;
      warnIfInvalidElement(Component, renderedElement);
      // new 一个无状态组件
      inst = new StatelessComponent(Component);
      this._compositeType = CompositeTypes.StatelessFunctional;
    } else {
      if (isPureComponent(Component)) {
        this._compositeType = CompositeTypes.PureClass;
      } else {
        this._compositeType = CompositeTypes.ImpureClass;
      }
    }

    // simpler class abstractions, we set them up after the fact.
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;

    this._instance = inst;

    // Store a reference from the instance back to the internal representation
    ReactInstanceMap.set(inst, this);

    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }

    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;

    var markup;
    if (inst.unstable_handleError) {
      markup = this.performInitialMountWithErrorHandling(
        renderedElement,
        hostParent,
        hostContainerInfo,
        transaction,
        context,
      );
    } else {
      markup = this.performInitialMount(
        renderedElement,  // 感谢钟佳锋同学的指正,这里的renderedElement是后面_renderValidatedComponent中调用内部的render方法获取
        hostParent,
        hostContainerInfo,
        transaction,
        context,
      );
    }

    if (inst.componentDidMount) { //生命周期componentDidMount
      transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }

    return markup;
  },

从上面我们知道自定义React组件真正实例化(new)的时候就在mountComponment之中,但是这只是简单的实例化,返回值html是在performInitialMount中处理的,在看看performInitialMount中的代码

performInitialMount: function(
    renderedElement,
    hostParent,
    hostContainerInfo,
    transaction,
    context,
  ) {
    var inst = this._instance;

    var debugID = 0;

    if (inst.componentWillMount) {
      //该实体如果有componentWillMount方法,生命周期之一
      inst.componentWillMount();
      // When mounting, calls to `setState` by `componentWillMount` will set
      // `this._pendingStateQueue` without triggering a re-render.
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }

    // If not a stateless component, we now render
    // 获取子组件,实际是调用React.createClass中的render方法,因为render方法会返回一个ReactElement对象
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }

    var nodeType = ReactNodeTypes.getType(renderedElement);
    this._renderedNodeType = nodeType;
    //再次调用instantiateReactComponent初始化
    var child = this._instantiateReactComponent(
      renderedElement,
      nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
    );
    this._renderedComponent = child;
    
    //再一次调用mountComponent
    var markup = ReactReconciler.mountComponent(
      child,
      transaction,
      hostParent,
      hostContainerInfo,
      this._processChildContext(context),
      debugID,
    );


    return markup;
  },

在这段代码中,renderedElement被_renderValidatedComponent重新进行了赋值,根据后面child这个变量命名,我们猜测,这个方法会返回子组件,最后,又在一次调用了ReactReconciler.mountComponent方法,和mountComponentIntoNode方法调用手段一致,只是第一个参数变成了child,简单猜测一下,这个时候在渲染子组件。为了证实猜测,在来看看_renderValidatedComponent方法

_renderValidatedComponent: function() {
    var renderedElement;
    renderedElement = this._renderValidatedComponentWithoutOwnerOrContext();

    return renderedElement;
  },

对应的_renderValidatedComponentWithoutOwnerOrContext方法

/**
   * @protected
   */
  _renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    var renderedElement;
    //回想一下我们React.createClass中render方法里面被babel解析后是什么
    //render中{return React.createElement}
    //所以他又是一个ReactElement
    renderedElement = inst.render();

    return renderedElement;
  },

果然,是调用了render方法,也就是render中的子组件。随后组件又调用了_instantiateReactComponentfan方法初始化最后再一次mountComponent去实例化子组件,形成递归去渲染,直到得到最后的markup,同时不难想象,随着不断的递归只有下面的2类会返回正常的mask对象

  1. ReactHostComponent.createInternalComponent(element); 原生组件
  2. ReactHostComponent.createInstanceForText(node); 字符串/数字

我们在来看看createInternalComponent方法,该方法会new一个ReactDOMComponent(指代原生组件),我们来看看ReactDOMComponent的mountComponent有什么

mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
    this._rootNodeID = globalIdCounter++;
    this._domID = hostContainerInfo._idCounter++;
    this._hostParent = hostParent;
    this._hostContainerInfo = hostContainerInfo;

    var props = this._currentElement.props;
    //针对不同的element,进行不同的处理,在构造函数中this._tag=element.tag.toLowCase()
    switch (this._tag) {
      case 'audio':
      case 'form':
      case 'iframe':
      case 'img':
      case 'link':
      case 'object':
      case 'source':
      case 'video':
        this._wrapperState = {
          listeners: null,
        };
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
      case 'input':
        ReactDOMInput.mountWrapper(this, props, hostParent);
        props = ReactDOMInput.getHostProps(this, props);
        transaction.getReactMountReady().enqueue(trackInputValue, this);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
      case 'option':
        ReactDOMOption.mountWrapper(this, props, hostParent);
        props = ReactDOMOption.getHostProps(this, props);
        break;
      case 'select':
        ReactDOMSelect.mountWrapper(this, props, hostParent);
        props = ReactDOMSelect.getHostProps(this, props);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
      case 'textarea':
        ReactDOMTextarea.mountWrapper(this, props, hostParent);
        props = ReactDOMTextarea.getHostProps(this, props);
        transaction.getReactMountReady().enqueue(trackInputValue, this);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
    }

    assertValidProps(this, props);

    // We create tags in the namespace of their parent container, except HTML
    // tags get no namespace.
    var namespaceURI;
    var parentTag;
    if (hostParent != null) {
      namespaceURI = hostParent._namespaceURI;
      parentTag = hostParent._tag;
    } else if (hostContainerInfo._tag) {
      namespaceURI = hostContainerInfo._namespaceURI;
      parentTag = hostContainerInfo._tag;
    }
    if (
      namespaceURI == null ||
      (namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject')
    ) {
      namespaceURI = DOMNamespaces.html;
    }
    if (namespaceURI === DOMNamespaces.html) {
      if (this._tag === 'svg') {
        namespaceURI = DOMNamespaces.svg;
      } else if (this._tag === 'math') {
        namespaceURI = DOMNamespaces.mathml;
      }
    }
    this._namespaceURI = namespaceURI;

    var mountImage;
    // 调用render时,useCreateElement为true
    if (transaction.useCreateElement) {
      var ownerDocument = hostContainerInfo._ownerDocument;
      var el;
      if (namespaceURI === DOMNamespaces.html) {
        if (this._tag === 'script') {
          // Create the script via .innerHTML so its "parser-inserted" flag is
          // set to true and it does not execute
          var div = ownerDocument.createElement('div');
          var type = this._currentElement.type;
          div.innerHTML = `<${type}></${type}>`;
          el = div.removeChild(div.firstChild);
        } else if (props.is) {
          el = ownerDocument.createElement(this._currentElement.type, props.is);
        } else {
          // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
          // See discussion in https://github.com/facebook/react/pull/6896
          // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
          // 创建标签
          el = ownerDocument.createElement(this._currentElement.type);
        }
      } else {
        el = ownerDocument.createElementNS(
          namespaceURI,
          this._currentElement.type,
        );
      }
      ReactDOMComponentTree.precacheNode(this, el);
      this._flags |= Flags.hasCachedChildNodes;
      if (!this._hostParent) {
        DOMPropertyOperations.setAttributeForRoot(el);
      }
      this._updateDOMProperties(null, props, transaction);
      var lazyTree = DOMLazyTree(el);
      // 准备实例化子组件
      this._createInitialChildren(transaction, props, context, lazyTree);
      mountImage = lazyTree;
    } else {
      var tagOpen = this._createOpenTagMarkupAndPutListeners(
        transaction,
        props,
      );
      var tagContent = this._createContentMarkup(transaction, props, context);
      if (!tagContent && omittedCloseTags[this._tag]) {
        mountImage = tagOpen + '/>';
      } else {
        mountImage =
          tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
      }
    }

    switch (this._tag) {
      case 'input':
        transaction.getReactMountReady().enqueue(inputPostMount, this);
        if (props.autoFocus) {
          transaction
            .getReactMountReady()
            .enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'textarea':
        transaction.getReactMountReady().enqueue(textareaPostMount, this);
        if (props.autoFocus) {
          transaction
            .getReactMountReady()
            .enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'select':
        if (props.autoFocus) {
          transaction
            .getReactMountReady()
            .enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'button':
        if (props.autoFocus) {
          transaction
            .getReactMountReady()
            .enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'option':
        transaction.getReactMountReady().enqueue(optionPostMount, this);
        break;
    }

    return mountImage;
  }

由代码可以知道react通过type对input,select,script等特殊元素进行单独的处理,最后调用createElement来生成一个普通的元素,同时调用了this._createInitialChildren(transaction, props, context, lazyTree)方法去实例化其中的子组件,同原生组件一下,形成递归返回到tree中,最后mountImage = lazyTree;拿到生成的节点,这里就不在看原生组件_createInitialChildren的实现了

回到开mountCompouentIntoNode的代码,最后调用了ReactMount._mountImageIntoNode方法,这个方法就是真真的把节点插入带容器中去了,看一下实现;

_mountImageIntoNode: function(
    markup,
    container,
    instance,
    shouldReuseMarkup,
    transaction,
  ) {

    //暂时不用关心
    if (shouldReuseMarkup) {
     ......
    }

    //render的时候该值为true
    if (transaction.useCreateElement) {
      //移除container中的节点
      while (container.lastChild) {
        container.removeChild(container.lastChild);
      }
      DOMLazyTree.insertTreeBefore(container, markup, null);
    } else {
      setInnerHTML(container, markup);
      ReactDOMComponentTree.precacheNode(instance, container.firstChild);
    }
  },

最后实际上是调用DOMLazyTree.insertTreeBefore完成节点插入,这一段代码很简单,就不在单独分析了

总结

终于走完了React整个渲染流程,我们可以发现React抽象程度非常的高,源码读起来特别的复杂,这里也只是简单的阅读整个流程,细节太多,没有深究

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

推荐阅读更多精彩内容

  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,813评论 1 18
  • It's a common pattern in React to wrap a component in an ...
    jplyue阅读 3,260评论 0 2
  • [原创] 作者/张庆九 我把心给了你 你说爱是什么 我说爱就是把心给了你 你说爱是什么 我说爱就是让你幸...
    9陆海空9阅读 590评论 4 14
  • 意识:人的头脑对客观物质世界的反映,是各种心理过程的总和,是觉察、是感觉、佛教六识之一,心理学的定义是人所特有的一...
    梦的精灵解梦客阅读 289评论 0 0