跳到主要内容

· 阅读需 4 分钟

当我们的应用随着业务的发展,变得越来越复杂的时候,组件之间的状态也是越来越复杂。

reducer函数

之前说过redux中的action,是用来描述一种变化。但是完成变化并且生辰新的数据数据状态的是reducer方法。

reducer方法,必须是纯函数,才能保证数据变化的可预测性。也就是说执行每一步变化,我们都可以预测数据的结果。比如:

export default function menuReducer(
state = {
menuState,
isCollapse: false,
},
action: any
) {
switch (action.type) {
case types.MENU_COLLAPSES:
return {
...state,
isCollapse: action.payload,
};
default:
return { ...state };
}
}

这reducer函数,是控制菜单的收缩状态的,它对应的action,我是这样写的:

import * as types from "@/store/types";

export const changeCollapse = (isCollapse: boolean) =>({
type: types.MENU_COLLAPSES,
payload: isCollapse,
});

按钮组件的代码如下:

import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { connect } from "react-redux";
import { changeCollapse } from "@/store/redux/menu/action";

const CollapsesIcon = (props: any) => {
const { isCollapses, changeCollapse } = props;
return (
<div
className="collapsed"
onClick={() => {
changeCollapse(!isCollapses)
}}
>
{isCollapses ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
);
};
const mapStateToProps = (state: any) => {
return { isCollapses: state.menus.isCollapse };
};
const mapDispatchToProps = { changeCollapse };
export default connect(mapStateToProps, mapDispatchToProps)(CollapsesIcon);

在一个应用中,我们不可能使用一个action来描述所有的变化,这样不利于维护。一般情况是一个action描述一种变化,同时也会定义对应的reducer函数。

redux提供了一个工具函数combineReducers,来合并所有的reducer函数。比如:

import { combineReducers } from "redux";
import menuReducer from "../redux/menu/menu";
import globalReducer from "../redux/global/reducer";
import { auth } from "../redux/auth";
const reducers = combineReducers({
menus: menuReducer,
global:globalReducer,
auth
});

export default reducers

combineReducers函数接收一个Object类型的参数,这个参数包含了页面的数据状态、已经状态数据和reducer函数之间的映射关系。

这就是redux的纯函数reducer。

中间件

redux中间件就是在派发action和执行reducer之间,添加一些自定义的功能。

比如记录日志、调用异步接口或者路由等等。

redux是通过applyMiddleware方法来接入中间件的,比如:

let store = legacy_createStore(configStore, composeEnhancers(
applyMiddleware(...middleware, reduxPromise)
));

比如现在在开发环境中,想记录一下状态数据变化的日志,可以使用redux-logger:

import { createLogger } from 'redux-logger';
const logger = createLogger();
store = legacy_createStore(configStore, composeEnhancers(
applyMiddleware(...middleware, reduxPromise,logger)
));

这样就是轻松的查看状态数据的变化记录。

但是,需要留意一下,redux-logger需要放在所有中间件的最后面,才能准确打印日志哦。

· 阅读需 3 分钟

Redux的核心是store,store作为应用状态的容器,保存着这个页面的状态数据树。

store

但是store本质上是一个JavaScript对象,这个对象含有了dispatch以及获取页面状态数据的方法等等。

store提供几个方法给开发者调用:

  1. dispatch(action):派发action;
  2. subscribe(listener):订阅页面状态数据,也就是store中的state;
  3. getState:获取当前页面状态数据树;
  4. replaceReducer(nextReducer):这个方法很少使用到。

当应用引入Redux后,通过redux.createStore方法来创建应用的store,这就生成一个对象实例。比如:

    import {createStore}  from  'redux';
const store = createStore(reducer,preloadedState,enhancer);

参数reducer是必传参数,创建store的同时,也必须定义好reducer函数,通过reducer函数,来通知store数据状态是如何根据action来进行更新的。

preloadedState:是应用的初始状态。

action

action是描述了状态变更的信息,通过dispatch来派发。redux规定了action对象需要有一个type属性,来保证action的唯一性。

除此之外,action可以携带其他数据信息,这些数据信息的属性就没有限制了。比如:

const action = {
type: "AAA",
data: {
name:"aaa"
}
}

一般情况下,action携带的信息,是页面动态输入的,所以我们可以定义action相关的函数:

const action = data=>({type:"AAA",data});

之后就通过dispatch来派发action,代码如下:

store.dispatch(action(908));

由此可以看出,action就是描述了一个状态变化,这个状态包含了type属性和变化的信息。

然而,真正将这些变化转为数据状态的是reducer函数。reducer必须是一个纯函数,来保证数据变化的可预测性。比如现在要定义一个变更状态的函数:changesState()。

const changesState = (prevState={},action)=>{
switch(action.type){
case:'AAA':
return:action.data
default:
return prevState;
}
}

一般情况下,reducer函数会处理多了action。

这就是Redux基本使用。

· 阅读需 10 分钟

Hooks规则

React Hooks的使用,有两个规则:

  1. Hooks只能在函数组件中使用;
  2. 不能在条件、循环或者嵌套函数中使用hook。确保每一次渲染中都按照同样的顺序被调用,
import React, { useState } from "react";
export default function PersonalInfoComponent() {
const [name, setName] = useState("读心悦");
const [career, setCareer] = useState("前端");
return (
<div className="personalInfo">
<p>姓名:{name}</p>
<p>职业:{career}</p>
<button
onClick={() => {
setName("duxinyues");
}}
>
修改姓名
</button>
</div>
);
}

这一段代码中的hook是按照正常的顺序来执行的,现在把hook放到条件语句中:

import React, { useState } from "react";

let isMounted = false;
export default function PersonalInfoComponent() {
let name, career, setName;
console.log("isMounted", isMounted);

if (!isMounted) {
// eslint-disable-next-line
[name, setName] = useState("读心悦");
// if 内部的逻辑执行一次后,就将 isMounted 置为 true(说明已挂载,后续都不再是首次渲染了)
isMounted = true;
}
[career] = useState("前端开发");
console.log("career", career);

return (
<div className="personalInfo">
<p>姓名:{name}</p>

<p>职业:{career}</p>
<button
onClick={() => {
setName("duxinyues");
}}
>
修改姓名
</button>
</div>
);
}

这段代码,在首次渲染的时候,是可以正常显示。当我们点击修改按钮后,react就会提示报错:

在这里插入图片描述

从控制台中看出,我们明明修改的是name,最后更新的确实career,这就是违反规则的后果,会造成hooks状态紊乱。

Hooks机制

从源码中,看一hooks的源码,其中有一个Dispatcher,是一个对象,不同的hook调用的函数不同。 全局变量ReactCurrentDispatcher.current的赋值是通过判断是首次渲染还是更新阶段赋不同的值:

源码文件ReactFiberHooks.old.js

    ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

hooks在首次渲染和更新阶段是执行不同的逻辑。

HooksDispatcherOnMount代码:

const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useslug: mountId,

unstable_isNewReconciler: enableNewReconciler,
};

HooksDispatcherOnUpdate代码:

const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useslug: updateId,

unstable_isNewReconciler: enableNewReconciler,
};

useState初次渲染

先看一下useState的源码:

export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

在源码中定义比较简单,先获取当前的dispatcher,再追溯到resolveDispatcher方法,它的源码为:

function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
if (__DEV__) {
if (dispatcher === null) {
....
}
return ((dispatcher: any): Dispatcher);
}

useState在首次渲染,执行的是HooksDispatcherOnMount.useState,也就是mountState方法。再来看一下mountState函数的源码:

function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 将hook追加到链表尾部
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// initialState是回调函数的或=话,就获取回调函数执行的返回值
initialState = initialState();
}
// 把initialState保存下来
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: null,
lanes: NoLanes, // 优先级
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

mountState函数主要是为了初始化hook。再从mountWorkInProgressHook方法中,了解一下hook的数据结构。源码如下:

function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // 不同的hook,有不同的值

baseState: null, // 初始state
baseQueue: null, // 初始队列
queue: null, // 需要更新的队列

next: null, // 下一个hook
};

if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

hook的相关信息,是保存在一个对象中,hook对象之间是以单向链表的形式连接。hook则是保存在Fiber的memoizedState上,需要更新的是保存在hook.queue.pending中。

现在可以看出,组件所有的hook之间是以单向链表形式串联,环环相扣。如果链表上的某一个hook丢失了,那么链表的顺序就发生变化,链表上的hook与之前就不能意义对应,从而导致hook状态紊乱。

useState更新渲染

在更新阶段,使用的dispatcher是HooksDispatcherOnUpdate。我们在组件更新的时候,调用的是useState,实际上就是在调用HooksDispatcherOnUpdate.useState,也就是updateState方法,代码如下:

function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}

在源码中,useState和useReducer是复用一套更新机制,由于useReducer的代码有点多,就不贴出来了。

updateState就是按照顺序遍历之前已经构建好了的链表,取出对应的数据信息进行更新。

小总结: mountState(首次渲染)构建链表并且渲染; updateState(更新渲染)一次遍历链表并且渲染;

useEffect初次渲染

源码中useEffect的定义如下:

export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}

在初次渲染的时候,调用的是mountEffect,mountEffect又调用的是mountEffectImpl,mountEffectImpl代码如下:

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook(); //获取链表
const nextDeps = deps === undefined ? null : deps; // 依赖
currentlyRenderingFiber.flags |= fiberFlags; // 添加flag
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}

mountEffect代码如下:

function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (
__DEV__ &&
enableStrictEffects &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
) {
return mountEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
} else {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
}

在首次渲染,useEffect也是保存在hook.memoizedState上的。

useEffect更新阶段

在更新阶段,调用的是updateEffect,代码如下:

function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;

if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}

currentlyRenderingFiber.flags |= fiberFlags;

hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}

浅比较依赖,如果依赖发生变化,那么就需要重新执行了。

useRef

useRef在首次渲染,调用的是mountRef,代码如下:

function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook(); // 获取useRef
const ref = {current: initialValue}; //初始化ref
hook.memoizedState = ref;
return ref;
}

在render的时候,带有ref属性的Fiber就会标记上Ref tag。

更新阶段的时候,调用的是updateRef,然后返回hook链表。

function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}

useMemo & useCallback

首次渲染useCallback和useMemo的源码:

function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}

它们的区别是在memoizedState上存储的是callback还是value。

更新阶段的源码:

function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}

这就是常用到的hooks的源码以及简单解析。

· 阅读需 4 分钟

合成事件

React有自己一套独立的事件系统,就是合成事件,它模拟原生事件所有的功能。根据W3C规范来定义合成事件,兼容了所有的浏览器,拥有和浏览器原生事件相同的接口。

在react中,可以通过e.nativeEvent获取到DOM事件,比如:<div onClick={(e)=>{console.log(e.nativeEvent)}}>点击</div>

在这里插入图片描述

合成事件的目的是:

  1. 实现浏览器兼容,方便跨平台。react采取顶层事件机制,保证事件冒泡一致性,可以跨浏览器执行。把不同平台事件,模拟合成事件。
  2. 方便事件统一管理和事务机制。

DOM原生事件

事件流:页面中绑定了很多的事件,而页面接收事件的顺序就是事件流。

事件传播大致分为三个阶段:

  1. 事件捕获阶段
  2. 目标阶段
  3. 事件冒泡阶段

首先,当一个事件被触发的时候,顶层对象发出一个事件流,一直流向目标节点,直到事件真正发生的目标元素;到达目标元素后,就执行目标元素该事件对应的处理逻辑;

事件冒泡就是事件从目标元素开始,往顶层元素传播,途中遇到节点绑定的事件,就会触发一次。如果需要阻止事件冒泡的话,就使用e.stopPropagation()来阻止事件冒泡。

事件代理:把响应事件委托到一个元素上,当子元素触发事件的时候,事件就向上冒泡,父节点获取到事件后,再判断是否是对应节点的事件,然后进行处理。事件代理的优点在于它可以减少内存消耗和动态绑定事件。

原生事件和合成事件区别

  1. 事件命名方式不同,原生事件命名纯小写,react事件命名方式是小驼峰,比如onClick;
  2. 事件处理函数不一样,原生事件的处理函数为字符串,在react中,是传入一个函数作为事件处理函数;
  3. 阻止默认行为不一样,原生事件可以通过返回false来阻止事件的默认行为。在react中,需要使用preventDefault()方法来阻止事件的默认行为。

react合成事件,是以事件委托的方式绑定在组件最上层。并且在组件卸载阶段会自动销毁。

react的所有事件都是绑定在document上【react17之后,事件是绑定在root容器上】,当真实DOM元素上触发事件的时候,就会冒泡到document或者是root容器上。然后再来处理react事件;

· 阅读需 4 分钟

整理一些react相关问题

jsx和Fiber的关系

jsx就是JavaScript的扩展语法,是React.createElement的语法糖,我们可以直接通过html的格式来表现节点结构和信息。

Fiber,就是用来存储节点的信息以及节点的优先级。

在初次渲染的时候,通过jsx对象来调用createFiberFromElement生成Fiber。在更新阶段,通过对比新的jsx和老的jsx,生成新的 Fiber树。

react17后可以不用引入“import React from ‘react’”

之前jsx通过编译后,变成了React.createElement,这样不引入react,就会报错。

react17改变了编译的方式,变成了jsx.createElement。

Fiber为什么可以提高性能?

Fiber是一个js对象,包含了节点信息、优先级、更新队列,也是一个工作单元。

Fiber在构建好workInProgress Fiber树后就切换成current Fiber,在内存中一次性切换,这样可以提高性能。

Fiber可以中断异步更新,作为工作单元,可以在时间片段内执行,结束后还没有完成的工作就暂停,在下次时间片段执行之前的工作,最后返回Fiber。Fiber可以在协调的时候,进行对应的diff,让最后的更新应用到真实DOM上。

hooks为什么不能写在条件判断中

因为hooks是按照顺序存储在链表中的,如果把hook放在条件判断中,就不无法保持链表的顺序。

setState是异步还是同步的

是异步的,因为react把多个state合并更新的

为什么部分生命周期函数标记了UNSAFE

新的Fiber架构可以在schedule的调度下实现暂停、继续,排列优先级。当优先级高的任务打断了优先级低的任务时,优先级低的人物就会被跳过,所有的生命周期可能执行多次,

标记UNSAFE表示这些生命周期过时,但是依然有效。

React元素的$$typeof属性

$$typeof属性,表示react元素类型,是一个symbol类型

React是如何区分类组件和函数组件的

类组件prototype上有isReactComponent属性

函数组件和类型的相同与不同

相同的地方就是都可以接收props和返回react元素。

类组件是面向对象编程,需要创建实例并且保存实例,占用了一定的内存;

函数组件,不用创建实例,直接输入返回输出,是函数式编程

· 阅读需 6 分钟

组件生命,就是组件在不同阶段提供对应的钩子函数,来处理逻辑操作。比如初始化阶段,我们需要初始化组件相关的状态和变量。组件销毁阶段时,我们需要把一些数据结构销毁来节约内存。

React组件生命周期分为三个阶段:挂载阶段【Mount】、更新阶段【Update】和卸载阶段【Umount】。

组件挂载阶段

挂载阶段,就是组件实例化并且挂载到DOM树的这个过程。在这个阶段中依次调用生命周期函数为:

  1. constructor()
  2. getDerivedStateFromProps()
  3. render()
  4. componentDidMount()
constructor(props)

constructor,是构造函数,用来初始化组件的状态和操作。React类组件一般是通过继承React.Component来创建的,所以需要调用super(props),初始化父类。

在constructor函数中,我们可以初始化state,同时可以对操作函数进行bind绑定。但是需要注意的是,不能在constructor中调用setState,否则会报错,因为setState是用来更新状态的。

操作函数绑定如下:

import React from "react";
interface PropsType {
name: string;
}
class App extends React.Component<any, PropsType> {
constructor(props: any) {
super(props);
this.state = {
name: "Duxinyues",
};

this.handle.bind(this);
}
handle = function(param:any) {
console.log(param);
};
render(): React.ReactNode {
console.log("this.state", this.state);
const { name } = this.state;
return (
<div>
{name}
<button onClick={()=>this.handle(345)}>点击</button>
</div>
);
}
}
export default App;

如果在不想在constructor中绑定this的话,也可以在使用操作函数的时候绑定this。比如:

 <button onClick={this.handle.bind(this,423)}>点击</button>

或者使用箭头函数来定义操作函数,因为箭头函数的this指向的是它定义时所在的上下文环境,而不是指向它的调用者。

 handle = (param:any) =>{
console.log(param);
};

当然,也可以不用再constructor中初始化状态,组件会默认初始化的。


getDerivedStateFromProps()

这个生命周期函数是在初始化后或者是在更新阶段中,接收新props后返回一个对象作为新的state。如果返回的是null,那么说明组件不用更新state。

另外,getDerivedStateFromProps()是static方法,在内部是不能获取到this的。


render

render方法是根据状态state和属性props来渲染组件。如果state和props不变,那么render返回的结果都是一样。需要注意的是不要在render方法内改变组件状态,也不要在render方法内直接个浏览器进行交互。


componentDidMount()

componentDidMount函数在组件挂载后调用,也就是在render方法后调用,而且是在组件整个生命周期内只调用一次。一般情况下,我们会在这个方法内进行网络请求等等。


更新阶段

当组件的状态或者属性变化的时候,会导致组件更新,依次调用生命周期函数:

  1. getDerivedStateFromProps()
  2. componentWillUpdate()
  3. render()
  4. getSnapshotBeforeUpdate()
  5. componentDidUpdate()

shouldComponentUpdate(nextProps,nextState)

在组件更新之前,我们可以通过shouldComponentUpdate()函数来判断组件是否需要更新,它默认返回true,每次状态和属性发生变化的时候,组件都重新render一下。如果返回false,那么组件就不会更新。

另一种方法就是用React.PureComponent来创建组件,PureComponent默认进行state和props的比较这是组件优化的一种手段。


getSnapshotBeforeUpdate()

这个方法是在render之后,dom渲染之前返回一个值,作为componentDidUpdate函数的第三个参数,比如官方的例子:

class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动​​位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
在上述示例

卸载阶段

componentWillUnmount(),这个方法在组件卸载阶段中调用

捕获错误

React有一个生命周期函数componentDidCatch(error,info)。

React组件产生错误没有被捕获,那么就抛给上层组件,如果上层组件也没有捕获,就会抛到顶层组件,最终导致浏览器白屏。

· 阅读需 6 分钟

React,用来构建用户界面,它有三个特点:

  1. 作为view,构建上用户界面
  2. 虚拟DOM,目的就是高性能DOM渲染【diff算法】、组件化、多端同构
  3. 单向数据流,是一种自上而下的渲染方式。

Flux

在一个React应用中,UI部分是由无数个组件嵌套构成的,组件和组件之间就存在层级关系,也就是父组件、子组件以及顶层组件。

当我们获取到数据后,根据React自上而下的渲染方式,我们把数据传给顶层组件,由顶层组件通过属性props,将数据传给各个子组件,这样数据就一层一层传递下去,各个子组件获取自己所需的数据,最终完成UI的渲染。

当有一个数据发生了变化后,是如何通知组件更新呢?思路如下:

  1. 定义一个数据存储store,它存储的是应用的当前的状态state;
  2. 用户操作导致数据变更的时候,就会触发一个action,告知store有数据变化了,以及哪些数据发生变化;
  3. store接收到通知后,就更新修改数据,返回新的数据传递到顶层组件。再进行一次自上而下的渲染,从而达到更新UI。

Flux是一种数据处理的模式,描述的是单向数据流的思想。让数据流变得简单,方便调试和追踪,所以它更适合与react结合使用。

Flux中,引入的dispatch、action、store和view模块,触发action时,需要使用dispatch来调度

Redux

Redux是FLux的一种实现形式,保留了数据流的单向性,除了flux原本的几个模块,还添加了reducers和middlewares。

store

store,作为一个数据存储中心,连接action和组件【view】。它接收组件传来的action,然后根据action.type和action.payload来对store中的数据进行更新。最后store通知组件有数据变化。组件就获取最先的数据,来完成重新新渲染。

reducer修改数据后,在组件中调用subscribe方法,subscribe传入一个回调函数。在每次调用dispatch时,subscribe传入的回调函数被触发,这样就完成了reducer修改数据后,通知组件获取最新数据,实现组件重新渲染。

小总结一下:

  1. store中数据的修改,是通过reducer来完成的,调用dispatch(action),action->reducers->store
  2. store提供了消息发布和订阅功能,来实现连接action和组件,dispatch触发消息发布,subscribe触发消息订阅。

Reducer

reducer是一个纯函数,用来修改store数据的。它有优势:数据拆解。

redux有一个原则,就是单一的数据源,整个应用,只有一个store,存储着所有的数据。如果一个应用存在多个应用的话,并且store之间存在数据关联关系,那么处理起来比较麻烦。

单一数据源,也有一个问题,那就是数据结构嵌套太深,导致数据访问繁琐。

所redux提出通过定义多个reducer对数据进行访问和修改,最后通过combineReducers函数,来组合所有的数据。比如:

import { combineReducers } from "redux";
import menuReducer from "./menu";
const reducers = combineReducers({
menus: menuReducer,
});

export default reducers

这就是redux的数据拆解。

Middleware

Middleware是中间件,Middleware处理对象是action,通过对action的type属性进行判断,采取不同的操作。

React-redux

为了让redux和react更好的结合,就需要引入React-redux,它提供了两个API:Provider和connect

Provider

Provider组件作用就是把唯一的数据源store传给任意组件。

connect

connect作用就是让组件和store进行关联。需要定义两个函数mapStateToProps和mapDispatchToProps,如果不想使用connect的话,也可以通过useSelector,useDispatch,也就是React-redux对应的hooksAPI,来完成组件和store关联。

· 阅读需 0 分钟

· 阅读需 3 分钟

我的技术栈是React,最近在整理react的源码,react版本是18.1.0,之前版本,没有看过,就此略过。

源码目录

从github将源码下载后,先看看源码目录结构,如下图所示: 在这里插入图片描述 fixtures:代码贡献者提供的测试react package:react源码的主要部分,包含了Schedule、reconcile等等 scripts:react构建相关

查阅源码,我们主要查阅package目录下即可。package目录如下: 在这里插入图片描述 react:核心的API,比如:Children,Component,Fragment,Profiler,PureComponent,StrictMode,Suspense等等

react-art:canvas、svg的渲染; react-dom:浏览器环境; react-native-renderer:原生客户端相关的; react-noop-renderer:调试或者fiber; react-server:ssr相关; react-fetch:请求相关; react-interactions:和事件相关,比如点击事件; react-reconciler:构建节点; shared:包含公共方法和变量; react-is:判断类型; react-client:流相关; react-refresh:热加载相关 scheduler:调度器相关

核心API

从react目录的index.js中找到React暴露出来的API:

export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
act as unstable_act,
Children,
Component,
Fragment,
Profiler,
PureComponent,
StrictMode,
Suspense,
SuspenseList,
cloneElement,
createContext,
createElement,
createFactory,
createMutableSource,
createRef,
createServerContext,
forwardRef,
isValidElement,
lazy,
memo,
startTransition,
unstable_Cache,
unstable_DebugTracingMode,
unstable_LegacyHidden,
unstable_Offscreen,
unstable_Scope,
unstable_TracingMarker,
unstable_getCacheSignal,
unstable_getCacheForType,
unstable_useCacheRefresh,
useId,
useCallback,
useContext,
useDebugValue,
useDeferredValue,
useEffect,
useImperativeHandle,
useInsertionEffect,
useLayoutEffect,
useMemo,
useMutableSource,
useSyncExternalStore,
useReducer,
useRef,
useState,
useTransition,
version,
} from './src/React';

是否找到自己常用的API呢?比如Children、memo、useRef等等。

调试源码

如果想自己调试源码的话,就需要先安装依赖:yarn或者npm install

build源码:npm run build react/index,react/jsx,react-dom/index,scheduler --type=NODE

给源码创建软链: 切换到build/node_modules/react,执行命令:npm link 切换到build/node_modules/react-dom,执行命令:npm link

使用create-react-app脚手架创建项目:

npx create-react-app demo
npm link react react-dom

使用创建项目时候。默认是安装了react的最新版本,从package.json文件中可以看出: 在这里插入图片描述 执行npm link react react-dom后,在依赖文件夹下找到react目录,查看react的实际版本。

查看该项目下react的版本,可以从version得到,因为version是react暴露出来的API,直接使用即可:

import { version } from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<p>React:版本{version}</p>
</header>
</div>
);
}

export default App;

在这里插入图片描述

· 阅读需 8 分钟

react15的协调【reconcile】过程是不能打断的,那么在进行大量节点更新的时候,就会造成卡顿,因为浏览器所有的时间都用来执行js,而js的执行是单线程的,所以在协调过程中就会卡顿。

react16之后,添加schedule过程,也就是调度过程,给每一个工作单元一定的时间,在这个时间内没有执行完成的,就暂时跳出来。那么异步中断的更新需要一定的数据结构来存储工作单元的信息,这就是Fiber。

Fiber的作用:

  1. 作为工作单元,存储了节点信息以及节点的优先级,这些节点通过指针形式构成了Fiber树;
  2. 增量渲染更新,通过jsx对象和currentFiber对比,找出差异,并且应用到真实DOM上去
  3. 根据节点的优先级暂停、继续、排序优先级:因为Fiber上存有优先级,那么就可以通过不同节点的优先级的对比,来完成任务的暂停、继续、排列优先级,为上层实现批量更新、Suspense提供基础;
  4. 保存状态:Fiber上有状态和更新信息,就可以实现函数组件的状态更新,这就是hooks。

Fiber结构

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 实例
this.tag = tag; // 标记不同的组件类型
this.key = key; // reactElement的key,也就是key属性
this.elementType = null; // 元素类型,是createElement函数的第一个参数
this.type = null; // 异步组件resolve返回的内容,是function或者class
this.stateNode = null; // 真实dom节点

// Fiber树的结构
this.return = null; // 指向父节点,处理完该节点后,向上返回
this.child = null; // 指向child
this.sibling = null; // 指向兄弟节点
this.index = 0;

this.ref = null; // ref属性

this.pendingProps = pendingProps; // 新变动带来新的props
this.memoizedProps = null; // 上次渲染后的props
this.updateQueue = null; // 更新队列存放组件产生的update
this.memoizedState = null; // 上次渲染的state
this.dependencies = null; //

this.mode = mode; //

// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;

//优先级
this.lanes = NoLanes;
this.childLanes = NoLanes;
// current和workInProgress的指针
this.alternate = null;

// 启动分析器时间
if (enableProfilerTimer) {
this.actualDuration = Number.NaN; // 实际持续时间
this.actualStartTime = Number.NaN; // 实际开始时间
this.selfBaseDuration = Number.NaN; //
this.treeBaseDuration = Number.NaN;

this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}

if (__DEV__) {
// This isn't directly used but is handy for debugging internals:

this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}

这就是Fiber的结构。

创建FiberRoot和rootFiber

Fiber保存的是dom节点信息,当前dom对应的Fiber树是current Fiber,正在构建的Fiber树是workInProgress Fiber,它是在createWorkInProgress中完成。

首次渲染的时候,会创建fiberRootNode和rootFiber,fiberRootNode是应用的根节点,rootFiber是,之后根据jsx对象创建Fiber节点,Fiber节点连接形成Fiber树。

先创建fiberRoot,在ReactFiberRoot.old.js中找到createFiberRoot函数,这个函数就是创建FiberRoot,源码如下:

function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
console.log("创建FiberRootNode节点")
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}

if (enableTransitionTracing) {
root.transitionCallbacks = transitionCallbacks;
}

const uninitializedFiber = createHostRootFiber(
tag,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

if (enableCache) {
const initialCache = createCache();
retainCache(initialCache);

root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
transitions: null,
pendingSuspenseBoundaries: null,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: (null: any), // not enabled yet
transitions: null,
pendingSuspenseBoundaries: null,
};
uninitializedFiber.memoizedState = initialState;
}

initializeUpdateQueue(uninitializedFiber);

return root;
}

里面的createHostRootFiber方法,是创建rootFiber的。在react的入口文件中,我们调用的是ReactDOM的createRoot方法。这是react应用默认的入口代码:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();

在createRoot方法中,是通过createContainer方法来创建root的,源码如下:

function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
// 其他代码省略
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
// 其他代码省略
return new ReactDOMRoot(root);
}

再来看看createContainer的源码:

export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
}

这就Fiber最开始创建流程。

创建workInProgress Fiber

workInProgress Fiber,是组件发生更新时,生成新的jsx和current Fiber树进行对比【diff算法】后,生成workInProgress Fiber,然后FiberRoot的current指向workInProgress Fiber,那么workInProgress Fiber就变成的current Fiber,从而完成更新。完整源码如下:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) { // 是否是首次渲染还是更新阶段
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;

if (__DEV__) {
// DEV-only fields

workInProgress._debugSource = current._debugSource;
workInProgress._debugOwner = current._debugOwner;
workInProgress._debugHookTypes = current._debugHookTypes;
}

workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps; // 复用属性
// Needed because Blocks store data on type.
workInProgress.type = current.type;

workInProgress.flags = NoFlags;

// The effects are no longer valid.
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;

if (enableProfilerTimer) {
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = -1;
}
}

workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;

workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;

const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};

// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;

if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}

if (__DEV__) {
workInProgress._debugNeedsRemount = current._debugNeedsRemount;
switch (workInProgress.tag) {
case IndeterminateComponent:
case FunctionComponent:
case SimpleMemoComponent:
workInProgress.type = resolveFunctionForHotReloading(current.type);
break;
case ClassComponent:
workInProgress.type = resolveClassForHotReloading(current.type);
break;
case ForwardRef:
workInProgress.type = resolveForwardRefForHotReloading(current.type);
break;
default:
break;
}
}

return workInProgress;
}

这就是Fiber数据结构完整流程