彻底搞懂ahooks State实现原理前言 声明:本文所有代码块均来自ahooks 3.8.1版本。 上一篇我们详细了
前言
声明:本文所有代码块均来自ahooks 3.8.1
版本。
上一篇我们详细了解了ahooks
的所有副作用Effect
钩子:
今天来讲解所有的状态State Hook
。
useSetState
管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState
基本一致。
源码如下:
import { useCallback, useState } from 'react';
import { isFunction } from '../utils';
export type SetState<S extends Record<string, any>> = <K extends keyof S>(
state: Pick<S, K> | null | ((prevState: Readonly<S>) => Pick<S, K> | S | null),
) => void;
const useSetState = <S extends Record<string, any>>(
initialState: S | (() => S),
): [S, SetState<S>] => {
const [state, setState] = useState<S>(initialState);
const setMergeState = useCallback((patch) => {
setState((prevState) => {
const newState = isFunction(patch) ? patch(prevState) : patch;
return newState ? { ...prevState, ...newState } : prevState;
});
}, []);
return [state, setMergeState];
};
export default useSetState;
源码不复杂,入参和出参与React
自带的useState
一致,接收一个初始状态initialState
,可以是一个状态对象或者是返回状态对象的函数,同时定制化了set
函数,声明了一个setMergeState
函数。
- 使用
useCallback
创建了一个记忆化函数,确保当依赖项没有变化时不重新创建; - 接收
patch
参数,表示要合并到当前状态的部分内容,这个参数可以是状态部分对象; - 如果
patch
是一个函数,则会执行这个函数并获取新状态; - 最后在合并状态时会将老状态和新状态合并,如果新状态是
null
,则返回老状态;
useToggle
用于在两个状态值之间切换的 Hook。
用法如下:
const [state, { toggle, setLeft, setRight }] = useToggle('Hello', 'World');
源码如下:
import { useMemo, useState } from 'react';
export interface Actions<T> {
setLeft: () => void;
setRight: () => void;
set: (value: T) => void;
toggle: () => void;
}
function useToggle<T = boolean>(): [boolean, Actions<T>];
function useToggle<T>(defaultValue: T): [T, Actions<T>];
function useToggle<T, U>(defaultValue: T, reverseValue: U): [T | U, Actions<T | U>];
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
const [state, setState] = useState<D | R>(defaultValue);
const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);
return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// }, [defaultValue, reverseValue]);
}, []);
return [state, actions];
}
export default useToggle;
1. 接口定义
export interface Actions<T> {
setLeft: () => void; // 设置状态为默认值
setRight: () => void; // 设置状态为反转值
set: (value: T) => void; // 设置状态为指定值
toggle: () => void; // 切换状态
}
这个接口定义了 Actions
类型,它包含了四个函数来操作状态。
2. useToggle 函数重载
function useToggle<T = boolean>(): [boolean, Actions<T>];
function useToggle<T>(defaultValue: T): [T, Actions<T>];
function useToggle<T, U>(defaultValue: T, reverseValue: U): [T | U, Actions<T | U>];
这里使用了 TypeScript 的重载功能,定义了三个不同的 useToggle
函数签名,以支持不同的用法。可以:
- 不传参数,默认值为
false
。 - 传入一个默认值。
- 传入默认值和一个反转值。
3. 实现
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
const [state, setState] = useState<D | R>(defaultValue);
这里可以看到,useState
被用来管理当前的状态,状态初始化为 defaultValue
。
4. 操作函数
const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);
return {
toggle,
set,
setLeft,
setRight,
};
}, []);
在这个 useMemo
中,定义了四个操作函数:
toggle
:切换状态,如果当前状态是defaultValue
,则设置为reverseValueOrigin
,反之则设置为defaultValue
。set
:直接设置状态为给定值。setLeft
:设置状态为defaultValue
。setRight
:设置状态为reverseValueOrigin
。
5. 返回值
return [state, actions];
最后,返回当前状态和操作对象。
useBoolean
是ahooks
专门用于优雅地管理boolean
的hook
。
而useBoolean
与useToggle
非常类似,在实现上也是直接将useBoolean
的核心逻辑托管给了
useToggle
。
基本用法如下:
const [state, { toggle, setTrue, setFalse }] = useBoolean(true);
源码实现如下:
import { useMemo } from 'react';
import useToggle from '../useToggle';
export interface Actions {
setTrue: () => void;
setFalse: () => void;
set: (value: boolean) => void;
toggle: () => void;
}
export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(!!defaultValue);
const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);
return [state, actions];
}
暴露出去的函数和useToggle
完全一致,只是把值固定成了boolean
类型。
useCookieState
useCookieState
用于管理 cookie 的状态。它结合了 React 的 state 和 js-cookie
库来读取和写入 cookies。
源码如下:
import Cookies from 'js-cookie';
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';
import { isFunction, isString } from '../utils';
export type State = string | undefined;
export interface Options extends Cookies.CookieAttributes {
defaultValue?: State | (() => State);
}
function useCookieState(cookieKey: string, options: Options = {}) {
const [state, setState] = useState<State>(() => {
const cookieValue = Cookies.get(cookieKey);
if (isString(cookieValue)) return cookieValue;
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
});
const updateState = useMemoizedFn(
(
newValue: State | ((prevState: State) => State),
newOptions: Cookies.CookieAttributes = {},
) => {
const { defaultValue, ...restOptions } = { ...options, ...newOptions };
const value = isFunction(newValue) ? newValue(state) : newValue;
setState(value);
if (value === undefined) {
Cookies.remove(cookieKey);
} else {
Cookies.set(cookieKey, value, restOptions);
}
},
);
return [state, updateState] as const;
}
export default useCookieState;
类型定义
-
State:
- 可以是
string
或undefined
。它表示 cookie 的值。
- 可以是
-
Options:
- 继承了
Cookies.CookieAttributes
,用于配置 cookie 的属性(如过期时间、路径等)。 defaultValue
:可以是一个初始值或者一个返回初始值的函数。
- 继承了
Hook 的功能
-
初始化状态:
- 使用
useState
Hook 初始化状态,通过读取指定 cookie 的值。 - 如果 cookie 存在且为字符串,则使用它的值;如果 cookie 不存在并且
defaultValue
是一个函数,则调用这个函数得到值;如果defaultValue
不是函数,则直接使用defaultValue
指定的值。
- 使用
-
更新状态:
- 提供一个
updateState
函数,用于更新 cookie 和内部状态。 newValue
可以是新值或一个返回新值的函数(接受前一个状态作为参数)。- 合并
newOptions
和options
,然后设置 cookie。若新值是undefined
,则删除指定的 cookie。
- 提供一个
-
返回值:
- 返回一个数组,包含当前状态和更新状态的函数。
核心逻辑流程
- 当组件第一次渲染时,
useCookieState
会读取 cookie 的值并设置为状态。 - 当调用
updateState
时,会根据传入的新值或函数更新状态及 cookie。 - 如果状态被更新为
undefined
,则删除对应的 cookie。
useLocalStorageState
用于同步localStorage
数据到React
状态中的Hook
,源码入口是调用了一个公共函数,如下:
import { createUseStorageState } from '../createUseStorageState';
import isBrowser from '../utils/isBrowser';
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));
export default useLocalStorageState;
可以看到,熟悉的模块划分,也不难想象到这里useSessionStorageState
一定也通用了这个函数,createUseStorageState
源码如下:
import { useState } from 'react';
import useEventListener from '../useEventListener';
import useMemoizedFn from '../useMemoizedFn';
import useUpdateEffect from '../useUpdateEffect';
import { isFunction, isUndef } from '../utils';
export const SYNC_STORAGE_EVENT_NAME = 'AHOOKS_SYNC_STORAGE_EVENT_NAME';
export type SetState<S> = S | ((prevState?: S) => S);
export interface Options<T> {
defaultValue?: T | (() => T);
listenStorageChange?: boolean;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
}
export function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined;
const {
listenStorageChange = false,
onError = (e) => {
console.error(e);
},
} = options;
// https://github.com/alibaba/hooks/issues/800
try {
storage = getStorage();
} catch (err) {
onError(err);
}
const serializer = (value: T) => {
if (options.serializer) {
return options.serializer(value);
}
return JSON.stringify(value);
};
const deserializer = (value: string): T => {
if (options.deserializer) {
return options.deserializer(value);
}
return JSON.parse(value);
};
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
onError(e);
}
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
}
const [state, setState] = useState(getStoredValue);
useUpdateEffect(() => {
setState(getStoredValue());
}, [key]);
const updateState = (value?: SetState<T>) => {
const currentState = isFunction(value) ? value(state) : value;
if (!listenStorageChange) {
setState(currentState);
}
try {
let newValue: string | null;
const oldValue = storage?.getItem(key);
if (isUndef(currentState)) {
newValue = null;
storage?.removeItem(key);
} else {
newValue = serializer(currentState);
storage?.setItem(key, newValue);
}
dispatchEvent(
// send custom event to communicate within same page
// importantly this should not be a StorageEvent since those cannot
// be constructed with a non-built-in storage area
new CustomEvent(SYNC_STORAGE_EVENT_NAME, {
detail: {
key,
newValue,
oldValue,
storageArea: storage,
},
}),
);
} catch (e) {
onError(e);
}
};
const syncState = (event: StorageEvent) => {
if (event.key !== key || event.storageArea !== storage) {
return;
}
setState(getStoredValue());
};
const syncStateFromCustomEvent = (event: CustomEvent<StorageEvent>) => {
syncState(event.detail);
};
// from another document
useEventListener('storage', syncState, {
enable: listenStorageChange,
});
// from the same document but different hooks
useEventListener(SYNC_STORAGE_EVENT_NAME, syncStateFromCustomEvent, {
enable: listenStorageChange,
});
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
核心原理是自定义了一个ahook storage
的更新事件,名为SYNC_STORAGE_EVENT_NAME
,接下来我们详细拆解下每个模块
1. 导入模块
import { useState } from 'react';
import useEventListener from '../useEventListener';
import useMemoizedFn from '../useMemoizedFn';
import useUpdateEffect from '../useUpdateEffect';
import { isFunction, isUndef } from '../utils';
useState
: React 的状态管理 Hook。useEventListener
: 自定义 Hook,用于监听事件。useMemoizedFn
: 可能是一个优化 Hook,用于避免函数在每次渲染时重新创建。useUpdateEffect
: 可能是一个自定义 Hook,仅在组件更新时执行副作用。isFunction
和isUndef
: 工具函数,用于检查一个值是否为函数或未定义。
2. 常量和类型定义
export const SYNC_STORAGE_EVENT_NAME = 'AHOOKS_SYNC_STORAGE_EVENT_NAME';
export type SetState<S> = S | ((prevState?: S) => S);
export interface Options<T> {
defaultValue?: T | (() => T);
listenStorageChange?: boolean;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
}
SYNC_STORAGE_EVENT_NAME
: 同步存储的事件名称。SetState<S>
: 定义状态的类型,可以是一个值或一个接受当前状态的函数。Options<T>
: 用于配置 Hook 行为的选项,包含默认值、是否监听存储变化、序列化和反序列化函数,错误处理函数等。
3. 主函数 createUseStorageState
export function createUseStorageState(getStorage: () => Storage | undefined) {
- 这是一个工厂函数,接受一个参数
getStorage
,用于获取存储对象(例如localStorage
或sessionStorage
)。
4. useStorageState
Hook
function useStorageState<T>(key: string, options: Options<T> = {}) {
这是具体的自定义 Hook 函数,实现了对存储状态的管理。
4.1 读取存储对象
let storage: Storage | undefined;
const {
listenStorageChange = false,
onError = (e) => {
console.error(e);
},
} = options;
try {
storage = getStorage();
} catch (err) {
onError(err);
}
- 尝试获取
storage
对象,并处理可能的错误。
4.2 定义序列化和反序列化函数
const serializer = (value: T) => {
if (options.serializer) {
return options.serializer(value);
}
return JSON.stringify(value);
};
const deserializer = (value: string): T => {
if (options.deserializer) {
return options.deserializer(value);
}
return JSON.parse(value);
};
- 如果传入了自定义的序列化和反序列化函数,将调用这些函数,否则使用默认的 JSON 方法。
4.3 获取存储值
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
onError(e);
}
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
}
- 尝试从存储中获取值,并返回反序列化后的结果。如果没有值,则返回默认值。
4.4 状态管理
const [state, setState] = useState(getStoredValue);
- 使用
useState
创建存储状态。
4.5 更新状态和同步存储
const updateState = (value?: SetState<T>) => {
const currentState = isFunction(value) ? value(state) : value;
if (!listenStorageChange) {
setState(currentState);
}
// 更新 storage
// 发送自定义事件以同步数据到同一页面的其他组件
};
updateState
函数用于更新状态并在 Storage 中同步值。如果启用了存储变化监听,会同步更新。
4.6 监听 Storage 事件
useEventListener('storage', syncState, { enable: listenStorageChange });
useEventListener(SYNC_STORAGE_EVENT_NAME, syncStateFromCustomEvent, { enable: listenStorageChange });
- 监听来自其他文档的
storage
事件和自定义同步事件。
5. 返回值
return [state, useMemoizedFn(updateState)] as const;
- 返回当前状态和更新状态的函数,使用
useMemoizedFn
确保函数的引用在依赖未改变时不会变化。
useSessionStorageState
用法与useLocalStorageState
完全一致,切换为sessionStorage
存储,不再过多讲解。
源码如下:
import { createUseStorageState } from '../createUseStorageState';
import isBrowser from '../utils/isBrowser';
const useSessionStorageState = createUseStorageState(() =>
isBrowser ? sessionStorage : undefined,
);
export default useSessionStorageState;
useDebounce
上一篇文章介绍过useDebounceFn
和useThrottle
,基于lodash
实现了防抖、节流的通用能力,useDebounce
实际就是将能力包了一层,将值暴露出去。
源码如下:
import { useEffect, useState } from 'react';
import useDebounceFn from '../useDebounceFn';
import type { DebounceOptions } from './debounceOptions';
function useDebounce<T>(value: T, options?: DebounceOptions) {
const [debounced, setDebounced] = useState(value);
const { run } = useDebounceFn(() => {
setDebounced(value);
}, options);
useEffect(() => {
run();
}, [value]);
return debounced;
}
export default useDebounce;
useThrottle
与useDebounce
原理一致,不再过多讲解。
源码如下:
import { useEffect, useState } from 'react';
import useThrottleFn from '../useThrottleFn';
import type { ThrottleOptions } from './throttleOptions';
function useThrottle<T>(value: T, options?: ThrottleOptions) {
const [throttled, setThrottled] = useState(value);
const { run } = useThrottleFn(() => {
setThrottled(value);
}, options);
useEffect(() => {
run();
}, [value]);
return throttled;
}
export default useThrottle;
useMap
用于管理JavaScript
哈希表的Hook
,具体用法和实现比较简单,核心思路就是在React
状态机制中维护一个哈希表,进行增删改查,源码如下:
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
const getInitValue = () => new Map(initialValue);
const [map, setMap] = useState<Map<K, T>>(getInitValue);
const set = (key: K, entry: T) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, entry);
return temp;
});
};
const setAll = (newMap: Iterable<readonly [K, T]>) => {
setMap(new Map(newMap));
};
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
const reset = () => setMap(getInitValue());
const get = (key: K) => map.get(key);
return [
map,
{
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
] as const;
}
export default useMap;
实现流程如下:
1. 引入依赖
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';
useState
:React 的 Hook,用于在组件内部管理状态。useMemoizedFn
:假设这是一个自定义 Hook,用于在依赖不变时返回相同的函数引用,以避免不必要的重新渲染。
2. 定义 useMap
函数
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
- 这个函数接受一个可选的
initialValue
参数,它是一个可迭代的键值对集合,默认为undefined
。 K
和T
是泛型类型,用于定义Map
的键和值的类型。
3. 初始化状态
const getInitValue = () => new Map(initialValue);
const [map, setMap] = useState<Map<K, T>>(getInitValue);
getInitValue
:这是一个函数,它将initialValue
转换成一个新的Map
对象。useState
用于创建一个名为map
的状态,并提供setMap
方法来更新map
。
4. 定义操作方法
设置单个项
const set = (key: K, entry: T) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, entry);
return temp;
});
};
set
方法通过调用setMap
更新map
。它复制当前的map
,然后使用set
方法将新值添加到一个临时Map
中,并返回这个新的Map
。
设置多个项
const setAll = (newMap: Iterable<readonly [K, T]>) => {
setMap(new Map(newMap));
};
setAll
方法将传入的新键值对集合转换成一个Map
并直接更新map
。
删除项
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
remove
方法从当前map
中删除指定的键。
重置 Map
const reset = () => setMap(getInitValue());
reset
方法将map
重置为初始化时的值。
获取某个值
const get = (key: K) => map.get(key);
get
方法通过给定的键返回对应的值。
5. 返回值
return [
map,
{
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
] as const;
- 返回一个包含
map
和操作方法的元组。操作方法使用useMemoizedFn
包装,以确保这些函数在依赖未变时不会重新创建,从而改善性能及避免不必要的渲染。
useSet
实现原理和思路与useMap
类似,也是基于一个React state
来维护一个Set
,源码如下:
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';
function useSet<K>(initialValue?: Iterable<K>) {
const getInitValue = () => new Set(initialValue);
const [set, setSet] = useState<Set<K>>(getInitValue);
const add = (key: K) => {
if (set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.add(key);
return temp;
});
};
const remove = (key: K) => {
if (!set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.delete(key);
return temp;
});
};
const reset = () => setSet(getInitValue());
return [
set,
{
add: useMemoizedFn(add),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
},
] as const;
}
export default useSet;
usePrevious
用于保存上一次状态的Hook
,实现思路是通过useRef
来留存每一次的老状态。
源码如下:
import { useRef } from 'react';
export type ShouldUpdateFunc<T> = (prev: T | undefined, next: T) => boolean;
const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);
function usePrevious<T>(
state: T,
shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>();
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current;
}
export default usePrevious;
useSafeState
一个性能优化Hook
,如果setState
更新时机在组件销毁的时候,则不更新,可以避免内存泄漏。
源码如下:
import { useCallback, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import useUnmountedRef from '../useUnmountedRef';
function useSafeState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
function useSafeState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
function useSafeState<S>(initialState?: S | (() => S)) {
const unmountedRef = useUnmountedRef();
const [state, setState] = useState(initialState);
const setCurrentState = useCallback((currentState) => {
/** if component is unmounted, stop update */
if (unmountedRef.current) return;
setState(currentState);
}, []);
return [state, setCurrentState] as const;
}
export default useSafeState;
源码中对于setState
的前置做了一层判断,如果unmountedRef
为true
,则不更新,这也是useSafeState
的核心部分。
我们再看一下useUnmountedRef
的实现:
import { useEffect, useRef } from 'react';
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
export default useUnmountedRef;
useUnmountedRef
记录了一个ref
,在销毁后为变成true
,因此配合useSafeState
实现了这个能力。
useResetState
基于useState
二次封装,暴露出第三个参数,提供重置到最初状态的能力,用法如下:
const [state, setState, resetState] = useResetState<State>({
hello: '',
count: 0,
});
源码如下:
import { useRef, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { isFunction } from '../utils';
import useMemoizedFn from '../useMemoizedFn';
import useCreation from '../useCreation';
type ResetState = () => void;
const useResetState = <S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, ResetState] => {
const initialStateRef = useRef(initialState);
const initialStateMemo = useCreation(
() =>
isFunction(initialStateRef.current) ? initialStateRef.current() : initialStateRef.current,
[],
);
const [state, setState] = useState(initialStateMemo);
const resetState = useMemoizedFn(() => {
setState(initialStateMemo);
});
return [state, setState, resetState];
};
export default useResetState;
从源码上看,核心的部分是initialStateRef
,用于保存最初始的状态值,然后暴露的reset
重置方法就是将当前状态设置为initialStateRef
即可。
结尾
如有理解错误的地方欢迎指出,本专栏会定期更新,最后讲解完所有Hook
,欢迎关注。
如果喜欢我的文章或者想上岸大厂,可以关注公众号「量子前端」[1],将不定期关注推送前端好文、分享就业资料秘籍,也希望有机会一对一帮助你实现梦想。
转载自:https://juejin.cn/post/7402055702770073612