Micro View of Allium

Micro View of Allium

Jeango

Jeango 2/3/2021, 3:44:07 PM

⚑ Main Concepts 基本概念

这部分结合官方文档,讲解 React 最基本的概念,分析和理解 React 基本运行流程和基本内部原理,由浅到深掌握 React 的基本实现。

作为一个大型工程,React 也使用了静态类型工具来强化 JavaScript 代码的管理,并且是 facebook 自家出品的 Flow 静态类型检查工具。React 的源码使用用了 Flow 做了静态类型检查,所以了解 Flow 有助于我们阅读源码。

Flow 与 Typescript 不同的是,它可以部分引入,不需要完全重构整个项目,所以对于一个已有一定规模的项目来说,迁移成本更小,也更加可行。除此之外,Flow 可以提供实时增量的反馈,通过运行 Flow server 不需要在每次更改项目的时候完全从头运行类型检查,提高运行效率。可以简单总结为:对于新项目,可以考虑使用 TypeScript 或者 Flow,对于已有一定规模的项目则建议使用 Flow 进行较小成本的逐步迁移来引入类型检查。

Hello ReactDom.Render()

在前面 Hello Demo 的例子中,使用的是最简单的 React 例子,是直接使用 ReactDom.render() 方法渲染一个组件:

const mountNode = document.getElementById('example')
ReactDOM.render(
  <h1>Hello, world!</h1>,
  mountNode
);

// 页面存在一个容器元素 <div id="example"></div>

参考这个方法的原型,在参数提供的 container 指定一个 HTML 节点作为渲染容器,在它里面渲染传入 React 的各种元素 Element,并返回对该组件的引用,或者针对无状态组件返回 null。

ReactDOM.render(element, container[, callback])

注意 Element 的概念,像 HTML 中的 <p> 就是一个 HTMLElement,还有纯字符串也算是一种。而 React 的组件,无论是函数式组件还是类组件,按照这种带有尖括号的写法就是一种 JSX.Element。React 会在内部将它们拆解成为一组数据,用虚拟节点 VirtualNode 的形式进行跟踪维护,对应 ReactNode 对象。

如以下渲染一个 Element 数组:

ReactDOM.render([<h1>xyz</h1>,"ABC"], mountNode);

在页面容器中出现的渲染结果就是两个对应的 HTML 元素,p 段落元素和纯文本元素 PlainText:

<div id="example"><h1>xyz</h1>ABC</div>

React 会将传入的 Element 模板会在后续解析成一组数据,并供 React.createElement() 函数调用去构造出相应的 HTML 节点。

参考以下两种完全等效的示例代码,可以看到 createElement 方法使用的参数更接近真实的 HTML 节点:

// JSX style
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

// JS style
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React.createElement() 生成的的数据对象类似以下简化后的形式,这就是一个虚拟的节点数据:

// Note: this structure is simplified
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

再比如,设置样式属性:

let dstyle = { 
    background:"#222222", 
    color: "white", 
    padding: "2px 16px 16px", 
    margin: "20%", 
    textAlign: "center",
    borderRadius: "32px",
};
const element = <h1 className="greeting" style={dstyle} >Hello, world!</h1>;

得到的数据对象就会在 props 中包含相应的样式属性:

// Note: this structure is simplified
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!',
    style: {
      background: "#222222",
      borderRadius: "32px",
      color: "white",
      margin: "20%",
      padding: "2px 16px 16px",
      textAlign: "center",
    }
  }
};

来看看源代码中测试用的声明文件中定义的 render 和 createElement 方法原型:

// React.d.ts 
declare module 'react' {
  export class Component {
    props: any;
    state: any;
    context: any;
    static name: string;
    constructor(props?, context?);
    setState(partial : any, callback ?: any) : void;
    forceUpdate(callback ?: any) : void;
  }
  export let PropTypes : any;
  export function createElement(tag : any, props ?: any, ...children : any[]) : any
}

// ReactDom.d.ts 
declare module 'react-dom' {
  export function render(element : any, container : any) : any
  export function unmountComponentAtNode(container : any) : void
  export function findDOMNode(instance : any) : any
}

React 将 HTML 的标签元素、Component 组件和 FunctionComponent 函数式组件等等统一抽象为可以渲染的 UI 元素,这是一个很好的概念。进而根据不同的元素重载出不同 createElement 方法实现对应的构造过程,但是基本还是几个要素:

  • 元素出现在页面时的 tag;
  • 元素配套的属性值 props;
  • 元素的下一级节点 children;

以下是摘自源代码中部分 createElement 重载方法原型:

// DOM Elements
// TODO: generalize this to everything in `keyof ReactHTML`, not just "input"
function createElement<P extends DOMAttributes<T>, T extends Element>(
    type: string,
    props?: ClassAttributes<T> & P | null,
    ...children: ReactNode[]): DOMElement<P, T>;


// Custom components
function createElement<P extends {}>(
    type: FunctionComponent<P> | ComponentClass<P> | string,
    props?: Attributes & P | null,
    ...children: ReactNode[]): ReactElement<P>;

可以预计的是,最后还是要通过浏览器的 DOM API 去构造和管理节点的:

document.createElement('div')

如果要追踪具体实现代码,可以从 React 工程的相应模块中查找,目前的版本是 react-17.0.0:

// react-17.0.0\packages\react-dom\src\client\ReactDOMLegacy.js

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  if (__DEV__) {
    const isModernRoot =
      isContainerMarkedAsRoot(container) &&
      container._reactRootContainer === undefined;
    if (isModernRoot) {
      console.error(
        'You are calling ReactDOM.render() on a container that was previously ' +
          'passed to ReactDOM.createRoot(). This is not supported. ' +
          'Did you mean to call root.render(element)?',
      );
    }
  }
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

从字面可以看出 legacyRenderSubtreeIntoContainer 大致意思就是把虚拟节点树 VirtualDOM 渲染到真实的 Web DOM 容器中:

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
  if (__DEV__) {
    topLevelUpdateWarnings(container);
    warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
  }

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

再深入追踪 Render 的代码就涉及 React Fiber 部分了,这里的 fiberRoot 就是整个 React 应用的顶级 Fiber 对象,FiberRoot 对象主要用来管理组件树组件的更新进程,同时记录组件树挂载的DOM容器相关信息。