state状态

一、注意事项

1、用setState更新状态而不能直接修改

this.state.counter += 1; //错误的

2、setState是批量执行的,因此对同一个状态执行多次,只起⼀次作用,多个状态更新可以放在同一个setState中进行

componentDidMount() {
    // 假如couter初始值为0,执行三次以后其结果是多少?=>0
    this.setState({counter: this.state.counter +1});
    this.setState({counter: this.state.counter +1});
    this.setState({counter: this.state.counter +1});
}

3、setState通常是异步的

二、三种同步更新操作

如果要同步获取到最新状态值有以下三种方式:
1、传递函数给setState方法

this.setState((nextState, props) => ({
    counter: nextState.counter + 1
})); // 1
this.setState((nextState, props) => ({
    counter: nextState.counter + 1
})); // 2
this.setState((nextState, props) => ({
    counter: nextState.counter + 1
})); // 3

2、使用定时器,setTimeout或setInterval

setTimeout(() => {
    this.setState({
        counter: this.state.counter + 2
    });
    console.log(this.state.counter); //2
}, 0);

3、原生事件中修改状态

componentDidMount() {
    document.getElementsByTagName("button")[0].addEventListener(
        "click",
        () => {
            this.setState({
                counter: this.state.counter + 2
            });
            console.log(this.state.counter);  //2
        },
        false
    );
}

总结:setState只有在合成事件和钩子函数中是异步的,在原生事件和setTimeout、setInterval中都是同步的。

ReactDOM.render、hydrate源码

一、使用入口示例

// 目录
src/index.js

// 代码
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

2、源码流程图

3、源码解析

1、入口文件

packages\react-dom\src\client\ReactDOM.js

2、render与hydrate定义

// 目录
packages\react-dom\src\client\ReactDOMLegacy.js

// 代码
/**
 * 服务端渲染
 * @param element 表示一个ReactNode,可以是一个ReactElement对象
 * @param container 需要将组件挂载到页面中的DOM容器
 * @param callback 渲染完成后需要执行的回调函数
 */
export function hydrate(
    element: React$Node,
    container: Container,
    callback: ?Function
) {
    // TODO: throw or warn if we couldn't hydrate?
    // 注意第一个参数为null,第四个参数为true
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        true,
        callback
    );
}

/**
 * 客户端渲染
 * @param element 表示一个ReactElement对象
 * @param container 需要将组件挂载到页面中的DOM容器
 * @param callback 渲染完成后需要执行的回调函数
 */
export function render(
    element: React$Element<any>,
    container: Container,
    callback: ?Function
) {
    // 注意第一个参数为null,第四个参数为false
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback
    );
}

说明:render和hydrate方法都调legacyRenderSubtreeIntoContainer方法进入渲染流程,唯一区别就是第四个参数来区分,true表示服务端渲染,false表示客户端渲染

3、legacyRenderSubtreeIntoContainer源码:

// 目录
packages\react-dom\src\client\ReactDOMLegacy.js

/**
 * 开始构建FiberRoot和RootFiber,之后开始执行更新任务
 * @param parentComponent 父组件,可以把它当成null值来处理
 * @param children ReactDOM.render()或者ReactDOM.hydrate()中的第一个参数,可以理解为根组件
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param forceHydrate 表示是否融合,用于区分客户端渲染和服务端渲染,render方法传false,hydrate方法传true
 * @param callback ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数,组件渲染完成后需要执行的回调函数
 * @returns {*}
 */
function legacyRenderSubtreeIntoContainer(
    parentComponent: ?React$Component<any, any>,
    children: ReactNodeList,
    container: Container,
    forceHydrate: boolean,
    callback: ?Function
) {
    // TODO: Without `any` type, Flow says "Property cannot be accessed on any
    // member of intersection type." Whyyyyyy.
    // 在第一次执行的时候,container上是肯定没有_reactRootContainer属性的
    // 所以第一次执行时,root肯定为undefined
    let root: RootType = (container._reactRootContainer: any);
    let fiberRoot;

    if (!root) {
        // Initial mount
        // 首次挂载,进入当前流程控制中,container._reactRootContainer指向一个ReactDOMBlockingRoot实例
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
            container,
            forceHydrate
        );

        // root表示一个FiberRoot实例,实例中有一个_internalRoot方法指向一个fiberRoot实例
        fiberRoot = root._internalRoot;
        // callback表示ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数
        // 重写callback,通过fiberRoot去找到其对应的rootFiber,然后将rootFiber的第一个child的stateNode作为callback中的this指向
        // 一般情况下我们很少去写第三个参数,所以可以不必关心这里的内容
        if (typeof callback === "function") {
            const originalCallback = callback;
            callback = function() {
                const instance = getPublicRootInstance(fiberRoot);
                originalCallback.call(instance);
            };
        }

        // Initial mount should not be batched.
        // 对于首次挂载来说,更新操作不应该是批量的,所以会先执行unbatchedUpdates方法
        // 该方法中会将executionContext(执行上下文)切换成LegacyUnbatchedContext(非批量上下文)
        // 切换上下文之后再调用updateContainer执行更新操作
        // 执行完updateContainer之后再将executionContext恢复到之前的状态
        unbatchedUpdates(() => {
            updateContainer(children, fiberRoot, parentComponent, callback);
        });
    } else {
        // 不是首次挂载,即container._reactRootContainer上已经存在一个ReactDOMBlockingRoot实例
        fiberRoot = root._internalRoot;
        // 下面的控制语句和上面的逻辑保持一致
        if (typeof callback === "function") {
            const originalCallback = callback;
            callback = function() {
                const instance = getPublicRootInstance(fiberRoot);
                originalCallback.call(instance);
            };
        }
        // Update
        // 对于非首次挂载来说,是不需要再调用unbatchedUpdates方法的
        // 即不再需要将executionContext(执行上下文)切换成LegacyUnbatchedContext(非批量上下文)
        // 而是直接调用updateContainer执行更新操作
        updateContainer(children, fiberRoot, parentComponent, callback);
    }

    // 返回一个RootFiber
    return getPublicRootInstance(fiberRoot);
}

说明:通过container._reactRootContainer来判断是否为首次渲染,是的话就调用legacyCreateRootFromDOMContainer方法创建ReactDOMBlockingRoot实例(调度Root),然后调用updateContainer更新,最后调用getPublicRootInstance方法返回一个RootFiber。

4、legacyCreateRootFromDOMContainer源码:

// 目录
packages\react-dom\src\client\ReactDOMLegacy.js

/**
 * 删除子节点,创建并返回一个ReactDOMBlockingRoot实例
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param forceHydrate 是否需要强制融合,render方法传false,hydrate方法传true
 * @returns {FiberRoot}
 */
function legacyCreateRootFromDOMContainer(
    container: Container,
    forceHydrate: boolean
): RootType {
    // 判断是否需要融合
    const shouldHydrate =
        forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
    // First clear any existing content.
    // 针对客户端渲染的情况,需要将container容器中的所有元素移除
    if (!shouldHydrate) {
        let rootSibling;
        // 循环遍历每个子节点进行删除
        while ((rootSibling = container.lastChild)) {
            container.removeChild(rootSibling);
        }
    }

    // 返回一个FiberRoot实例
    // 该实例具有一个_internalRoot属性指向fiberRoot
    return createLegacyRoot(
        container,
        shouldHydrate
            ? {
                    hydrate: true
              }
            : undefined
    );
}

/**
 * 根据nodeType和attribute判断是否需要融合
 * @param container DOM容器
 * @returns {boolean}
 */
function shouldHydrateDueToLegacyHeuristic(container) {
    // 获取DOM容器中的第一个子节点
    const rootElement = getReactRootElementInContainer(container);
    // ELEMENT_NODE = 1 》代表元素节点
    // ROOT_ATTRIBUTE_NAME 》 data-reactroot
    // 是否具有ROOT_ATTRIBUTE_NAME属性来区分是客户端渲染还是服务端渲染
    // 客户端返回false, 服务器端返回true
    return !!(
        rootElement &&
        rootElement.nodeType === ELEMENT_NODE &&
        rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
    );
}

/**
 * 根据container来获取DOM容器中的第一个子节点
 * @param container DOM容器
 * @returns {*}
 */
function getReactRootElementInContainer(container: any) {
    if (!container) {
        return null;
    }

    // DOCUMENT_NODE = 9;》代表整个文档,即document
    if (container.nodeType === DOCUMENT_NODE) {
        return container.documentElement;
    } else {
        // 返回第一个子节点
        return container.firstChild;
    }
}

说明:
(1)补充一个知识点!!

1、!可将变量转换成boolean类型,null、undefined和空字符串取反都为true,其余都为false

!null = true;
!undefined = true;
!"" = true;

2!! 用来做类型判断,在第一步!(变量)之后再做逻辑取反运算
if (!!a) {
    //a有内容才执行的代码...
}
等价于:
var a;
if (a != null && typeof(a) != undefined && a! = ''){
    //a有内容才执行的代码...
}

(2)shouldHydrateDueToLegacyHeuristic方法说明

  • 首先根据container来获取DOM容器中的第一个子节点
  • 通过子节点的nodeType和是否具有ROOT_ATTRIBUTE_NAME属性来区分是客户端渲染还是服务端渲染
  • ROOT_ATTRIBUTE_NAME位于packages/react-dom/src/shared/DOMProperty.js,表示data-reactroot属性

(3)服务端与客户端的区分是,node服务会在后台先根据匹配到的路由生成完整的HTML字符串,然后再将HTML字符串发送到浏览器端,最终生成的HTML结构简化后如下:

<body>
    <div id="root">
        <div data-reactroot=""></div>
    </div>
</body>

(4)在客户端渲染中是没有data-reactroot属性的,因此就可以区分出客户端渲染和服务端渲染
(5)在React中的nodeType主要包含了五种,其对应的值和W3C中的nodeType标准是保持一致的

// 目录
packages\react-dom\src\shared\HTMLNodeType.js

// 代表元素节点
export const ELEMENT_NODE = 1;
// 代表文本节点
export const TEXT_NODE = 3;
// 代表注释节点
export const COMMENT_NODE = 8;
// 代表整个文档,即document
export const DOCUMENT_NODE = 9;
// 代表文档片段节点
export const DOCUMENT_FRAGMENT_NODE = 11;

5、createLegacyRoot > ReactDOMBlockingRoot > createRootImpl源码

/**
 * 创建并返回一个ReactDOMBlockingRoot实例
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param options 配置信息,只有在hydrate时才有值,否则为undefined
 * @returns {ReactDOMBlockingRoot}
 */
export function createLegacyRoot(
    container: Container,
    options?: RootOptions
): RootType {
    //LegacyRoot => 0
    return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

/**
 * 定义ReactDOMBlockingRoot构造函数
 * @param {*} container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param {*} tag  fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param {*} options 配置信息,只有在hydrate时才有值,否则为undefined
 */
function ReactDOMBlockingRoot(
    container: Container,
    tag: RootTag,
    options: void | RootOptions
) {
    // 实例挂载一个私有属性引用
    this._internalRoot = createRootImpl(container, tag, options);
}

/**
 * 创建并返回一个fiberRoot
 * @param container DOM容器
 * @param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param options 配置信息,只有在hydrate时才有值,否则为undefined
 * @returns {*}
 */
function createRootImpl(
    container: Container,
    tag: RootTag,
    options: void | RootOptions
) {
    // Tag is either LegacyRoot or Concurrent Root
    // 判断是否是hydrate模式
    const hydrate = options != null && options.hydrate === true;
    const hydrationCallbacks =
        (options != null && options.hydrationOptions) || null;

    // 创建一个fiberRoot
    const root = createContainer(container, tag, hydrate, hydrationCallbacks);
    // 给container附加一个内部属性用于指向fiberRoot的current属性对应的rootFiber节点
    markContainerAsRoot(root.current, container);
    if (hydrate && tag !== LegacyRoot) {
        const doc =
            container.nodeType === DOCUMENT_NODE
                ? container
                : container.ownerDocument;
        eagerlyTrapReplayableEvents(container, doc);
    }
    return root;
}

说明:createRootImpl方法通过调用createContainer方法来创建一个fiberRoot实例,并将该实例返回并赋值到ReactDOMBlockingRoot构造函数的内部成员_internalRoot属性上。

Component与PureComponent源码

1、源码目录:

packages\react\src\ReactBaseClasses.js

2、Component源码

import ReactNoopUpdateQueue from './ReactNoopUpdateQueue';

const emptyObject = {};

/**
 * Component构造函数,用于创建一个类组件的实例
 * @param props 表示所拥有的属性信息
 * @param context 表示所拥有的属性信息
 * @param updater 表示一个updater对象,定义在react-dom,用于处理后续的更新调度任务
 */
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // 该属性用于存储类组件实例的引用信息
  // 在React中我们可以有多种方式来创建引用
  // 通过字符串的方式,如:<input type="text" ref="inputRef" />
  // 通过回调函数的方式,如:<input type="text" ref={(input) => this.inputRef = input;} />
  // 通过React.createRef()的方式,如:this.inputRef = React.createRef(null); <input type="text" ref={this.inputRef} />
  // 通过useRef()的方式,如:this.inputRef = useRef(null); <input type="text" ref={this.inputRef} />
  this.refs = emptyObject;
  // 当state发生变化的时候,需要updater对象去处理后续的更新调度任务,没传入设置默认更新器
  this.updater = updater || ReactNoopUpdateQueue;
}

// 在原型上新增了一个isReactComponent属性用于标识该实例是一个类组件的实例
// 函数定义组件是没有这个属性的,所以可以通过判断原型上是否拥有这个属性来进行区分
Component.prototype.isReactComponent = {};

/**
 * 用于更新状态
 * @param partialState 表示下次需要更新的状态
 * @param callback 在组件更新之后需要执行的回调
 */
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

/**
 * 用于强制重新渲染
 * @param callback 在组件重新渲染之后需要执行的回调
 */
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

说明:isReactComponent属性来区分函数定义组件和类组件

// 返回true则表示类组件,否则表示函数定义组件
function shouldConstruct(Component) {
  return !!(Component.prototype && Component.prototype.isReactComponent);
}

3、PureComponent源码

// 通过借用构造函数,实现典型的寄生组合式继承,避免原型污染
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

// 定义PureComponent构造函数
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

// 将PureComponent的原型指向借用构造函数的实例
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
// 重新设置构造函数的指向
pureComponentPrototype.constructor = PureComponent;
// 合并原型,减少原型链查找所浪费的时间(原型链越长所耗费的时间越久)
Object.assign(pureComponentPrototype, Component.prototype);
// 定义PureComponent标志,用于区分
pureComponentPrototype.isPureReactComponent = true;

export {Component, PureComponent};

4、使用代码

// 源码目录
packages\react-dom\src\server\ReactPartialRenderer.js

// 实例化Component组件
let inst;
if (isClass) {
    inst = new Component(element.props, publicContext, updater);
}
————————————————