React原理 生命周期

预备

React 有两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历 React fiber 树,目的就是发现不同( diff ),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行 render 函数。在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。

类组件的处理逻辑在beginWork中被调用,react-reconciler/src/ReactFiberBeginWork.js

① instance 类组件对应实例。
② workInProgress 树,当前正在调和的 fiber 树 ,一次更新中,React 会自上而下深度遍历子代 fiber ,如果遍历到一个 fiber ,会把当前 fiber 指向 workInProgress。
③ current 树,在初始化更新中,current = null ,在第一次 fiber 调和之后,会将 workInProgress 树赋值给 current 树。React 来用workInProgress 和 current 来确保一次更新中,快速构建,并且状态不丢失。
④ Component 就是项目中的 class 组件。
⑤ nextProps 作为组件在一次更新中新的 props 。
⑥ renderLanes 作为下一次渲染的优先级。

在组件实例上可以通过 _reactInternals 属性来访问组件对应的 fiber 对象。在 fiber 对象上,可以通过 stateNode 来访问当前 fiber 对应的组件实例。

class Instance . _reactInternals => class Fiber

class Fiber . stateNode => class Instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
// stateNode 是 fiber 指向 类组件实例的指针。
const instance = workInProgress.stateNode;
let shouldUpdate;
// instance 为组件实例,如果组件实例不存在,证明该类组件没有被挂载过,那么会走初始化流程
if (instance === null) {
// 在这个方法中组件通过new被实例化
constructClassInstance(workInProgress, Component, nextProps);
// 初始化挂载组件流程
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
// shouldUpdate 标识用来证明 组件是否需要更新。
shouldUpdate = true;
} else if (current === null) {
// 已经存在了一个实例可以被复用
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
// 更新组件流程
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}

const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
return nextUnitOfWork;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes,
) {
// 即使 shouldComponentUpdate 返回了 false,Refs也应该被更新
markRef(current, workInProgress);

const instance = workInProgress.stateNode;

// Rerender
ReactCurrentOwner.current = workInProgress;
// 获取子节点
let nextChildren = instance.render();

// 调和子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);

// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;

// The context might have changed so we need to recalculate it.
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}

return workInProgress.child;
}

初始化阶段

constructClassInstance构建了组件的实例,在实例化组件之后,会调用 mountClassInstance 组件初始化。

react-reconciler/src/ReactFiberClassComponent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 在从没有渲染过的实例上执行挂载生命周期
function mountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): void {
// 组件实例
const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;

initializeUpdateQueue(workInProgress);

// 拿到类组件构造函数的静态方法
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
var prevState = workInProgress.memoizedState;
// 返回更新之后的state
var partialState = getDerivedStateFromProps(nextProps, prevState);
// 如果返回的state不合法,使用原有状态,否则合并两个状态生成一个新的state对象
var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;

// Once the update queue is empty, persist the derived state onto the
// base state.
if (workInProgress.lanes === NoLanes) {
// Queue is always non-null for classes
var updateQueue = workInProgress.updateQueue;
updateQueue.baseState = memoizedState;
}
instance.state = workInProgress.memoizedState;
}

if (typeof instance.componentDidMount === 'function') {
workInProgress.flags |= fiberFlags;
}
}

render 函数执行

到此为止 mountClassInstance 函数完成,但是上面 updateClassComponent 函数, 在执行完 mountClassInstance 后,执行了 render 渲染函数,形成了 children , 接下来 React 调用 reconcileChildren 方法深度调和 children

componentDidMount函数执行

上述提及的几生命周期都是在 render 阶段执行的。一旦 React 调和完所有的 fiber 节点,就会到 commit 阶段,在组件初始化 commit 阶段,会调用 componentDidMount 生命周期。

1
2
3
4
function commitRootImpl(root, renderPriorityLevel){
const finishedWork = root.finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
}

17.0.2

1
2
3
4
5
6
7
8
9
10
11
12
function commitLifeCycles(finishedRoot,current,finishedWork){
switch (finishedWork.tag){ /* fiber tag 在第一节讲了不同fiber类型 */
case ClassComponent: { /* 如果是 类组件 类型 */
const instance = finishedWork.stateNode /* 类实例 */
if(current === null){ /* 类组件第一次调和渲染 */
instance.componentDidMount()
}else{ /* 类组件更新 */
instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate);
}
}
}
}

17.0.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case ClassComponent: {
const instance = finishedWork.stateNode;
if (!offscreenSubtreeWasHidden) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidMount();
}

commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
}
}

更新阶段

回到了最开始 updateClassComponent 函数了,当发现 current 不为 null 的情况时,说明该类组件被挂载过,那么直接按照更新逻辑来处理。

react-reconciler/src/ReactFiberClassComponent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function updateClassInstance(current,workInProgress,ctor,newProps,renderExpirationTime){
// 类组件实例
const instance = workInProgress.stateNode;
// 判断是否具有 getDerivedStateFromProps 生命周期
const hasNewLifecycles = typeof ctor.getDerivedStateFromProps === 'function'
if(!hasNewLifecycles && typeof instance.componentWillReceiveProps === 'function' ){
// 浅比较 props 不相等
if (oldProps !== newProps || oldContext !== nextContext) {
// 执行生命周期 componentWillReceiveProps
instance.componentWillReceiveProps(newProps, nextContext);
}
}
let newState = (instance.state = oldState);
if (typeof getDerivedStateFromProps === 'function') {
/* 执行生命周期getDerivedStateFromProps ,逻辑和mounted类似 ,合并state */
ctor.getDerivedStateFromProps(nextProps,prevState)
newState = workInProgress.memoizedState;
}
let shouldUpdate = true
/* 执行生命周期 shouldComponentUpdate 返回值决定是否执行render ,调和子节点 */
if(typeof instance.shouldComponentUpdate === 'function' ){
shouldUpdate = instance.shouldComponentUpdate(newProps,newState,nextContext,);
}
if(shouldUpdate){
if (typeof instance.componentWillUpdate === 'function') {
/* 执行生命周期 componentWillUpdate */
instance.componentWillUpdate();
}
return shouldUpdate
}

getSnapshotBeforeUpdate 的执行也是在 commit 阶段,commit 阶段细分为 before Mutation( DOM 修改前),Mutation ( DOM 修改),Layout( DOM 修改后) 三个阶段,getSnapshotBeforeUpdate 发生在before Mutation 阶段

销毁阶段

在一次调和更新中,如果发现元素被移除,就会打对应的 Deletion 标签 ,然后在 commit 阶段就会调用 componentWillUnmount 生命周期,接下来统一卸载组件以及 DOM 元素。

1
2
3
4
5
6
7
8
var callComponentWillUnmountWithTimer = function (current, instance) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;

{
instance.componentWillUnmount();
}
};

各生命周期最佳实践

constructor
1
2
3
4
5
6
7
8
9
10
11
12
constructor(props){
super(props) // 执行 super ,别忘了传递props,才能在接下来的上下文中,获取到props。
this.state={ //① 可以用来初始化state,比如可以用来获取路由中的
name:'alien'
}
this.handleClick = this.handleClick.bind(this) /* ② 绑定 this */
this.handleInputChange = debounce(this.handleInputChange , 500) /* ③ 绑定防抖函数,防抖 500 毫秒 */
const _render = this.render
this.render = function(){
return _render.bind(this) /* ④ 劫持修改类组件上的一些生命周期 */
}
}
UNSAFE_componentWillMount

在新版本的react(v16.3)中componentWillMount已经变更为UNSAFE_componentWillMount,而且不在推荐使用,其中很大一部分原因是经常被滥用

  • 初始化状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ExampleComponent extends React.Component {
constructor(props){
this.state = {
color: "red"
};
}
state = {
color: "red"
};
componentWillMount() {
// 应该将初始化状态放到构造函数或属性的初始化状态中
// this.setState({
// color: "red"
// });
}
}
  • 获取异步的外部数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Before
class ExampleComponent extends React.Component {
state = {
externalData: null,
};

componentWillMount() {
this._asyncRequest = loadMyAsyncData().then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}

componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}

render() {
if (this.state.externalData === null) {
// 渲染加载状态 ...
} else {
// 渲染真实 UI ...
}
}
}

上述代码对于服务器渲染(异步的请求数据不会被放到state中)和即将推出的异步渲染模式(可能执行多次)都存在问题。通常会把上面的操作放到 componentDidMount

另一个问题是,componentWillMount的名字比较反直觉,听起来觉得在这个生命周期中获取数据,可以避免第一次render的时候进行一次空渲染,单实际上 componentWillMount执行后 render方法会立即执行,如果componentWillMount 没有获取到可用数据,render方法中同样获取不到数据。

如果想稍微提前一点请求,从而适应低性能的设备可以使用下面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// This is an advanced example! It is not intended for use in application code.
// Libraries like Relay may make use of this technique to save some time on low-end mobile devices.
// Most components should just initiate async requests in componentDidMount.

class ExampleComponent extends React.Component {
_hasUnmounted = false;

state = {
externalData: null,
};

constructor(props) {
super(props);

// Prime an external cache as early as possible.
// Async requests are unlikely to complete before render anyway,
// So we aren't missing out by not providing a callback here.
asyncLoadData(this.props.someId);
}

componentDidMount() {
// Now that this component has mounted,
// Wait for earlier pre-fetch to complete and update its state.
// (This assumes some kind of external cache to avoid duplicate requests.)
asyncLoadData(this.props.someId).then(externalData => {
if (!this._hasUnmounted) {
this.setState({ externalData });
}
});
}

componentWillUnmount() {
this._hasUnmounted = true;
}

render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
}
  • 事件监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Before
class ExampleComponent extends React.Component {
componentWillMount() {
this.setState({
subscribedValue: this.props.dataSource.value,
});
// 这是不安全的,它会导致内存泄漏!
this.props.dataSource.subscribe(
this.handleSubscriptionChange
);
}

componentWillUnmount() {
this.props.dataSource.unsubscribe(
this.handleSubscriptionChange
);
}

handleSubscriptionChange = dataSource => {
this.setState({
subscribedValue: dataSource.value,
});
};
}

上面的代码在服务端可能永远不会调用 componentWillUnmount, 或者在渲染完成之前可能被中断,导致不调用 componentWillUnmount,这两种场景都可能导致内存泄露,推荐的做法是移到componentDidMount

订阅的触发,导致属性和状态的改变,ReduxMobX 会帮助我们实现,对于应用开发场景可以使用 create-subscription, 在这里可以看到源码分析。

UNSAFE_componentWillReceiveProps getDerivedStateFromProps

首先明确一下这个两个方法在使用时,最常见的错误

  1. 直接复制 props 到 state 上
  2. 如果 props 和 state 不一致就更新 state
  3. 经常被误解只有props改变时这两个方法才会调用,实际上只要父组件重新渲染这两个方法就会被调用

想说清楚造成这两个错误的原因,需要先了解一个概念叫做 受控

受控非受控通常用来指代表单的 inputs,但是也可以用来描述数据频繁更新的组件。如果组件完全依赖于外部传入的props,可以认为是受控状态,因为组件完全被父组件的props控制。如果组件的状态只保存在组件(state)内部,可以认为是非受控的,因为组件有自己的状态,不受父组件的控制。

而组件中一旦将两种模式混为一谈(同时包含propsstate)就会造成问题

直接复制 props 到 state 上造成的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class EmailInput extends Component {
state = { email: this.props.email };

render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}

handleChange = event => {
this.setState({ email: event.target.value });
};

componentWillReceiveProps(nextProps) {
// 这会覆盖所有组件内的 state 更新!
this.setState({ email: nextProps.email });
}
}

初看还觉得可以,但是问题很严重,当通过input的输入改变了组件的状态,这时如果父组件更新就会触发componentWillReceiveProps方法,会将state.email状态重写,覆盖了刚才通过input输入更新的状态,导致状态丢失,这是两种模式混用最明显的错误。在实际的使用中会有多个props属性,任意一个属性的更新都会导致内部状态可能被覆盖。

既然这样,可以很容易想到,能不能只用props来更新组件,不让组件有自己的内部状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class EmailInput extends Component {
state = {
email: this.props.email
};

componentWillReceiveProps(nextProps) {
// 只要 props.email 改变,就改变 state
if (nextProps.email !== this.props.email) {
this.setState({
email: nextProps.email
});
}
}
}

但是仍然有个问题。想象一下,如果这是一个密码输入组件,拥有同样 email 的两个账户(假设一个邮箱可以注册多个账户)进行切换时,这个输入框不会重置(用来让用户重新登录)。因为父组件传来的 prop 值没有变化!这会让用户非常惊讶,因为这看起来像是帮助一个用户分享了另外一个用户的密码

最佳实践:完全可控的组件

从组件里面删除state,完全让外部的props的接管组件的状态

最佳实践:有 key 的非可控组件

让组件自己存储临时的 email state。在这种情况下,组件仍然可以从 prop 接收“初始值”,但是更改之后的值就和 prop 没关系了

1
2
3
4
5
6
7
8
9
10
11
class EmailInput extends Component {
state = { email: this.props.defaultEmail };

handleChange = event => {
this.setState({ email: event.target.value });
};

render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
}

我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。 Keys 一般用来渲染动态列表,但是这里也可以使用。在这个示例里,当用户输入时,我们使用 user ID 当作 key 重新创建一个新的 email input 组件

不用为每次输入都添加 key,在整个表单上添加 key 更有位合理。每次 key 变化,表单里的所有组件都会用新的初始值重新创建。

1
2
3
4
<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>

这听起来很慢,但是这点的性能是可以忽略的。如果在组件树的更新上有很重的逻辑,这样反而会更快,因为省略了子组件 diff。

备选:用 prop 的 ID 重置非受控组件

如果某些情况下 key 不起作用(可能是组件初始化的开销太大),一个麻烦但是可行的方案是在 getDerivedStateFromProps 观察 userID 的变化:、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};

static getDerivedStateFromProps(props, state) {
// 只要当前 user 变化,
// 重置所有跟 user 相关的状态。
// 这个例子中,只有 email 和 user 相关。
if (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
}

getDerivedStateFromProps 的存在只有一个目的:让组件在 props 变化时更新 state。 代替了原来的componentWillReceiveProps

nextProps 父组件新传递的 props ;

你可能想知道为什么我们不将上一个 props 作为参数传递给 getDerivedStateFromProps。我们在设计 API 时考虑过这个方案,但最终决定不采用它,原因有两个:

  • prevProps 参数在第一次调用 getDerivedStateFromProps(实例化之后)时为 null,需要在每次访问 prevProps 时添加 if-not-null 检查。

  • 在 React 的未来版本中,不传递上一个 props 给这个方法是为了释放内存。(如果 React 无需传递上一个 props 给生命周期,那么它就无需保存上一个 props 对象在内存中。)

prevState 组件在此次更新前的 state

需要注意每次组件更新时getDerivedStateFromProps都会执行,无论是以哪那种方式更新

通常用于吧props混入state作为初始状态,合并后的state可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。

1
getDerivedStateFromProps(nextProps,prevState)

总结

最重要的是确定组件是受控组件还是非受控组件。不要直接复制(mirror) props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。

对于不受控的组件,当你想在 prop 变化(通常是 ID )时重置 state 的话,可以选择以下几种方式:

建议: 重置内部所有的初始 state,使用 key 属性
选项一:仅更改某些字段,观察特殊属性的变化(比如 props.userID)。

UNSAFE_componentWillUpdate getSnapshotBeforeUpdate

当组件收到新的 propsstate 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。 有时人们使用 componentWillUpdate 是出于一种反直觉,当 componentDidUpdate 触发时,更新其他组件的 state 已经”太晚”了。事实并非如此。在UI渲染之前,componentWillUpdatecomponentDidUpdate中的state改变都将被记录。

1
getSnapshotBeforeUpdate(prevProps, prevState)

componentWillUpdate常见的错误是在生命周期中使用异步获取数据的方法,因为任何state的更新和父组件的重新渲染会触发componentWillUpdate重新执行,所有获取数据的方法可能被执行多次。相反,应该使用 componentDidUpdate 生命周期,因为它保证每次更新只调用一次。

1
2
3
4
5
6
7
8
9
10
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (
this.state.someStatefulValue !==
prevState.someStatefulValue
) {
this.props.onChange(this.state.someStatefulValue);
}
}
}

更新前读取 DOM 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ListBox extends React.Component {
ref = React.createRef();
previousScrollOffset=0;
// 在列表更新的时候,读取DOM属性
componentWillUpdate(nextProps, nextState) {
// 当列列表个数被改变的时候计算偏移量
if (this.props.list.length < nextProps.list.length) {
this.previousScrollOffset =
this.ref.current.scrollHeight - this.ref.current.scrollTop;
}
}
// 在列表被挂载的时候修改DOM属性
componentDidUpdate(){
// previousScrollOffset !== 容器高度时(2px是边框高度),表示滚动条没有滚动到底部,可能在查看历史记录的状态
if(this.previousScrollOffset!== this.ref.current.offsetHeight-2) return;

// newScrollHeight - oldScrollHeight + lastScrollTop
// 相当于在上一次的scrollTop上加上ScrollHeight的增量
this.ref.current.scrollTop = (this.ref.current.scrollHeight - this.previousScrollOffset )
this.previousScrollOffset = 0;
}
render() {
return (<div style={{ width: 300, height: 200, overflow: 'auto', border: '1px solid' }} ref={this.ref}>
{this.props.list.map(item => <div style={{ height: 20 }}>{item.val}</div>)}
</div>)
}
}

在上面的示例中,componentWillUpdate 用于读取 DOM 属性。但是,对于异步渲染,“渲染”阶段的生命周期(如 componentWillUpdate 和 render)和”提交”阶段的生命周期(如 componentDidUpdate)之间可能存在延迟。如果用户在这段时间内调整窗口大小,那么从 componentWillUpdate 读取的 scrollHeight 值将过时。

这个问题的解决方案是使用新的“提交”阶段生命周期 getSnapshotBeforeUpdate。这个方法在发生变化 前立即 被调用(例如在更新 DOM 之前)。它可以返回一个 React 的值作为参数传递给 componentDidUpdate 方法,该方法在发生变化 后立即 被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ListBox extends React.Component {
ref = React.createRef();
getSnapshotBeforeUpdate(prevProps, nextState) {
if (this.props.list.length > prevProps.list.length) {
return this.ref.current.scrollHeight - this.ref.current.scrollTop;
}
}
componentDidUpdate(prevProps, prevState, snapshot){
if(snapshot>this.ref.current.offsetHeight) return;
this.ref.current.scrollTop = (this.ref.current.scrollHeight - snapshot )
console.log(this.ref.current.scrollTop);
}
render() {
return (<div style={{ width: 300, height: 200, overflow: 'auto', border: '1px solid' }} ref={this.ref}>
{this.props.list.map(item => <div style={{ height: 20 }}>{item.val}</div>)}
</div>)
}
}
componentDidMount

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理

useEffect 和 useLayoutEffect

对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effectcallback, React 会像 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。

useLayoutEffectuseEffect 不同的地方是采用了同步执行

首先 useLayoutEffect 是在 DOM 绘制之前,这样可以方便修改 DOM ,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。useLayoutEffect callback 中代码执行会阻塞浏览器绘制。

useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。

React原理 props深入

props的几种用法

① props 作为一个子组件渲染数据源。
② props 作为一个通知父组件的回调函数。
③ props 作为一个单纯的组件传递。
④ props 作为渲染函数。
⑤ render props , 和④的区别是放在了 children 属性上。
⑥ render component 插槽组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* children 组件 */
function ChidrenComponent(){
return <div> In this chapter, let's learn about react props ! </div>
}
/* props 接受处理 */
class PropsComponent extends React.Component{
componentDidMount(){
console.log(this,'_this')
}
render(){
const { children , mes , renderName , say ,Component } = this.props
const renderFunction = children[0]
const renderComponent = children[1]
/* 对于子组件,不同的props是怎么被处理 */
return <div>
{ renderFunction() }
{ mes }
{ renderName() }
{ renderComponent }
<Component />
<button onClick={ () => say() } > change content </button>
</div>
}
}
/* props 定义绑定 */
class Index extends React.Component{
state={
mes: "hello,React"
}
node = null
say= () => this.setState({ mes:'let us learn React!' })
render(){
return <div>
<PropsComponent
mes={this.state.mes} // ① props 作为一个渲染数据源
say={ this.say } // ② props 作为一个回调函数 callback
Component={ ChidrenComponent } // ③ props 作为一个组件
renderName={ ()=><div> my name is alien </div> } // ④ props 作为渲染函数
>
{ ()=> <div>hello,world</div> } { /* ⑤render props */ }
<ChidrenComponent /> { /* ⑥render component */ }
</PropsComponent>
</div>
}
}

监听props改变

类组件

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

函数组件

函数组件中同理可以用 useEffect 来作为 props 改变后的监听函数。

props+children 最佳实践

增强子组件

通过 props.children 属性访问到 Chidren 组件,为 React element 对象。

  1. 可以根据需要控制 Chidren 是否渲染。

  2. Container 可以用 React.cloneElement 强化 props (混入新的 props ),或者修改 Chidren 的子元素。

1
2
3
<Container>
<Children>
</Container>

函数式子组件

  1. 根据需要控制 Chidren 渲染与否。
  2. 可以将需要传给 Children 的 props 直接通过函数参数的方式传递给执行函数 children 。
1
2
3
<Container>
{ (ContainerProps)=> <Children {...ContainerProps} /> }
</Container>

像下面这种情况下 children 是不能直接渲染的,直接渲染会报错。

1
2
3
4
5
6
7
function  Container(props) {
const ContainerProps = {
name: 'alien',
mes:'let us learn react'
}
return props.children(ContainerProps)
}

混合使用

1
2
3
4
<Container>
<Children />
{ (ContainerProps)=> <Children {...ContainerProps} name={'haha'} /> }
</Container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Children = (props)=> (<div>
<div>hello, my name is { props.name } </div>
<div> { props.mes } </div>
</div>)

function Container(props) {
const ContainerProps = {
name: 'alien',
mes:'let us learn react'
}
return props.children.map(item=>{
if(React.isValidElement(item)){ // 判断是 react elment 混入 props
return React.cloneElement(item,{ ...ContainerProps },item.props.children)
}else if(typeof item === 'function'){
return item(ContainerProps)
}else return null
})
}

const Index = ()=>{
return <Container>
<Children />
{ (ContainerProps)=> <Children {...ContainerProps} name={'haha'} /> }
</Container>
}

props的意义

层级间数据传递

父组件 props 可以把数据层传递给子组件去渲染消费。另一方面子组件可以通过 props 中的 callback ,来向父组件传递信息。还有一种可以将视图容器作为 props 进行渲染。

React 可以把组件的闭合标签里的插槽,转化成 children 属性,一会将详细介绍这个模式。

用于更新判断

在 React 中,props 在组件更新中充当了重要的角色,在 fiber 调和阶段中,diff 可以说是 React 更新的驱动器,熟悉 vue 的同学都知道 vue 中基于响应式,数据的变化,就会颗粒化到组件层级,通知其更新,但是在 React 中,无法直接检测出数据更新波及到的范围,props 可以作为组件是否更新的重要准则,变化即更新,于是有了 PureComponent ,memo 等性能优化方案。

使用技巧

使用剩余参数过滤props

1
2
3
4
function Father(props){
const { age,...fatherProps } = props
return <Son { ...fatherProps } />
}

混合props

1
2
3
function Father(prop){
return React.cloneElement(prop.children,{ mes:'let us learn React !' })
}

实现一个简易 webpack

webpack 的执行流程

  • 初始化 Compiler: new Webpack(config) 得到 Compiler 对象
  • 开始编译,调用 Compiler 对象 run 方法开始编译。
  • 确定入口,根据配置中的 entry 找出所有入口文件
  • 编译模块,从入口出发,调用所有配置的 Loader 对模块进行编译,找出该模块依赖的模块,递归直到所有的模块被加载进来。
  • 完成模块编译:在经过第四步使用 Loader 编译完所有模块之后,得到了每个模块被编译后的最终内容以及他们之间的依赖关系。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。(注意:这步是可以修改输出内容的最后机会)
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

做一些准备工作

想要打包总要有个项目吧,让我们着手准备一些项目文件

src,目录是项目的源文件,包含了一个工具方法util/add.js

1
2
const add = (a, b) => a + b;
export default add;

还有另一个打印方法 log.js 有一个更深层的依赖文件

1
2
3
4
import bind from "./util/bind";
const log = bind(console.log, console);

export default log;

在项目的入口文件中,引入并使用这两个方法

1
2
3
4
5
import add from "./util/add";
import log from "./log";

const count = add(1, 2);
log(count);

下面我们需要添加打包命令,就像 create-react-app 做的一样,我们想通过一个npm build命令打包, 所以通过 npm init -y初始化了package.json文件并添加了一个脚本

1
2
3
4
5
6
7
8
9
{
"name": "my-webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "node scripts/build"
}
}

显然我们并没有用于打包的可执行脚本,所以要创建一个,放在scripts/build.js文件夹下面

正如上一小结流程描述的一样,我们通过一个自定义的myWebpack方法,传入配置后生成了compiler对象

1
2
3
4
const webpack = require("../lib/myWebpack");
const config = require("../config/webpack.config");
const compiler = webpack(config);
compiler.run();

myWebpack文件是主要要去实现的功能,我们暂时先建一个空文件,那么剩下的就只有这个webpack.config.js配置文件了,简单的给一些必须配置

1
2
3
4
5
6
7
8
9
const path = require("path");

module.exports = {
entry: "../src/index.js",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "main.js",
},
};

解析入口文件依赖并编译代码

myWebpack.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const fs = require("fs");
const path = require("path");
const { parse } = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

const webpack = (config) => {
return new Compiler(config);
};

class Compiler {
constructor(options = {}) {
this.options = options;
}

run() {
const { entry } = this.options;
// 获取node进程执行的目录
const cwdPath = process.cwd();

// 因为readFile中使用相对路径是以node进程执行时的路径作为基准路径
// 可能有查不到文件报错的情况,这里使用path.resolve转换成绝对路径
const relativeEntryPath = path.resolve(__dirname, entry);
const file = fs.readFileSync(relativeEntryPath, "utf-8");

// 把文件内容转换成ast抽象语法树
// https://www.babeljs.cn/docs/babel-parser
const ast = parse(file, {
sourceType: "module",
});

// 收集入口文件依赖
const deps = [];
// 分析ast中的依赖关系保存奥依赖中
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 获取到依赖文件的引用路径
const traverseModulePath = node.source.value;
// 转换为绝对路径
const relativePath = path.resolve(cwdPath, "src", traverseModulePath);
deps.push(relativePath);
},
});

// 编译代码,从ast编译为es5

const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
console.log(code);
}
}
module.exports = webpack;

深层递归,生成依赖关系图

虽然我们拿到了入口文件的依赖,但显然是不够的,依赖的文件可能还有自己的依赖,需要递归的去获取

可以把递归解析依赖的方法,抽取成公共方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const fs = require("fs");
const path = require("path");
const { parse } = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

const webpack = (config) => {
return new Compiler(config);
};

class Compiler {
constructor(options = {}) {
this.options = options;
}
analysis(entry) {
// 获取node进程执行的目录
const cwdPath = process.cwd();

// 因为readFile中使用相对路径是以node进程执行时的路径作为基准路径
// 可能有查不到文件报错的情况,这里使用path.resolve转换成绝对路径
const relativeEntryPath = path.resolve(cwdPath, "src", entry);
const file = fs.readFileSync(relativeEntryPath, "utf-8");

// 把文件内容转换成ast抽象语法树
// https://www.babeljs.cn/docs/babel-parser
const ast = parse(file, {
sourceType: "module",
});

// 收集入口文件依赖
const deps = {};
// 分析ast中的依赖关系保存到依赖中
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 获取到依赖文件的引用路径
const traverseModulePath = node.source.value + ".js";
// 转换为绝对路径
const relativePath = path.resolve(cwdPath, "src", traverseModulePath);
deps[traverseModulePath] = relativePath;
},
});

// 编译代码,从ast编译为es5

const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});

return {
code,
deps,
entry,
};
}
run() {
const { entry } = this.options;
// 保存加载的模块
let module = [];
let index = 0;
let parseModule = this.analysis(entry);
module.push(parseModule);

while ((parseModule = module[index])) {
const { deps } = parseModule;
Object.keys(deps).forEach((depPath) => {
parseModule = this.analysis(depPath);
module.push(parseModule);
});
index++;
}

// 把各模块依赖关系从数据形式转换成对象的形式,方便使用
module = module.reduce((o, item) => {
o[item.entry] = {
deps: item.deps,
code: item.code,
};
return o;
}, {});
}
}

module.exports = webpack;

生成代码

我们需要用刚才创建好的依赖关系图来动态加载我们的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Compiler {
generate(module) {
/**
* 入口文件
* var _add = _interopRequireDefault(require("./util/add"));
* var _log = _interopRequireDefault(require("./log"));
* function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
* var count = (0, _add["default"])(1, 2);
*/

/**
* 模块文件
* Object.defineProperty(exports, "__esModule", {
* value: true
* });
* exports["default"] = void 0;
* var bind = require('./util/bind');
* var log = bind(console.log, console);
* var _default = log;
* exports["default"] = _default;
*/
const js = `
(function(modules){
//加载入口文件
var fn = function(path){
var code = modules[path].code;
// 提供给模块内部使用的require
var require = function(path){
return fn(path+'.js');
}
const exports = {};

// 根据commonjs规范包装模块的方法
(function(exports,require,code){
// eval方法中的字符串在运行时会向上级作用于查找需要的变量
eval(code)
})(exports,require,code);
// 导出给下一个模块使用
return exports;
}

// 加载入口文件
fn('${this.options.entry}')

})(${JSON.stringify(module)})
`;
const filename = path.resolve(
this.options.output.path,
this.options.output.filename
);
fs.writeFileSync(filename, js, "utf-8");
}
}

把关系图直接变成函数声明

由于用了JSON.stringify所以生成的文件中各个模块都是以字符串的形式保存,需要用eval执行

所以我们选择另一种处理方法,直接拼接出字符串形式的模块依赖表,这样在生成文件的时候可以直接变成可执行函数

下面是优化过的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class Compiler {
constructor(options = {}) {
this.options = options;
}
analysis(entry) {
// 获取node进程执行的目录
const cwdPath = process.cwd();

// 因为readFile中使用相对路径是以node进程执行时的路径作为基准路径
// 可能有查不到文件报错的情况,这里使用path.resolve转换成绝对路径
const relativeEntryPath = path.resolve(cwdPath, "src", entry);
const file = fs.readFileSync(relativeEntryPath, "utf-8");

// 把文件内容转换成ast抽象语法树
// https://www.babeljs.cn/docs/babel-parser
const ast = parse(file, {
sourceType: "module",
});

// 收集入口文件依赖
const deps = {};
// 分析ast中的依赖关系保存到依赖中
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 获取到依赖文件的引用路径
const traverseModulePath = node.source.value + ".js";
// 转换为绝对路径
const relativePath = path.resolve(cwdPath, "src", traverseModulePath);
deps[traverseModulePath] = relativePath;
},
});

// 编译代码,从ast编译为es5

let { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
code = `
(function(exports,require){
${code}
})
`;
return {
code,
deps,
entry,
};
}
run() {
const { entry } = this.options;
// 保存加载的模块
let module = [];
let index = 0;
let parseModule = this.analysis(entry);
module.push(parseModule);

while ((parseModule = module[index])) {
const { deps } = parseModule;
Object.keys(deps).forEach((depPath) => {
parseModule = this.analysis(depPath);
module.push(parseModule);
});
index++;
}
let moduleString = "{";
module.forEach((m) => {
moduleString += `"${m.entry}":${m.code},`;
});
moduleString += "}";
this.generate(moduleString);
}
generate(moduleString) {
/**
* 入口文件
* var _add = _interopRequireDefault(require("./util/add"));
* var _log = _interopRequireDefault(require("./log"));
* function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
* var count = (0, _add["default"])(1, 2);
*/

/**
* 模块文件
* Object.defineProperty(exports, "__esModule", {
* value: true
* });
* exports["default"] = void 0;
* var bind = require('./util/bind');
* var log = bind(console.log, console);
* var _default = log;
* exports["default"] = _default;
*/
const js = `
(function(modules){
function require(path){
var exports = {};
modules[path+'.js'](exports,require)
return exports;
}
// 加载入口文件
modules['${this.options.entry}']({},require)

})(${moduleString})
`;
const filename = path.resolve(
this.options.output.path,
this.options.output.filename
);
fs.writeFileSync(filename, js, "utf-8");
}
}

发布一个npm包

注册账号

npm注册 记得要去邮箱确认,不然发布的时候回报错

也可以使用命令行的方式

1
2
3
4
npm adduser

#注册并登录
npm login

登录成功提示 Logged in as xxx on https://registry.npmjs.org/.

创建一个包

一个最小的包,只需要一个package.json文件,可以用npm init来生成这个文件

1
2
3
4
5
6
7
8
9
10
11
{
"name": "@noopn/log",
"version": "0.0.1",
"main": "index.js",
"keywords": [
"log",
"logger",
"console"
],
"license": "MIT"
}

其中有一些字段是必须的:

name: 包的名字,你可能看到我用了 @noopn/log 这样的名字,这表示会创建一个在我们用户名 scope(作用范围) 下的一个包。这个叫做 scoped package。它允许我们将已经被其他包使用的名称作为包名,比如说,log 包 已经在 npm 中存在。比如 @angular/core@angular/http

version: 版本号,以便开发人员在安全地更新包版本的同时不会破坏其余的代码。npm 使用的版本系统被叫做 SemVer,是 Semantic Versioning 的缩写。

给定版本号 MAJOR.MINOR.PATCH,增量规则如下:
MAJOR 版本号的变更说明新版本产生了不兼容低版本的 API 等,
MINOR 版本号的变更说明你在以向后兼容的方式添加功能
PATCH 版本号的变更说明你在新版本中做了向后兼容的 bug 修复.

发布

现在准备好可以使用 npm publish 发布了,但不兴的是会得到一个错误

1
2
3
npm ERR! publish Failed PUT 402
npm ERR! code E402
npm ERR! You must sign up for private packages : @noopn/log

Scoped packages 会被自动发布为私有包,因为这样不但对我们这样的独立用户有用,而且它们也被公司用于在项目之间共享代码。我们想让每个人都可以使用这个模块.使用下面这个命令:

1
npm publish --access=public

成功之后会看见 + @noopn/log@0.0.1

渐入佳境

虽然发布了我们的第一个包,但是现在还不能向别人展示我们的代码,那这个地方就是github

首先新建一个项目

让我们把本地项目和远程项目关联起来

1
2
3
4
5
6
7
8
# 跟踪新文件,或者说将内容从工作目录添加到暂存区
git add .
# 将暂存区内容添加到本地仓库中。
git commit -m "xx"
# 拉去远程代码
git pull origin master
# 提交代码
git push -u origin master

在添加了新的内容之后,升级一下包的版本,并重新发布

1
2
npm version batch
npm publish

React原理 state深入

同步还是异步

batchUpdate批量更新可能并不准确,React 是有多种模式的,基本平时用的都是 legacy 模式下的 React,除了legacy 模式,还有 blocking 模式和 concurrent 模式, blocking 可以视为 concurrent 的优雅降级版本和过渡版本,React 未来将以 concurrent 模式作为默认版本,这个模式下会开启一些新功能。

对于 concurrent 模式下,会采用不同 State 更新逻辑。前不久透露出未来的React v18 版本,concurrent 将作为一个稳定的功能出现。

setState时候发生了什么

  • 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。

  • 接下来 React 会从 fiber Root 根部 root fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。

  • 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。

  • 接下来会执行 setStatecallback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。

对更新的限制

① pureComponent 可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新。

② shouldComponentUpdate 生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false。

实现原理

setState实际上调用了Component上的updater对象的类方法

/react-reconciler/src/ReactFiberClassComponent.new.js

1
2
3
4
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const classComponentUpdater = {
enqueueSetState(inst, payload, callback) {
// 获取当前fiber节点
const fiber = getInstance(inst);
// 获取当前更新时间
const eventTime = requestEventTime();
// 获取更新优先级
const lane = requestUpdateLane(fiber);

// 每一次调用`setState`,react 都会创建一个 update
const update = createUpdate(eventTime, lane);
update.payload = payload;

// 保存更新之后的会掉函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
/* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */
enqueueUpdate(fiber, update, lane);

// 开始调度更新
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}

批量更新在何时处理?

大部分的更新都是由UI交互产生,或异步的方法和函数,例如setTimeoutxhr,批量更新和事件系统息息相关

事件系统的函数调用过程为:

/react-dom/src/client/ReactDOMRoot.js

1
2
3
4
5
6
7
8
export function hydrateRoot(
container: Container,
initialChildren: ReactNodeList,
options?: HydrateRootOptions,
): RootType {
// 在root元素上监听所有的时间
listenToAllSupportedEvents(container);
}
1
2
3
4
5
6
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// 循环所有的事件名称,绑定事件
allNativeEvents.forEach(domEventName => {
listenToNativeEvent(domEventName, true, rootContainerElement);
});
}
1
2
3
function listenToNativeEvent(domEventName, isCapturePhaseListener, rootContainerElement, targetElement) {
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
1
2
3
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); // If passive option is not supported, then the
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
var eventPriority = getEventPriorityForPluginSystem(domEventName);
var listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;

case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;

case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
1
2
3
4
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
} // Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.

1
2
3
4
5
6
// 在`legacy`模式下,所有的事件都将经过此函数同一处理 
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
batchedEventUpdates(function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
function batchedEventUpdates(fn, a, b) {
// 标记为批量更新
// scheduleUpdateOnFiber中会根据这个变量判断是否批量更新
isBatchingEventUpdates = true;
try {
return batchedEventUpdatesImpl(fn, a, b);
} finally {
// try 不会影响finally执行,执行结束后标记为false
isBatchingEventUpdates = false;
finishEventHandler();
}
}

更新调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default class index extends React.Component{
state = { number:0 }
handleClick= () => {
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback1', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback2', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback3', this.state.number) })
console.log(this.state.number)
}
render(){
return <div>
{ this.state.number }
<button onClick={ this.handleClick } >number++</button>
</div>
}
}

最终打印的结果是 0,0,0,callback1 ,1,callback2 ,1,callback3 ,1,

如果是异步执行,调用栈会被改变

1
2
3
4
5
6
7
8
setTimeout(()=>{
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback1', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback2', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback3', this.state.number) })
console.log(this.state.number)
})

在异步环境批量更新

1
2
3
4
5
6
7
8
9
10
11
12
import ReactDOM from 'react-dom'
const { unstable_batchedUpdates } = ReactDOM
setTimeout(()=>{
unstable_batchedUpdates(()=>{
this.setState({ number:this.state.number + 1 })
console.log(this.state.number)
this.setState({ number:this.state.number + 1})
console.log(this.state.number)
this.setState({ number:this.state.number + 1 })
console.log(this.state.number)
})
})

那么如何提升更新优先级呢?

React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handerClick=()=>{
setTimeout(()=>{
this.setState({ number: 1 })
})
this.setState({ number: 2 })
ReactDOM.flushSync(()=>{
this.setState({ number: 3 })
})
this.setState({ number: 4 })
}
render(){
console.log(this.state.number)
return ...
}

最终结果打印 3,4,1

flushSync补充说明:flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。

综上所述, React 同一级别更新优先级关系是:

flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

hooks中的state

行为与类中的相似, 需要注意的是,在一个方法中的执行上下文中,是获取不到最新的state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export default function Index(props){
const [ number , setNumber ] = React.useState(0)
/* 监听 number 变化 */
React.useEffect(()=>{
console.log('监听number变化,此时的number是: ' + number )
},[ number ])

const handerClick = ()=>{
// 遇到下面高优先级更新被合并更新
setNumber(5)

// 和handerClick方法中下面的几个打印函数一样
// 打印值都为0,每次触发更新之后,Index函数都会被重新执行
// number值已经与当前环境绑定
console.log(number);

/** 高优先级更新 **/
ReactDOM.flushSync(()=>{
setNumber(3)
})

// 批量更新,只会触发一次更新
setNumber(1)
setNumber(2)
console.log(number);

// 滞后更新 ,批量更新规则被打破
setTimeout(()=>{
setNumber(4)
console.log(number);
})

};

// 每次函数被重新执行的时候,打印最新的state值
console.log(number)
return <div>
<span> { number }</span>
<button onClick={ handerClick } >number++</button>
</div>
}

相同与不同

相同

首先从原理角度出发,setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。

不同:

在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。

setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。

setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。

Ubuntu20.04安装VMwarePlayer

workstation 和 player之间有什么区别

前期准备

安装 build-essential

1
sudo apt install build-essential

下载 VMware Player

VMware Workstation Player

安装 VMware Player

添加可执行权限

1
sudo chmod a+x ./VMware-Player-xxx.bundle

安装

1
sudo ./VMware-Player-xxx.bundle

1
sudo sh VMware-Player-xxx.bundle

启动

按步骤安装

在BIOS中开启虚拟化技术

一般为BIOS中,【Configuration】选项下的【Intel Virtual Technology】

为 vmnet 和 vmmon 服务创建私有密钥

官方文档中解决找不到/dev/vmmon的问题

“Cannot open /dev/vmmon: No such file or directory” error when powering on a VM (2146460)

最后一步执行需要用root权限执行,并输入密码

1
2
sudo su
mokutil –import MOK.der

重启电脑,在UEFI BIOS中选择Enroll MOK

重启后就可以正常使用

实现一个plugin

通过plugin拷贝额外资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const fs = require('fs');
const path = require('path');
const glob = require("glob")
const {promisify} = require('util');
const readFile = promisify(fs.readFile);

const {RawSource} = require('webpack').sources;
const { validate } = require('schema-utils');

const schema = {
"type": 'object',
"properties": {
"from": {
"type": 'string',
},
"to": {
"type": 'string',
},
"ignore": {
"type": 'array',
},
// 不允许有其他属性
"additionalProperties": false
}
};

class CopyfilePlugin {
constructor(options={}){
// 验证参数
validate(schema, options);
this.options = options;
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('CopyfilePlugin',(compilation)=>{
compilation.hooks.additionalAssets.tapAsync('CopyfilePlugin', (callback) => {

const {from,to,ignore} = this.options;
// 获取系统运行时的文件目录
const {context} = compiler.options;
// 获取from的绝对路径
const fromRelative = path.resolve(context,from,'*.*');

// 获取from文件夹的文件
// 排除不需要的文件
const filePaths = glob.sync(fromRelative,{
ignore:['**/index.html']
})
filePaths.forEach(async (filePath)=>{
const file = await readFile(filePath);
// 获取文件名称
const filename = path.basename(filePath);
// 添加额外的打包资源
compilation.assets[filename] = new RawSource(file);
})

callback()
});
})
}
}

module.exports =CopyfilePlugin;

webpack中complation用法

Compilation

Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const {RawSource}  = require('webpack').sources;
const fs = require('fs');
const {promisify} = require('util');
const readFile = promisify(fs.readFile);
const path = require('path');

class Plugin1 {
apply(compiler) {
compiler.hooks.thisCompilation.tap('Plugin2',(compilation)=>{
// compilation也有自己的生命周期
// 可以对compilation对象上的资源进行操作
compilation.hooks.additionalAssets.tapAsync('Plugin2', async (callback) => {

const file = await readFile(path.resolve(__dirname,'../src/b.txt'));

// 添加额外的打包资源
compilation.assets['b.txt'] = new RawSource(file);
callback()
});
})
}
}

module.exports = Plugin1;

webpack中complier用法

tapable

tapbale暴露了很多钩子,可以为webpack插件创建时使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const {SyncHook,SyncBailHook,AsyncParallelHook,AsyncSeriesHook} = require('tapable')

class Lesson {
constructor(){
// 初始化hooks容器
// 数组中的参数为绑定钩子回调函数的字段描述
this.hooks = {
// 同步hooks,任务依次被执行
go:new SyncHook(['address']),
// 同步hooks,一旦其中一个有返回值,后面的hooks就停止执行
go1:new SyncBailHook(['address']),
// 异步钩子,并行执行
go2:new AsyncParallelHook(['name','age']),
// 异步钩子,并行串行
go3:new AsyncSeriesHook(['name','age']),
}
}
// 添加事件,可以同时绑定多个事件
tap(){
this.hooks.go1.tap('class',(address)=>{
console.log('class',address)
})
this.hooks.go1.tap('class1',(address)=>{
console.log('class1',address)
})

// 异步钩子的两种写法
this.hooks.go2.tapAsync('class3',(name,age,cb)=>{
setTimeout(() => {
console.log('class3',name,age);
cb()
},2000);
})

this.hooks.go2.tapPromise('class4',(name,age)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log('class4',name,age);
},1000);
})
})
}
//触发hooks
start(){
this.hooks.go1.call('触发时间时候传入的参数')
this.hooks.go2.callAsync('Gavin',18)
}
}

const l = new Lesson();
l.tap();
l.start();

Compiler

Compiler 模块是 webpack 的主要引擎,它通过 CLI 传递的所有选项, 或者 Node API,创建出一个 compilation 实例。 它扩展(extend)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。

在插件中使用不同的生命周期钩子来处理资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Plugin1 {
apply(compiler) {
compiler.hooks.initialize.tap('initialize',()=>{
console.log('编译器对象初始化')
})

compiler.hooks.emit.tapAsync(
'emit',
(compilation, callback) => {
setTimeout(() => {
console.log('出发emit事件');
callback();
}, 2000);
}
);
compiler.hooks.afterEmit.tapPromise(
'afterEmit',
(compilation, callback) => {
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log('出发afterEmit事件')
}, 1000);
})
}
);
}
}

module.exports = Plugin1;
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信