react源代码重点难点分析

时间:2023-03-09 04:27:41
react源代码重点难点分析

网上已经有不少react源码分析文档,但都是分析主流程和主要功能函数,没有一个是从reactDOM.render()入口开始分析源码把流程走通尤其是把复杂重要的细节环节走通直到把组件template编译插入网页生效。

react源码设计太复杂,react的缺点就是把事情搞太复杂了,相比之下vue用简单的编程方法也能实现同样的功能甚至更多的功能,可以说是后起之秀超越了前辈,个人设计超过了专业团队设计。

react源码分布在几十个文件中,找函数代码要在这批文件中搜索出来,这无所谓,其中最重要的源码文件如下:

React.js

ReactClass.js

ReactElement.js

ReactDOM.js

ReactMount.js

instantiateReactComponent.js

ReactComponent.js

ReactDOMComponent.js

ReactCompositeComponent.js

ReactReconciler.js

ReactUpdates.js

Transaction.js

还有react-redux两个文件:

connect.js

provider.js

首先描述一下测试项目基本环境和细节,测试项目采用minimum项目,只有一个/路由组件,显示<div>hello world</div>,如果ajax从后台获取到数据,则显示从后台获取的一个字符串,组件使用redux的store数据。

react项目入口代码:
ReactDOM.render(
<Provider store={store}>
<Router history={history} children={routes} />
</Provider>,
MOUNT_NODE

路由routes.js代码:

import { injectReducer } from 'REDUCER'
import createContainer from 'UTIL/createContainer'

const connectComponent = createContainer(
  ({ userData, msg }) => ({ userData, msg }),
  require('ACTION/msg').default
)

export default {
  path: '/',

  getComponent (nextState, cb) {
    require.ensure([], (require) => {
      injectReducer('msg', require('REDUCER/msg/').default)
        cb(null, connectComponent(require('COMPONENT/App').default))
      }, 'App')
    }
}

/路由组件App.js代码:

import React, { Component } from 'react'

export default class App extends Component {
componentWillMount () {
  let _this = this
  setTimeout(function() {
    _this.props.fetchMsg()
  }, 2000)
}
componentWillReceiveProps (nextProps) {
  console.log(this)
}
render () {
  return (
    <div>{ this.props.msg.msgs[0] ? this.props.msg.msgs[0].content : 'hello world' }</div>
  )
}
}

react项目入口方法是:
ReactDOM.render(根组件template)

相当于vue 1.0的router.start(app)或vue 2.0的new Vue(app)。

ReactDOM.render方法代码:
function (nextElement, container, callback) {
  return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
},

_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
  var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement); // 构造一个Element,第一次是构造toplevel Element
  //React.createElement创建ReactElement对象(vnode),含有type,key,ref,props属性,这个过程中会调用getInitialState()初始化state。
  var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
  return component;

产生的component就是根组件template中第一个标签provider组件实例,里面有层层子节点,有/路由组件实例,有props属性。
那么是从这里开始层层递归到/路由组件节点的。

_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
  //nextelement就是根元素节点provider组件元素
  var componentInstance = instantiateReactComponent(nextElement, false); //element -> instance

    function instantiateReactComponent(node, shouldHaveDebugID) { //这个方法是根据Element产生warpper instance,再执行instance.mountComponent开始编译组件节点
      //instance = new ReactCompositeComponentWrapper(element); //这句相当于是下面一句
      instance = new this.construct(element); // 这是instance的构造函数,就是设置一些属性,很普通
        var ReactCompositeComponentMixin = { // 这是instace构造函数代码
          construct: function (element) {
            this.xxx = yyy;
          }
        }
      return instance;
    //instantiateReactComponent根据ReactElement的type分别创建ReactDOMComponent, ReactCompositeComponent,ReactDOMTextComponent等对象,

    //不同的对象instance有不同的mountComponent方法,所以react源码文件有无数个mountComponent函数,其实html元素节点可以不当做component处理
    //instantiateReactComponent被调用执行多次,每次被调用就处理一个元素节点产生一个instance,在本例minimum项目中,产生如下instance:
_instance:TopLevelWrapper
_instance:Provider // provide节点
_instance:Constructor // router节点
_instance:Constructor
_instance:Connect // /路由组件外层
_instance:App //这是/路由组件实例


  ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context); //从根元素provider开始编译处理

至此处理结束,根组件template已经插入网页生效,已经从根元素递归处理到最底层元素,所以处理root元素很简单,复杂在于从root元素递归处理子节点再层层返回。

batchedUpdates会调用batchedMountComponentIntoNode,从这个函数开始追踪:

function batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context) { //从根元素provider开始
  //batchedMountComponentIntoNode以transaction事务的形式调用mountComponentIntoNode
  transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);

    function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReu //从根元素provider开始
      var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, //mountComponent相当于compile,从根节点开始编译
      //Reconciler.mountComponent就是执行instance里面的mountComponent,在执行performInitialMount时会递归调用自己。
      //mountComponent的目的是解析得到每个节点的HTML代码(最后插入网页生效),react叫做markup,是类似vue的vnode对象。

        Reconciler.mountComponent: function (internalInstance, transaction, 
          var markup = internalInstance.mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
          //对于provider这样的控制组件标签来说,产生的html代码就是一个不可见的comment文本
          return markup;
          // internalInstance是不同的component实例,最典型的component节点类型是html元素节点比如<div>和组件元素节点比如<App>

        Reconciler.mountComponent会递归调用自己完成从根元素递归到最底层元素<div>,是react源码的最核心最关键最牛的代码,因为前端代码要递归html元素tree,这是与后台代码不同的,也是非常复杂的,一旦递归元素tree,就要开始晕菜了,但代码效率巨高,一个递归就完事了,递归也是最体现程序牛的地方,人工智能自我学习肯定也是要用程序递归技术的。

不同的instance有不同的mountComponent方法,我们先来看组件元素的mountComponent方法:

mountComponent: function (transaction, hostParent, hostContainerInfo, context) { //mountComponent其实就是编译节点的意思,react把一切节点视为component
  var publicProps = this._currentElement.props;
  var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);
  //inst就是new组件实例,那么是在mountComponent阶段初始化组件实例的,new组件实例之后,执行其render()方法就又产生Element,就又需要递归循环element -> instance -> instance.mountComponent -> inst(组件实例) -> render() -> element 如此递归到子节点
    _constructComponent: function (doConstruct, publicProps, publicContext, updateQueue) {
      return this._constructComponentWithoutOwner(doConstruct, publicProps, publicContext, updateQueue);

        _constructComponentWithoutOwner: function (doConstruct, publicProps, publicContext, updateQueue) {
          var Component = this._currentElement.type; //type就是组件构造函数(组件定义代码)
          return new Component(publicProps, publicContext, updateQueue); //App组件外套connect组件,这就是new Connect()组件实例的位置,找到这个位置在分析react源码的道路上就前进了一大步,因为组件定义代码无外乎就是定义一些属性,框架肯定准备了一个组件基类,到时一合并,再new实例,这是js唯一的机制,不可能有其它方法,找到这个位置,再前后去追踪,就能看懂框架到底是如何初始化组件实例的,我们定义的组件代码到底到底是如何被执行的。

  inst.props = publicProps; // double set this.props
  this._instance = inst; //new组件实例保存在instance中,只要执行instance的方法,就可以从this取回inst实例,再执行inst实例里面的render()方法产生一个Element递归下去
  markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
  //inst保存在this实例中,调用performInitialMount时无需传递inst,renderedElement是空的
  return markup; //markup就是编译产生的结果,相当于vnode,含html代码

    performInitialMount: function (renderedElement, hostParent, hostContainerInfo, transaction, context) {

      renderedElement = this._renderValidatedComponent(); //执行inst.render()产生Element,每种组件inst有自己的render方法,provder/router/connect/app组件都有自己的render方法,app的render方法是应用写的,系统组件的render方法都是事先设计好的,比如connect的render方法,还有一个router-context组件
//app的render方法里面是jsx语法,编译时每个节点已经转换为createElement(),所以render方法就是返回一个根元素Element,它里面有多少子元素再递归处理

      var child = this._instantiateReactComponent(renderedElement,    //根据元素类型生成instance
      this._renderedComponent = child;
      var markup = ReactReconciler.mountComponent(child, // 在这里递归调用Reconciler.mountComponent,处理下一个子节点child,是前面根据Element生成的
      return markup;

这是组件component的mountComponet编译方法,再来看html元素component的mountComponent编译方法:

//ReactDOMComponent是针对html元素,在这个minimum项目中,根组件template中只有一个<div>元素节点
ReactDOMComponent.Mixin = {
  mountComponent: function (transaction, hostParent, hostContainerInfo, context) { //只针对<div>执行一次
    var tagContent = this._createContentMarkup(transaction, props, context);

      _createContentMarkup: function (transaction, props, context) {
        var mountImages = this.mountChildren(childrenToUse, transaction, context);

          mountChildren: function (nestedChildren, transaction, context) {
            var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);

              _reconcilerInstantiateChildren: function (nestedChildren, transaction, context) {
                return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);

                  instantiateChildren: function (nestedChildNodes, transaction, context, ) {
                    return instantiateChild(childInsts, child, name, selfDebugID);

                      function instantiateChild(childInstances, child, name, selfDebugID) {
                        childInstances[name] = instantiateReactComponent(child, true);

再回到mountComponentIntoNode看Reconciler.mountComponent从根元素provider开始递归编译子节点之后再执行什么:

function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReu 
  var markup = ReactReconciler.mountComponent(wrapperInstance, transaction,

  ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
  //这是递归完成之后执行的方法,就是把编译结果插入网页生效,编译结果就是一个<div>字符串</div>,具体的方法无非是用innerHTML/Context方法,或者用appendChild方法,本文忽略

  

至此可以小结一下从根Element开始的处理流程:
Element -> wrapper instance -> instance.mountComponent(compile) -> new instance._currentElement.type()组件实例 -> 组件实例.render()产生Element -> 递归子节点

元素树结构:
provider->router->connect->app

每层元素根据类型创建instance,执行其mountComponent,再new 组件实例inst,再执行组件实例inst的render()方法产生一个element,再递归。

connect的render方法:
Connect.prototype.render = function render() {
this.renderedElement = (0, _react.createElement)(WrappedComponent, this.mergedProps);

当store中的属性变化触发执行connect组件的render方法时,可以看到,它产生的Element是App组件元素,那么递归编译处理就是编译App组件,就是更新App组件,connect是App的代理组件,App组件并没有定义props属性,也没有定义如何更新。

router的render方法:
render: function render(props) {
return _react2.default.createElement(_RouterContext2.default, props);
}
router也会创建一个element,带路由参数。