Commit ae5bc858 authored by liyan's avatar liyan
parents 8e477e08 71f8eeed
Pipeline #16131 canceled with stage
...@@ -3,8 +3,7 @@ title: Map和Object ...@@ -3,8 +3,7 @@ title: Map和Object
date: 2022/4/26 10:00:00 date: 2022/4/26 10:00:00
tags: tags:
- map - map
- object - object
author: 张芳利 author: 张芳利
--- ---
...@@ -14,6 +13,9 @@ author: 张芳利 ...@@ -14,6 +13,9 @@ author: 张芳利
## 描述 ## 描述
Map 对象存有键值对,其中的键可以是任何数据类型。<br />Map 对象记得键的原始插入顺序。<br />Map 对象具有表示映射大小的属性。 Map 对象存有键值对,其中的键可以是任何数据类型。<br />Map 对象记得键的原始插入顺序。<br />Map 对象具有表示映射大小的属性。
<a name="g2duN"></a> <a name="g2duN"></a>
<!-- more -->
## 创建 ## 创建
构造函数创建 构造函数创建
```javascript ```javascript
......
---
title: React中Hooks源码简单解析
date: 2022/4/30 09:00:00
updated: 2022/5/6 15:42:24
tags:
- 前端
- JavaScript
author: 方玉新
---
## 使用Hooks时的疑惑
Hooks的出现让我们对`Function Component`逐步拥有了对标 `Class Component`的特性,比如私有状态以及生命周期函数等。`useState``useReducer`这两个`Hooks`让我们可以在`Function Component`里使用到私有状态。而`useState`其实相当于简化版的`useReducer`,下面就将这两个放到一起简单介绍下。
#### 1、useState使用示例
```javascript
function PersionInfo ({initialAge,initialName}) {
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
return (
<>
Age: {age}, Name: {name}
<button onClick={() => setAge(age + 1)}>Growing up</button>
</>
);
}
```
> `useState`可以初始化一个私有状态,它会返回这个状态的最新值和一个用来更新状态的方法。而`useReducer`则是针对更复杂的状态管理场景:
#### 2、useReducer使用示例
```javascript
const initialState = {age: 0, name: 'Dan'};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {...state, age: state.age + action.age};
case 'decrement':
return {...state, age: state.age - action.age};
default:
throw new Error();
}
}
function PersionInfo() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Age: {state.age}, Name: {state.name}
<button onClick={() => dispatch({type: 'decrement', age: 1})}>-</button>
<button onClick={() => dispatch({type: 'increment', age: 1})}>+</button>
</>
);
}
```
> `useReducer`同样也是返回当前最新的状态,并返回一个用来更新数据的方法。
在使用这两个方法的时候也许我们会想过这样的问题:
```javascript
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
```
#### 问题1、React内部如何区分这两个状态
`Function Component`不像`Class Component`那样可以将私有状态挂载到类实例中并通过对应的`key`来指向对应的状态,而且每次的页面刷新或者组件的重新渲染都会使得`Function`重新执行一遍。所以在`React`中必定有一种机制来区分这些`Hooks`
```javascript
const [age, setAge] = useState(initialAge);
// 或
const [state, dispatch] = useReducer(reducer, initialState);
```
#### 问题2、React是如何在每次重新渲染之后都能返回最新的状态
`Class Component`因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 `Function Component` 由于本质就是一个函数,并且每次渲染都会重新执行。所以`React`必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
### 源码中找出上面问题的答案
先来了解`useState`以及`useReducer`的源码实现,并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始:
```javascript
import React, { useState } from 'react';
```
> 在项目中我们通常会以这种方式来引入`useState`方法,被我们引入的这个`useState`方法是什么样子的呢?其实这个方法就在源码 `packages/react/src/ReactHook.js` 中。
```javascript
// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// ...
return dispatcher;
}
// 我们代码中引入的useState方法
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
```
从源码中可以看到,我们调用的其实是 `ReactCurrentDispatcher.js` 中的`dispatcher.useState()`,那么我们继续前往`ReactCurrentDispatcher.js`文件:
```javascript
import type { Dispacther } from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
```
好吧,它继续将我们带向 `react-reconciler/src/ReactFiberHooks.js`这个文件。那么我们继续前往这个文件。
```javascript
// react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
// 其他hooks类型定义
}
```
兜兜转转我们终于清楚了`React Hooks` 的源码就放 `react-reconciler/src/ReactFiberHooks.js` 目录下面。在这里如上图所示我们可以看到有每个`Hooks`的类型定义。同时我们也可以看到`Hooks`的具体实现,大家可以多看看这个文件。首先我们注意到,我们大部分的`Hooks`都有两个定义:
```javascript
// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useReducer: mountReducer,
useState: mountState,
// 其他Hooks
};
// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
useEffect: updateEffect,
useReducer: updateReducer,
useState: updateState,
// 其他Hooks
};
```
从这里可以看出,我们的`Hooks``Mount`阶段和`Update`阶段的逻辑是不一样的。在`Mount`阶段和`Update`阶段他们是两个不同的定义。我们先来看`Mount`阶段的逻辑。在看之前我们先思考一些问题。`React Hooks`需要在`Mount`阶段做什么呢?就拿我们的`useState``useReducer`来说:
* 我们需要初始化状态,并返回修改状态的方法,这是最基本的。
* 我们要区分管理每个Hooks。
* 提供一个数据结构去存放更新逻辑,以便后续每次更新可以拿到最新的值。
先看下mountState的实现。
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 声明一个链表来存放更新
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer,
lastRenderedState,
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
```
#### 区分管理Hooks
关于初始化状态并返回状态和更新状态的方法。这个没有问题,源码也很清晰利用`initialState`来初始化状态,并且返回了状态和对应更新方法 `return [hook.memoizedState, dispatch]`。那么我们来看看`React`是如何区分不同的`Hooks`的,这里我们可以从 `mountState` 里的 `mountWorkInProgressHook`方法和`Hook`的类型定义中找到答案。
```javascript
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null, // 指向下一个Hook
};
```
首先从`Hook`的类型定义中就可以看到,`React``Hooks`的定义是链表。也就是说我们组件里使用到的`Hooks`是通过链表来联系的,上一个`Hooks``next`指向下一个`Hooks`。这些`Hooks`节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体`mount` 阶段 `Hooks`函数调用的 `mountWorkInProgressHook`方法里:
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
```
`mount`阶段,每当我们调用`Hooks`方法,比如`useState``mountState`就会调用`mountWorkInProgressHook` 来创建一个`Hook`节点,并把它添加到`Hooks`链表上。比如我们的这个例子:
```javascript
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})
```
那么在mount阶段,就会生产如下图这样的单链表:
![链式图](/yifu-study-front-share/images/hooks1.png)
#### 返回最新的值
`useState``useReducer`都是使用了一个`queue`链表来存放每一次的更新。以便后面的`update`阶段可以返回最新的状态。每次我们调用`dispatchAction`方法的时候,就会形成一个新的`updata`对象,添加到`queue`链表上,而且这个是一个循环链表。可以看一下`dispatchAction`方法的实现:
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action) {
const update = {
action,
next: null,
};
// 将update对象添加到循环链表中
const last = queue.last;
if (last === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// 在最新的update对象后面插入新的update对象
update.next = first;
}
last.next = update;
}
// 将表头保持在最新的update对象上
queue.last = update;
// 进行调度工作
scheduleWork();
}
```
也就是每次执行`dispatchAction`方法,比如`setAge``setName`。就会创建一个保存着此次更新信息的`update`对象,添加到更新链表`queue`上。然后每个`Hooks`节点就会有自己的一个`queque`。比如假设我们执行了下面几个语句:
```javascript
setAge(19);
setAge(20);
setAge(21);
```
那么Hooks链表就会变成这样:
![链式图](/yifu-study-front-share/images/hooks2.png)
`Hooks`节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在`update`阶段可以通过这些更新获取到最新的值返回。这就是在第一次调用`useState``useReducer`之后,每次更新都能返回最新值的原因。再来看看`mountReducer`,你会发现和`mountState`几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟`useState`其实就是阉割版的`useReducer`。这里不详细介绍`mountReducer`了。
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init,) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
let initialState;
// 初始化
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg ;
}
hook.memoizedState = hook.baseState = initialState;
// 存放更新对象的链表
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
```
然后我们来看看`update`阶段,也就是看一下我们的`useState``useReducer`是如何利用现有的信息,去返回最新的最正确的值的。先来看一下`useState``update`阶段的代码也就是`updateState`
```javascript
// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
```
可以看到,`updateState`底层调用的其实就调用`updateReducer`,因为我们调用`useState`的时候,并不会传入`reducer`,所以这里会默认传递一个`basicStateReducer`进去。我们先看看这个`basicStateReducer`
```javascript
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
```
在使用`useState(action)`的时候,`action`通常会是一个值,而不是一个方法。所以b`aseStateReducer`要做的其实就是将这个`action`返回。来继续看一下`updateReducer`的逻辑:
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 拿到更新列表的表头
const last = queue.last;
// 获取最早的那个update对象
first = last !== null ? last.next : null;
if (first !== null) {
let newState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
const dispatch = queue.dispatch;
// 返回最新的状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
```
`update`阶段,也就是我们组件第二次第三次。。执行到`useState``useReducer`的时候,会遍历`update`对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。`useState``reducer``baseStateReducer`,因为传入的`update.action`为值,所以会直接返回`update.action`,而`useReducer``reducer`是用户定义的`reducer`,所以会根据传入的`action`和每次循环得到的`newState`逐步计算出最新的状态。
![链式图](/yifu-study-front-share/images/hooks3.png)
### useState/useReducer 小总结
* React 如何管理区分Hooks?
React通过单链表来管理Hooks
按Hooks的执行顺序依次将Hook节点添加到链表中
* useState和useReducer如何在每次渲染时,返回最新的值?
每个Hook节点通过循环链表记住所有的更新操作
在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
* 为什么不能在条件语句等中使用Hooks?
链表!
![链式图](/yifu-study-front-share/images/hooks4.png)
如图所示,我们在`mount`阶段调用了`useState('A')`, `useState('B')`, `useState('C')`,如果我们将`useState('B')` 放在条件语句内执行,并且在`update`阶段中因为不满足条件而没有执行的话,那么没法正确的重回`Hooks`链表中获取信息。React也会报错。
#### Hooks链表位置
现在已经了解了`React` 通过链表来管理 `Hooks`,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个`Hooks`链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的`FiberNode`
![链式图](/yifu-study-front-share/images/hooks5.png)
如图所示,组件构建的`Hooks`链表会挂载到`FiberNode`节点的`memoizedState`上面去。
![链式图](/yifu-study-front-share/images/hooks6.png)
#### useEffect
先看下`useEffect`是如何工作的
```javascript
function PersionInfo () {
const [age, setAge] = useState(18);
useEffect(() =>{
console.log(age)
}, [age])
const [name, setName] = useState('Dan');
useEffect(() =>{
console.log(name)
}, [name])
return (
<>
...
</>
);
}
```
`PersionInfo`组件第一次渲染的时候会在控制台输出`age``name`,在后面组件的每次`update`中,如果`useEffect`中的`deps`依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在`unmount`的时候就会执行清除函数(如果有)。`React`中是怎么实现的呢?其实很简单,在`FiberNode`中通过一个`updateQueue`来存放所有的`effect`,然后在每次渲染之后依次执行所有需要执行的`effect``useEffect` 也分为`mountEffect``updateEffect`
#### mountEffect
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function mountEffect( create,deps,) {
return mountEffectImpl(
create,
deps,
);
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取当前Hook,并把当前Hook添加到Hook链表
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 将当前effect保存到Hook节点的memoizedState属性上,
// 以及添加到fiberNode的updateQueue上
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// componentUpdateQueue 会被挂载到fiberNode的updateQueue上
if (componentUpdateQueue === null) {
// 如果当前Queue为空,将当前effect作为第一个节点
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 保持循环
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 否则,添加到当前的Queue链表中
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
```
可以看到在`mount`阶段,`useEffect`做的事情就是将自己的`effect`添加到了`componentUpdateQueue`上。这个`componentUpdateQueue`会在`renderWithHooks`方法中赋值到`fiberNode``updateQueue`上。
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
const renderedWork = currentlyRenderingFiber;
renderedWork.updateQueue = componentUpdateQueue;
}
```
也就是在`mount`阶段我们所有的`effect`都以链表的形式被挂载到了`fiberNode`上。然后在组件渲染完毕之后,`React`就会执行`updateQueue`中的所有方法。
![链式图](/yifu-study-front-share/images/hooks7.png)
#### updateEffect
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function updateEffect(create,deps){
return updateEffectImpl(
create,
deps,
);
}
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
// 获取当前Hook节点,并把它添加到Hook链表
const hook = updateWorkInProgressHook();
// 依赖
const nextDeps = deps === undefined ? null : deps;
// 清除函数
let destroy = undefined;
if (currentHook !== null) {
// 拿到前一次渲染该Hook节点的effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对比deps依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖没有变化,就会打上NoHookEffect tag,在commit阶段会跳过此
// effect的执行
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
```
`update`阶段和`mount`阶段类似,只不过这次会考虑`effect` 的依赖`deps`,如果此次更新`effect`的依赖没有变化的话,就会被打上`NoHookEffect`标签,最后会在`commit`阶段跳过改`effect`的执行。
```javascript
function commitHookEffectList(unmountTag,mountTag,finishedWork) {
const updateQueue = finishedWork.updateQueue;
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount 阶段执行所有tag !== NoHookEffect的effect.create,
// 我们的清除函数(如果有)会被返回给destroy属性,一遍unmount执行
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
```
### useEffect 小总结
![链式图](/yifu-study-front-share/images/hooks8.png)
useEffect做了什么?
`FiberNdoe`节点中又一个`updateQueue`链表来存放所有的本次渲染需要执行的`effect`
`mountEffect`阶段和`updateEffect`阶段会把`effect` 挂载到`updateQueue`上。
`updateEffect`阶段,`deps`没有改变的`effect`会被打上`NoHookEffect tag``commit`阶段会跳过该`Effect`
到此为止,`useState/useReducer/useEffect`源码也阅读完毕了,相信有了这些基础,剩下的Hooks的源码阅读不会成问题,最后放上完整图示:
![链式图](/yifu-study-front-share/images/hooks9.png)
---
title: 性能优化---Preload&Prefetch
date: 2022/5/7 14:06:20
tags:
- 前端
- 盒子模型
- CSS
author: 刘磊
---
# 性能优化---Preload&Prefetch
在preload和prefetch之前,我们一般根据编码需求通过link或者script标签引入页面渲染和交互所依赖的js和css等;然后这些资源由浏览器决定优先级进行加载、解析、渲染等。
然而,有些情况我们需要某些依赖在浏览器进入渲染的主进程之前就希望被加载,但是实际资源加载的状况并不受我们控制,而是根据浏览器内部决定资源的优先级等状况进行加载的。即使我们将需要的资源按照希望的那样放在期望的位置,但是由于资源的加载和解析,尤其是js的加载、解析和编译是阻塞式的,因此往往还是达不到预期。或者,有些资源是我们后边需要的,我们希望在需要它的时候在进行解析和执行,并不想让它的加载影响其他资源的加载及首屏渲染时间。
preload和prefetch的出现为我们提供了可以更加细粒度地控制浏览器加载资源的方法。
```
<link ref='preload'>
<link ref='prefetch'>
```
#### preload特点
- preload加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞onload事件;
- preload可以支持加载多种类型的资源,并且可以加载跨域资源;
- preload加载的js脚本其加载和执行的过程是分离的。即preload会预加载相应的脚本代码,待到需要时自行调用;
#### prefetch特点
- prefetch加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少5分钟(无论资源是否可以缓存);并且,当页面跳转时,未完成的prefetch请求不会被中断;
#### preload和prefetch对比
- preload主要用于预加载当前页面需要的资源;而prefetch主要用于加载将来页面可能需要的资源;
- 不论资源是否可以缓存,prefecth会存储在net-stack cache中至少5分钟;
- preload需要使用as属性指定特定的资源类型以便浏览器为其分配一定的优先级,并能够正确加载资源;
## webpack中的perload和perfetch
问题:异步加载模块是Webpack官方所希望开发者实现的,而且异步加载也能提升首页加载速度,但是又会导致异步加载的那部分代码逻辑迟迟不能执行,可能导致用户的交互长时间没有响应。
所谓预取,就是提前获取将来可能会用到的资源。prefetch chunk会在父chunk加载完成之后进行加载。
那么该怎么做呢?我们要通过一种称作Magic Comment(魔法注释)的方式,让Webpack输出"resource hint(资源提示)",来告知浏览器。
```
document.getElementById('btn').onclick = function () {
import( /* webpackChunkName: 'base',webpackPrefetch:true*/ './base').then(({
mul
}) => {
console.log(mul(40, 3));
}).catch((err) => {
console.log(`base.js加载失败:${err}`);
});
}
```
---
title: TypeScript泛型
date: 2022/5/10 16:00:00
tags:
- 前端
- typescript
author: 杨靖
---
## 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
我们可以用一个函数createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
```typescript
function createArray(length: number, value: any): Array<any> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
上例中,我们使用了之前提到过的数组泛型来定义返回值的类型。
这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:
Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。
这时候,泛型就派上用场了:
```typescript
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']
```
上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。
接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来:
```typescript
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
*定义泛型的时候,可以一次定义多个类型参数:*
```typescript
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
```
上例中,我们定义了一个 swap 函数,用来交换输入的元组。
**泛型约束**
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
```typescript
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
```
上例中,因为泛型 T 不一定包含属性 length,所以编译的时候报错了,是一个错误的例子。
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
```typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
```
上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。
此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:
```typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity(7);
// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
```
多个类型参数之间也可以互相约束:
```typescript
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });
```
上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。
**泛型接口**
使用接口的方式来定义一个函数需要符合的形状:
```typescript
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
```
当然也可以使用含有泛型的接口来定义函数的形状:
```typescript
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
```typescript
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
*使用泛型接口的时候,需要定义泛型的类型。*
**泛型类**
与泛型接口类似,泛型也可以用于类的类型定义中:
```typescript
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
```
**泛型参数的默认类型**
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
```typescript
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
```
...@@ -4,7 +4,7 @@ date: 2022/4/22 17:30:00 ...@@ -4,7 +4,7 @@ date: 2022/4/22 17:30:00
tags: tags:
- 前端 - 前端
- typescript - typescript
author: 杨靖 author: 杨靖
--- ---
## typescript的高级类型详解 ## typescript的高级类型详解
......
## 路由懒加载
#### 什么是路由懒加载
路由懒加载也叫延迟加载,即在需要的时候进行加载,随用随载。在单页面应用中浏览器默认会将所有的资源全部下载下来,这可能会导致首屏加载的时间过长。路由懒加载可以做到点击对应的路由再加载对应的文件资源
#### 路由懒加载的三种写法 (推荐用法:es6的import)
1. component: resolve => require(['../pages/home], resolve)
1. component: import(/* webpackChunkName: 'ImportFuncDemo' */ '../pages/home')
1. component: resolve=>{require.ensure(,()=>{resolve(require('../pages/home'))})}
#### 路由懒加载使用中的疑惑
项目中已经使用了路由懒加载为什么首屏渲染仍然请求了所有路由的chunk;
解答:vue-cli webpack中默认配置了prefetch这个功能可以在浏览器闲置时加载网站未来可能会用到的资源;但
经过验证这个prefetch预请求阻塞了首屏的加载
#### 解决prefetch影响首页加载问题
- 在webpack配置中删除prefetch插件
```
configplugins.delete(prefetch)
```
- 在引用时配置
```
import(<!--webpackChunkName:'[...]',webpackPrefetch:false-->)
```
#### 思考:既然可以通过import异步加载路由组件,非路由组件是否可以通过异步加载的方式,从而优化首屏的加载速度?很多情况中复杂的弹窗等组件比页面组件的资源更大。
- 经过查阅文档发现VUE官方也提到了异步组件,并有相关的用法
- 通过异步引入的弹窗组件从主包分离出来,但是仍然在首页加载是就将弹窗组件加载出来。并未实现点击组件时
再去加载文件:
```
const wechatEnter = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import(
/*webpackChunkName:'wechatEnter',webpackPrefetch:true */ '@/views/index/components/wechatEnter.vue'
),
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000,
})
```
#### 利用webpack——SplitChunks优化
SplitChunks 插件来控制 Webpack 打包生成的 js 文件的内容的精髓在于:防止模块被重复打包,拆分过大的js
文件,合并零散的js文件;
##### 每个浏览器并发请求数量不同,分包需要考虑并发
由于并发数量的限制,对于splitchunks的分包并不是越多越好。需要具体情况具体分析,比如并发请求数量设置为4,目前有三个包一个包体积较大,那么这时候对大包拆成两个包加载速度肯定会有提升;如果本来有4个包,并发数量也是4,这时候拆个包出来如果包的体积也不大,第5个包请求的时间加上队列之间多出来的时间,综合下来谁快谁慢还不好说;最好的办法还是对静态资源做多域名请求;
#### 通过CDN引入第三方库,但是这个有风险,就是第三方库挂掉或者什么的,可以选择付费的cdn加速。或者下载到本地上传到自己的服务器,总共三套方案,可以同时使用,保证安全性。
#### 静态资源压缩,代码压缩,图片压缩;gzip压缩一定要开,并且服务端配合开一下,真的很重要。开启gzip后你会发现加载速度有质的提升,尤其是你的vendor.js和app.js体积过大的时候。
#### 代码层面的优化
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment