高频考点

209 阅读19分钟

Typescript

TypeScript 中 const 和 readonly 的区别?枚举和常量枚举的区别?接口和类型别名的区别?

const只能限制变量不可改变值,readonly可以限制对象中的属性也不可改变值;
常量枚举是只能用常量枚举表达式,编译阶段会被删除,只有在被使用时才会被内联进来,原因是常量枚举不允许包含计算成员;
接口和类型别名相同点都可以定义对象、函数的类型,都支持扩展(extends),不同点类型别名不仅有接口的功能,还可以定义基本类型、联合类型、元组,type可以使用typeof获取实例的类型进行赋值,相同的interface可以自动合并;

enum str { A, B, C }
type strType = keyof typeof str; // A | B | C

TypeScript 中 any、never、unknown、null & undefined 和 void 有什么区别?

any: 任何类型,意味不会进行类型检查
never: 永不存在的值,例如总是抛出异常或者根本不会有返回值的函数的返回值类型
unknown: 任何值类型都可以赋值给unknown,但是unknown只能赋值给any和unknown

let a: any
console.log(a.b) // undefined
let b: unknown
console.log(b.a) // error
let c = 'hh'
a = c // 没问题
b = c // type error

null & undefined: 他们只能赋值给void和他们自身
void: 没有任何类型,例如函数没有返回值可以是void

Typescript相较于JavaScript有什么优势和劣势?

  • 优势
  1. Typescript是js的超集,兼容js,具有一切js的方法
  2. Ts是静态类型,支持静态类型检查,可以在编译阶段显示出所有语法错误
  3. 良好的代码编写体验,可以提供方法提示,错误校验,自动联想等
  4. 由于是静态类型,在编译时省略了判断类型这一环节,编译速度更快
  5. 类型在一定程度上可以充当文档
  • 劣势
  1. 有一定学习成本
  2. 老项目重构需要花费一定的精力

const func = (a, b) => a + b; 要求编写Typescript,要求a,b参数类型一致,都为number或者都为string

type numString = number | string
const func = (a: numString, b: numString) => {
   if ((typeof a === 'string' && typeof b === 'string') || (typeof a === 'number' && typeof b === 'number')) {
      return a + b
   }
   throw new Error('参数类型必须同为number或者同为string')   
}
复制代码

TS内置工具类型

  • exclude<T,U>从T可分配给的类型中排除U
exclude<T,U> = T extends U ? never : T
type E = exclude<string|number, string>
let e: E = 10
复制代码
  • extruct<T,U>从T可分配的类型中提取出U
extruct<T,U> = T extends U ? T : never
type E = exclude<string|number, string>
let e: E = 10
复制代码
  • NonNullable从T中排除null和undefined
type NonNullable<T> = T extends null|undefined ? never : 
复制代码
  • ReturnType infer最早出现在此pr中,表示extends中待推断的类型
ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]) => infer R ? R : never

function getUserInfo() {
    return { name: 'hh', age: 29 }
}
type userInfo = ReturnType<type of getUserInfo>
const info: userInfo = { name: '2r', age: 23 }
复制代码

该工具类型主要用来获取函数的返回类型

  • Parameters该工具类型主要用来获取函数的参数类型
Parameters<T> = T extends (...args: infer R) => any ? R : any
type T0 = Parameters<() => string> // []
type T1 = Parameters<(s: string) => void> // [string]
复制代码
  • Partial可以让传入的属性由必选变为可选
type Partial<T> = { [P in keyof T]?: T[P]}
interface A {
    a1: string
    a2: number
}
type partial = Partial<A>
const a3: partial = {} // 不会报错
复制代码
  • Required可以让传入的属性由可选变为必选
type Required<T> = { [P in keyof T]-?: T[P] }
复制代码
  • Readonly传入的每个属性都修改为只读属性
type Readonly<T> = { readonly [P in keyof T]: T[P] }
interface Person {
  name: string
  age: number
}
const a: Readonly<Person> = { name: 'sfsf', age: 23 }
a.name = '234' // Error
复制代码
  • Pick<T,K> 从传入的属性中摘取部分返回
type Pick<T,K> = { [P in keyof K]: T[P] }
interface Todo {
  title: string;
  description: string;
  done: boolean;
}
type TodoBase = Pick<Todo, "title" | "done">;
type TodoBase = { title: string; done: boolean; };
复制代码
  • Record<T,K> 对象类型,T是key的类型,K是value的类型
type Record<T,K> = { [P in T]: K}
type keyType = 'x' | 'y'
type objType = Record<keyType, number>
const a: objType = { x: 1, y: 2 }
复制代码
  • Omit<T,K>从传入的属性中剔除部分返回
type Omit<T,K> = Pick<T, exclude<key of T, K>>
type User = { id: string; name: string; email: string; };
type UserWithoutEmail = Omit<User, "email">; // UserWithoutEmail ={id: string;name: string;} };

简述Typescript的模块加载机制

假如引入模块import { a } from A

  1. 首先按照相对路径或者绝对路径查找模块
  2. 如果找不到,就去寻找外部模块声明'.d.ts'文件
  3. 如果再找不到,就会报错提示'can not find module A'

Typescript中的配置文件tsconfig.json主要配置项有哪些

files: 是个数组,指定要编译的文件列表
include: 需要编译的文件
exclude: 不需要编译的文件
compileOnsave: 让IDE在编译的时候根据tsconfig.json重新生成文件
extends: 是一个路径,可以继承路径文件里的配置
compilerOptions: 编译配置项

compilerOptions主要配置举例

target: 要编译成的js语言版本,例如'ES5'
module: 编译使用的模块,默认target === 'es5' || target === 'es3' ? 'commonjs' : 'es6'
outdir: 编译后的指定输出目录
allowjs: 是否支持js,jsx
sourcemap: 是否生产sourcemap文件
removecomments: 移除注释
strict: 是否开启所有类型的严格模式

React

虚拟dom的理解

虚拟dom真正的意义并不是性能提升,事实上,当页面dom元素非常少的时候,比如页面只有一个div,改变颜色,那很明显是直接操作dom更快,虚拟dom还要从真实dom生成虚拟dom,以及做diff,这个过程是消耗性能和时间的。
虚拟dom的真正意义:

  1. 保证性能的下限,就是无论试图内dom元素数量多少,更新所耗费的时间和资源是一定的,而不会渲染忽快忽慢。
  2. 提供了一种抽象试图的方法和思路
  3. 为跨平台提供了很好的方案,比如taro3也是采用了虚拟dom的方式,在小程序平台也有了良好的表现

对React的理解

React是一个网页UI框架,通过组件化的方式解决图层开发复用的特点,本质上是一个组件化框架。具有声明式、组件化和通用性的优点。

  1. 声明式的优点主要是直观和组合
  2. 组件化的优点主要是将视图拆分和模块复用,做到了高内聚低耦合
  3. 通用性主要体现在一次学习,随处编译。无论是react native,小程序等,都可以使用react,这主要依靠虚拟dom实现
  4. 以上特性可以让react在多平台使用,web,native,小程序等
  5. 但作为一个视图层框架,劣势也很明显,主要是没有提供一揽子的完整解决方案,在碰到一些框架问题时,需要求助于社区,不过也促进了社区的繁荣
  6. React提供了一系列的优化方法供用户选择,可以提升项目的性能,比如usecallback, usememo, react.memo等

React16架构变化导致的生命周期注意点

  1. componentWillmount、componentWillupdate,已经被废弃,因为react16的异步更新机制,会导致此函数被重复触发
  2. shouldcomponentupdate,仍然保留,用于性能优化
  3. componentwillunmount,很重要,在此函数中对定时器,事件监听的清除,否则会导致内存泄漏等问题
  4. react的请求一般放在componentdidmount函数中,其实constructor函数和componentwillmount函数内都可以放,但是constructor一般用来初始化变量,和业务逻辑无关,而且因为类静态属性的流行,这个函数使用频率已经变低。componentwillmount已经被废弃

React Fiber

核心思想就是任务拆分,可中断重启

React快速响应制约因素

快速响应是React的宗旨之一,制约快速响应的主要原因有两个

  1. CPU影响,js是单线程,当组件比较复杂,页面逻辑很复杂的时候,就会出现CPU计算时间长,页面得不到响应,卡顿的情况
  2. IO等待,当网络延迟比较严重,http请求等待时间长,也会出现响应延迟,甚至白屏的情况

针对这两个情况,fiber提出了timing slice(时间切片) 和 suspense的方案。但fiber出现的原因不止于此。

React版本优化历史

React 15
分为两个主要阶段,虚拟dom和diff算法的雏形

  • Reconciler(协调器):负责生成虚拟dom,并使用diff算法找到需要修改的部分
  • Renderer(渲染器):负责将变化的部分渲染到页面上 版本问题:Reconciler是Stack Reconciler,也就是栈,是以递归的方式对节点进行遍历,递归过程一气呵成,无法中断,如果递归过程超过16ms,就会造成页面卡顿

React 16
基于15的问题,16提出了并发模式(Concurrent Mode),分为三个阶段

  • Scheduler(调度器):调度任务优先级,优先级高的优先进入Reconciler
  • Reconciler(协调器):找出变化的部分,但是可中断,采用了fiber架构,也就是fiber reconciler
  • Renderer(渲染器):变化的部分渲染到页面上

React 17
仍然为3个阶段,但是schduler阶段的优先级算法调整了,16是根据任务的expires time来决定,17新增了lane的概念,是二进制的形式对优先级进行区分。至此concurrent mode已经比较成熟,由两部分组成

  • fiber reconciler一套协程架构
  • 控制协程工作方式的算法

Scheduler实现原理

Scheduler是控制任务调度的步骤,实现原理主要是基于RequestAnimationFrame和RequestIdleCallback。在浏览器的一帧中,各个事件的执行顺序如下:

  1. 接受输入时间,比如click
  2. 执行事件回调,比如settimeout
  3. 开始一帧,比如window.resize, scroll, media query change
  4. RAF
  5. 布局和绘制
  6. RIC RIC最为特殊,是前面任务执行完一帧有空闲时间的时候才会执行,并且部分浏览器没这个方法,所以React进行了pollyfill。同时将高优先级的任务放置于RAF中,将低优先级的放于RIC中执行,这就是Scheduler

React Fiber架构总结

老的React中Reconciler是递归遍历,容易同步阻塞,解决方法就是异步和任务拆分。React fiber就是为了任务拆分诞生的。 节点树遍历变成了具有链表和指针的单链表遍历算法,每个单元都记录上一步和下一步。链表遍历算法(其实是一种DFS算法)如下:

  1. 首先通过不断遍历子节点,到达树末尾
  2. 遍历sibling兄弟节点
  3. 返回父节点,返回第二步
  4. 直到root节点,跳出遍历 这样就可以做到暂停和重启。在reconciler阶段,react将任务拆分成一个个小的fiber单元,这个过程中就可以进行中断。任务可以根据优先级策略执行,高优先级在RAF中执行,低优先级在RIC中执行。reconciler完成后,在renderer阶段,就是一次性更新所有变化,这时候就不能中断了。

setState是同步还是异步

setState可同步可异步。而且这个异步也不是真正的异步,只是把setState暂存在队列中,批量处理,并且后面的setState结果会覆盖之前的,这样做可以减少渲染次数,优化性能,还有一点原因是和props保持一致的逻辑,都是批量处理。
异步还是同步判断的逻辑就是isBatchingUpdates的结果是true还是false,对于React的合成事件以及钩子函数,结果为true,就是异步。对于js原生事件以及settimeout等函数,结果为false,就是同步

class Test extends React.Component {
  state  = {
      count: 0
  };

    componentDidMount() {
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);

    this.setState({count: this.state.count + 1});
    console.log(this.state.count);

    setTimeout(() => {
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);

      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
    }, 0);
  }
 
  render() {
    return null;
  }
};

// 结果输出为 0,0,2,3

为什么直接修改this.state不会引起重新渲染

因为this.setState修改状态的动作,在异步中是会放在队列中,在批量执行。直接操作this.state赋值,不会放在队列里,在批量更新时不会执行赋值操作

setState之后的事情

setState之后,会对状态进行diff,如果不是放在队列里,而是批量执行,那意味着每次setState之后都会进行diff,这对性能无疑是灾难

如何知道state已经被更新

在this.setState的第二个参数,回调函数中可以查看

setState循环调用

setState在批量更新之后,会调用receiveComponent和updateComponent方法进行组件更新。如果在shouldComponentUpdate或者componentWillUpdate方法中调用setState,会造成批量更新队列一直不为空,造成循环调用,内存泄露

React渲染流程

React以16版本为界线,渲染流程是有一定变化的

React 16之前

只有两个阶段reconciler和renderer。reconciler是stack reconciler,核心调度方式是递归。调度的基本处理单位是事务transaction。挂载是调用reactmounted模块,更新是调用reactupdate模块,模块之间相互独立,落脚执行点也是tranction。

React 16之后

分为三个阶段Scheduler, renconciler和renderer

  • Scheduler:调度任务优先级,优先级高的先进入renconciler
  • Renconciler:使用fiber reconciler,根据diff算法找出变化的部分,这个过程是可以中断的
  • Renderer:将变化的部分渲染到视图上 具体步骤
  1. jsx根据babel词法分析,调用React.createElement方法,生成jsx对象,也就是虚拟dom
  2. 不管是在首次渲染还是更新的过程中,刚开始都会有scheduler这一步,具有协调调度任务的作用。比如当前有一个任务A在执行,突然有一个优先级更高的任务B需要执行,那么会中断A,先执行完B,再执行A。实现这一作用的核心API就是RIC和RAF。同时,在初始化任务时,会给每个任务一个过期时间,优先级越高,过期时间越短。在React 17中,引入了赛道(lane)的概念来表示优先级,lane其实是二进制的形式形象的展现了优先级,有点类似css选择器的权重概念。Scheduler分配时间片给需要渲染的任务,如果一个时间片没完成,那么从当前的fiber节点暂停,执行权交还给浏览器,等浏览器有空闲的时候,再从暂停的fiber节点继续执行渲染任务
  3. Reconciler阶段。对于首次渲染和更新的处理略有不同,主要是双缓冲树的处理以及fiber节点中更新tag的区别。在首次渲染中,根据jsx对象生成workinprogressfiber,所有的fiber节点中更新tag记录为placement(插入),再讲这些有副作用的fiber节点加入effectlist的副作用链表中。对于更新阶段,react会根据最新状态的jsx对比currentfiber,再生成workinprogressfiber,这个对比就是diff算法。对比的过程中,给fiber节点打上更新标记(placement, update, delete),再讲这些有副作用的fiber加入effectlist链表
  4. commit:遍历effectlist,处理相应的生命周期,再将这些副作用应用到真实节点,这个过程会调用不同的渲染器,在浏览器环境是react-dom,在svg或者canvas中是react-art

diff算法

首先明确几个基本概念

  • currentFiber:当前视图中真实dom节点对应的fiber节点
  • workinprogressfiber:即将要显示的视图中真实dom节点对应的fiber节点
  • 真实dom节点
  • jsx对象:class中render方法或者function中return返回的对象,包含真实dom的节点信息 diff算法的作用就是根据对比currentfiber和jsx对象,生成workinprogressfiber

传统diff算法瓶颈

即时是最先进的算法,将前后两个树对比的时间复杂度也为O(n^3)。节点越多,耗时越多。

diff算法预设3个限制

对于这样的性能损耗,diff算法预设了3个限制,以进行优化:

  1. 同级节点的diff,如果前后两次跨越了层级,那么react不会再去复用它
  2. 同级不同类型的节点,会销毁之前的节点及其子节点,再新建新的节点及其子节点
  3. 通过key可以控制哪些节点不需要改变

diff算法的分治思想

diff算法将节点分为两种,比较方法也略有不同

  1. 子节点为Object、number、string等这样的单节点
  2. 子节点为Array这样的多节点

单节点

  1. 首先判断currentFiber中是否存在对应的节点,如果不存在就直接创建新节点
  2. 如果存在,首先比较key是否相同,如果不相同,将该节点删除,再创建新节点
  3. 如果key相同,再比较type,如果不同,就调用deleteRemainingChildren方法,删除节点及子节点,再创建新节点
  4. 如果key相同,type也相同,复用该节点

多节点

多节点的情况相对复杂,需要使用两轮遍历。
第一轮遍历可以复用的节点,直到最后一个可以复用的位置。
第二轮遍历对不可复用节点再进行处理。

第一轮遍历结束,可能有4种结果(假设旧节点树为old,新节点树为new):

  1. old和new同时遍历结束,说明一样,整体都能复用
  2. old遍历完,new还没结束,new剩下节点全部新增插入
  3. old遍历完,new还没结束,new剩下节点全部删除
  4. old、new都没有遍历完,这种情况一定是节点位置发生了移动,比如
/* old */
<ul>
    <li key='1'>1</li> <li key='2'>2</li> <li key='3'>3</li>
</ul>
/* new */ 
<ul> 
    <li key='1'>1</li> <li key='3'>2</li> <li key='2'>3</li> 
</ul>

针对这种情况,diff算法首先会记录old中最后一个可复用节点的index,记录为lastPlacedIndex,然后将old中未遍历的节点的key和index做成map,map的key就是key,value是index。遍历new,根据new节点的key,找到map中的index,如果index > lastPlacedIndex,节点不做任何处理,index赋值给lastplacedindex,如果index < lastPlacedindex,节点移动到最后。举两个栗子,key值abcd->acdb和abcd->dabc

image.png

可以看出,应该尽量避免节点往前移动的操作

为什么React元素有一个$$typeof对象

image.png

可以防止xss攻击,因为$$typeof是symbol对象,无法被序列化。这样react就能区分这个react元素是自己生成的,还是从数据库来的。
在React的老版本中,dangeroushtml写法会出现xss攻击。

// 服务端允许用户存储 JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* 把你想的搁着 */'
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// React 0.13 中有风险
<p>
  {message.text}
</p> 

虚拟DOM的工作原理是什么

本质

通过JS模拟的DOM对象

作用

提高代码抽象能力,避免直接操作DOM元素,避免xss攻击

制作原理

虚拟DOM在实现上通常是plain object。以React为例,jsx文件通过babel转义,调用React.createElement方法,通过React.createElement方法,处理jsx返回的参数,包括tag type, props, children,最后生成plain object。虚拟DOM组合在一起称为虚拟DOM树,虚拟DOM树比较的过程被称为diff,比较的结果被称为patch。最后将patch作用于真实dom上。

优缺点

优点

  1. 提升代码抽象能力
  2. 避免直接操作DOM元素
  3. 避免XSS攻击
  4. 提供了一个大规模操作DOM的性能下限
  5. 低成本实现跨平台开发
  6. 由于服务端没有dom的概念,所以可以利用虚拟DOM完成服务端渲染

缺点

  1. 内存占用较大,因为实际DOM体积大了
  2. 对于有高性能要求的应用,比如google earth,虚拟DOM并不适用

其他应用场景

可以记录虚拟DOM的变化,用于埋点和数据记录

  • rrweb: 页面录制与回放

React性能优化手段

image.png

React状态管理

React状态管理 (1).jpg

Redux中connect函数的原理

image.png

React Hooks

React Hooks.jpg

组件间通信

image.png

类组件和函数组件

image.png

HOC

image.png

React Router

image.png

image.png

React 17变化

image.png

H5

link标签预处理

合理利用Link标签的rel值

image.png

JS

画出以下代码中的原型链

class A {}
class B extends A {}

const b = new B();

image.png

实现js的抽象方法

匿名函数使用场景

  1. IIFE立即执行函数
(function () {})()
  1. 回调函数
setTimeout(function() {}, 1000)
  1. 对象中的属性
{
    a: funciton() {}
}
  1. 函数表达式
var a = function() {}
  1. 作为函数返回值
funciton a() { return function() {} }

改变原数组和不改变原数组方法

  • 改变 sort, push, pop, shift, unshift, reverse, splice, copyWithin, fill
  • 不改变 很多就不列举了

Ajax, Fetch和Axios的优缺点

Ajax、Fetch和Axios.png

es6有哪些变化

gitmind.cn/app/doc/f2c…

实现私有变量

  • #标志
  • Weakmap存储私有变量
const map = new WeakMap();

// 创建一个在每个实例中存储私有变量的对象
const internal = obj => {
  if (!map.has(obj)) {
    map.set(obj, {});
  }
  return map.get(obj);
}

class Shape {
  constructor(width, height) {
    internal(this).width = width;
    internal(this).height = height;
  }
  get area() {
    return internal(this).width * internal(this).height;
  }
}

const square = new Shape(10, 10);
console.log(square.area);      // 100
console.log(map.get(square));  // { height: 100, width: 100 }
  • Symbol表示私有变量变量名
const widthSymbol = Symbol('width');
const heightSymbol = Symbol('height');

class Shape {
  constructor(width, height) {
    this[widthSymbol] = width;
    this[heightSymbol] = height;
  }
  get area() {
    return this[widthSymbol] * this[heightSymbol];
  }
}

const square = new Shape(10, 10);
console.log(square.area);         // 100
console.log(square.widthSymbol);  // undefined
console.log(square[widthSymbol]); // 10
  • 闭包实现
function Shape() {
  // 私有变量集
  const this$ = {};

  class Shape {
    constructor(width, height) {
      this$.width = width;
      this$.height = height;
    }

    get area() {
      return this$.width * this$.height;
    }
  }

  const instance = new Shape(...arguments);
  Object.setPrototypeOf(Object.getPrototypeOf(instance), this);
  return instance;
}

const square = new Shape(10, 10);
console.log(square.area);             // 100
console.log(square.width);            // undefined
console.log(square instanceof Shape); // true
  • Proxy实现
class Shape {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  get area() {
    return this._width * this._height;
  }
}

const handler = {
  get: function(target, key) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    } else if (key === 'toJSON') {
      const obj = {};
      for (const key in target) {
        if (key[0] !== '_') {
          obj[key] = target[key];
        }
      }
      return () => obj;
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    target[key] = value;
  },
  getOwnPropertyDescriptor(target, key) {
    const desc = Object.getOwnPropertyDescriptor(target, key);
    if (key[0] === '_') {
      desc.enumerable = false;
    }
    return desc;
  }
}

const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area);             // 100
console.log(square instanceof Shape); // true
console.log(JSON.stringify(square));  // "{}"
for (const key in square) {           // No output
  console.log(key);
}
square._width = 200;                  // 错误:试图访问私有属性

js判断类型

js判断类型.png

浏览器

浏览器渲染原理

浏览器渲染原理 (1).png

Webpack

Webpack (1).png

网络

安全

冷门知识

htmlcollection和nodelist区别

nodelist是静态的,节点变化不会变化,相当于快照 htmlcollection是动态的,节点变化会变化