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);
}
智一面|前端面试必备练习题