React标准参考教程(alpha)

React15源码解析(v15.6.2)

React v16新功能

  1. render 函数支持返回数组和字符串:终于不需要再将多个同级元素包裹在一个冗余的 DOM 元素中了,但每个同级元素还是需要唯一的 key 值方便 react 进行更新。而且在未来,react 可能还会提供一个特殊的 jsx 片段来支持无 key 值的 DOM 元素。

  2. 更好的异常处理:在老版本的 react 中,某个组件在 render 阶段的运行错误可能会 break 掉整个应用,而且抛出的异常信息含义也非常模糊,难以确定错误的发生位置。在 v16.0 中,如果某个组件在执行 render 或其他生命周期函数时出错,整个组件将被从根节点上移除掉,方便开发者快速定位异常组件。在定位到异常组件后,开发者可以为该组件添加 componentDidCatch 方法,并在这个方法中为组件定义一个备用视图用于渲染异常状态下的组件。当然,在这个新的生命周期函数中,开发者也可以获得更加有帮助的错误信息进行 debug。这被称作组件的错误边界,大家可以理解为组件层面的 try catch 声明。

  3. 新的组件类型 portals:ReactDOM.createPortal(child, container) 可以将子组件直接渲染到当前容器组件 DOM 结构之外的任意 DOM 节点中,这将使得开发对话框,浮层,提示信息等需要打破当前 DOM 结构的组件更为方便。

  4. 更好的服务端渲染:与之前 renderToString 方法不同,新版本提供的 renderToNodeStream 将返回 Readable,可以持续产生字节流(a stream of bytes)并在下一部分的 document 生成之前将之前已生成的部分 document 传回给客户端。通常来讲,新的服务端渲染将比老的快3倍以上。在 document 到达客户端之后,新版本的 react 也将不会再去将客户端的初次渲染结果与服务端的渲染结果进行比较,而是尽可能地去重用相同的 DOM 元素。

  5. 支持自定义 DOM 元素:新版本将不会再抛出不支持的 DOM 元素错误,而是将所有开发者自定义的 DOM 元素都传递到相应的 DOM 节点上。

  6. 更小的打包大小:总体体积减少 30%

  • react is 5.3 kb (2.2 kb gzipped), 老版本 20.7 kb (6.9 kb gzipped)
  • react-dom is 103.7 kb (32.6 kb gzipped), 老版本 141 kb (42.9 kb gzipped)
  • react + react-dom is 109 kb (34.8 kb gzipped), 老版本 161.7 kb (49.8 kb gzipped)
  1. MIT 许可:除了最新的 16.0 版本外,Facebook 还发布了使用 MIT 许可的 15.6.2 版本,以方便无法立刻升级的使用者。

  2. 新的核心架构 Fiber:新版本将使用 Fiber 作为底层架构。正是得益于 FIber,上述提到的支持返回数据及错误边界等功能才变得可能。Fiber 相较于之前最大的不同是它可以支持异步渲染(async rendering),这意味着 React 可以在更细的粒度上控制组件的绘制过程,从最终的用户体验来讲,用户可以体验到更流畅交互及动画体验。而因为异步渲染涉及到 React 的方方面面甚至未来,在 16.0 版本中 React 还暂时没有启用,并将在未来几个月陆续推出。

React16源码解析(v16.4.1)

组件构成

1、组件

我们以react脚手架初始的App组件为例:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

为了弄清楚<App />是什么?我们将App组件打印一下:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

console.log(<App />);

export default App;

由上得知,<App />其实是js对象而不是真实的DOM,注意此时props是空对象。接下来,我们打印<App><div>这是App组件</div></App>,看看控制台会输出什么:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

console.log(<App><div>这是App组件</div></App>);

export default App;

我们看到,props发生了变化,由于<App />组件中嵌套了一个divdiv中又包含文本,所以在描述<App />对象的props中增加了children属性,其值为描述divjs对象。同理,如果我们进行多层的组件嵌套,其实就是在父对象的props中增加children字段及对应的描述值,也就是js对象的多层嵌套。

以上描述是基于ES6的React开发模式,其实在ES5中通过React.createClass({})方法创建的组件,与ES6中是完全一样的,同样可以通过控制台打印输出组件结果进行验证。

那么形如HTML标签实际上却是对象的React组件是如何构成的呢?

因为我们的组件声明基于ReactComponent,所以首先我们看看入口文件react/index.js,可以看到如下代码:

const React = require('./src/React');

// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest.
module.exports = React.default ? React.default : React;

./src/React.js如下:

// React.js
const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
  unstable_Profiler: REACT_PROFILER_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    ReactCurrentOwner,
    // Used by renderers to avoid bundling object-assign twice in UMD bundles:
    assign,
  },
};

if (enableSuspense) {
  React.Placeholder = REACT_PLACEHOLDER_TYPE;
}

if (__DEV__) {
  Object.assign(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, {
    // These should not be included in production.
    ReactDebugCurrentFrame,
    // Shim for React DOM 16.0.0 which still destructured (but not used) this.
    // TODO: remove in React 17.0.
    ReactComponentTreeHook: {},
  });
}

export default React;

我们在import React from 'react'时,引入的就是源码中提供的React对象。在extends Component时,继承了Component类。这里需要说明两点:

  • 源码中明明使用的module.exports而不是export default,为什么还能够成功引入呢?其实这是babel解析器的功劳。它令(ES6)import === (CommonJS)require。而在typescript中,需要严格的export default声明,故在typescript下就不能使用import React from 'react'了,有兴趣的读者可以尝试一下。

  • 我们可以写extends Component也可以写extends React.Component,这两者是否存在区别呢?答案是否定的。因为ComponentReact.Component的引用。也就是说Component === React.Component,在实际项目中写哪个都可以。

沿着React.Component的线索,我们打开packages/react/src/ReactBaseClasses.js:

/**
 * Base class helpers for the updating state of a component.
 */
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

export {Component, PureComponent};

React.Component是一个构造函数,同时我们也注意到setState是定义在原型上具有两个参数的方法。上述代码表明,我们在最开始声明的组件App,其实是继承React.Component类的子类,它的原型具有setState等方法。这样组件App已经有了最基本的雏形。

小结:

2、组件的初始化

声明组件App后,我们可以在其内部自定义方法,也可以使用生命周期的方法,如ComponentDidMount等等,这些和我们在写"类"的时候是完全一样的。唯一不同的是组件类必须拥有render方法输出类似<div>这是App组件</div>的结构并挂载到真实DOM上,才能触发组件的生命周期并成为DOM树的一部分。首先我们观察ES6的"类"是如何初始化一个react组件的。

将最初的示例代码放入babel中:

var App = function (_Component) {
  _inherits(App, _Component);

  function App() {
    _classCallCheck(this, App);

    return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).apply(this, arguments));
  }

  _createClass(App, [{
    key: "render",
    value: function render() {
      return React.createElement(
        "div",
        { className: "App" },
        React.createElement(
          "header",
          { className: "App-header" },
          React.createElement("img", { src: logo, className: "App-logo", alt: "logo" }),
          React.createElement(
            "h1",
            { className: "App-title" },
            "Welcome to React"
          )
        ),
        React.createElement(
          "p",
          { className: "App-intro" },
          "To get started, edit ",
          React.createElement(
            "code",
            null,
            "src/App.js"
          ),
          " and save to reload."
        )
      );
    }
  }]);

  return App;
}(Component);

其中_Component是对象React.Component_inherit方法是extends关键字的函数实现。关键在于我们发现render方法实际上是调用了React.createElement方法(实际是ReactElement方法)。然后我们打开ReactElement.js:

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

看到这里我们发现,其实每一个组件对象都是通过React.createElement方法创建出来的ReactElement类型的对象。换句话说,ReactElment是一种内部记录组件特征并告诉React你想在屏幕上看到什么的对象。在ReactElement中:

参数功能
$$typeof组件的标识信息
keyDOM结构标识,提升update性能
props子结构相关信息(有则增加children字段/没有为空)和组件属性(如style)
ref真实DOM的引用
_owner_owner === ReactCurrentOwner.current(ReactCurrentOwner.js),值为创建当前组件的Fiber对象,默认值为null

看完上述内容相信大家已经对React组件的实质有了一定的了解。通过执行React.createElement创建出的ReactElement类型的js对象,就是"React组件",这与控制台打印出的结果完全对应。总结来说,如果我们通过class关键字声明React组件,那么他们在解析成真实DOM之前一直是ReactElement类型的js对象。

小结

组件的挂载

我们都知道可以通过ReactDOM.render(component, mountNode)的形式对自定义组件/原生DOM/字符串进行挂载,那么挂载的过程又是如何实现的呢?

React16中,虽然也是通过JSX编译得到一个虚拟DOM对象,但对这些虚拟DOM对象的再加工则是经过翻天覆地的变化。我们需要追根溯底,看它是怎么一步步转换过来的。我们找到ReactDOM.render。在packages/react-dom/src/ReactDOM.js的源码里,有三个类似的东西:

const ReactDOM: Object = {
  //新API,代替render
  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      true,
      callback,
    );
  },
  // React15的重要API,逐渐退出舞台
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
  // 用于生成子树,(不稳定api)
  unstable_renderSubtreeIntoContainer(
    parentComponent: React$Component<any, any>,
    element: React$Element<any>,
    containerNode: DOMContainer,
    callback: ?Function,
  ) {
    invariant(
      parentComponent != null && ReactInstanceMap.has(parentComponent),
      'parentComponent must be a valid React Component',
    );
    return legacyRenderSubtreeIntoContainer(
      parentComponent,
      element,
      containerNode,
      false,
      callback,
    );
  },
  unmountComponentAtNode(container: DOMContainer) {
    // ...
    return false;
  }
}

其实ReactDOM.render/hydrate/unstable_renderSubtreeIntoContainer/unmountComponentAtNode都是legacyRenderSubtreeIntoContainer方法的加壳方法。因此ReactDOM.render实际调用了legacyRenderSubtreeIntoContainer,这是一个内部API。从字面意思可以看出它是将"子DOM"插入容器的方法,我们看下legacyRenderSubtreeIntoContainer源码实现:

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
  // 检查提供的DOM节点是有效的节点元素
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );

  if (__DEV__) {
    // 开发模式render时进行检查并提供许多有用的警告和错误提示信息
    topLevelUpdateWarnings(container);
  }

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  let root: Root = (container._reactRootContainer: any);
  if (!root) {
    // 创建ReactRoot对象
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // 首次安装不应该批处理
    DOMRenderer.unbatchedUpdates(() => {
      // 对newRoot对象进行更新
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else {
        root.render(children, callback);
      }
    });
  } else {
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // 对root对象进行更新
    if (parentComponent != null) {
      root.legacy_renderSubtreeIntoContainer(
        parentComponent,
        children,
        callback,
      );
    } else {
      root.render(children, callback);
    }
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

我们先来解析传入legacyRenderSubtreeIntoContainer的参数:

参数功能
parentComponent当前组件的父组件,第一次渲染时为null
children要插入DOM中的组件,如app
container要插入的容器,如document.getElementById('root')
forceHydrate是否需要“注水”
callback完成后的回调函数

legacyRenderSubtreeIntoContainer里面调用legacyCreateRootFromDOMContainer创建一个ReactRoot对象,然后再调用其renderlegacy_renderSubtreeIntoContainer方法,我们看一下legacyCreateRootFromDOMContainer源码:

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      if (__DEV__) {
        if (
          !warned &&
          rootSibling.nodeType === ELEMENT_NODE &&
          (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
        ) {
          warned = true;
          warning(
            false,
            'render(): Target node has markup rendered by React, but there ' +
              'are unrelated nodes as well. This is most commonly caused by ' +
              'white-space inserted around server-rendered markup.',
          );
        }
      }
      container.removeChild(rootSibling);
    }
  }
  if (__DEV__) {
    if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
      warnedAboutHydrateAPI = true;
      lowPriorityWarning(
        false,
        'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
          'will stop working in React v17. Replace the ReactDOM.render() call ' +
          'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
      );
    }
  }
  // Legacy roots are not async by default.
  const isAsync = false;
  return new ReactRoot(container, isAsync, shouldHydrate);
}

小结

React Fiber

React16启用了全新的架构,叫做Fiber,其最大的使命是解决大型React项目的性能问题,再顺手解决之前的一些痛点。

基础

调用 setState 之后发生了什么?

在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。在 React 得到元素树之后,React 会自动计算出新元素树与旧元素树的节点差异,然后根据差异对界面进行最小化更新。在diff算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而并非全部更新。

redux store取代react state合理吗?

不经思考一股脑地把数据放到 Redux store 里维护,一定是错误的做法!

参考:redux store取代react state合理吗?

React 首屏优化

1、通过webpack的UglifyJsPlugin插件对代码进行压缩

2、

路由

参考:

  1. 剖析单页面应用路由实现原理
  2. 前端路由实现与 react-router 源码分析
  3. 单页面应用路由实现原理:以 React-Router 为例

状态管理框架

MobX

参考:

  1. MobX 核心源码解析

Flux

参考:

  1. 图解 Flux
  2. 如何理解 Facebook 的 flux 应用架构?
  3. Flux架构模式

开发问题

(ReactDOM as any).hydrate是什么?不应该是ReactDOM.render吗?

首先说一下为什么使用hydrate而不是render,这个是react 16版本中的一个变更,hydrate主要是用于给服务端渲染出的html结构进行“注水”,由于新版本中ssr出的dom节点不再带有data-react,为了能尽可能复用ssr的html内容,所以需要使用新的hydrate方法进行事件绑定等客户端独有的操作。

参考:

  1. ReactDOM - hydrate
  2. react中出现的"hydrate"这个单词到底是什么意思?
上次更新: 2018-7-16 09:34:08