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有什么优势和劣势?
- 优势
- Typescript是js的超集,兼容js,具有一切js的方法
- Ts是静态类型,支持静态类型检查,可以在编译阶段显示出所有语法错误
- 良好的代码编写体验,可以提供方法提示,错误校验,自动联想等
- 由于是静态类型,在编译时省略了判断类型这一环节,编译速度更快
- 类型在一定程度上可以充当文档
- 劣势
- 有一定学习成本
- 老项目重构需要花费一定的精力
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
- 首先按照相对路径或者绝对路径查找模块
- 如果找不到,就去寻找外部模块声明'.d.ts'文件
- 如果再找不到,就会报错提示'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的真正意义:
- 保证性能的下限,就是无论试图内dom元素数量多少,更新所耗费的时间和资源是一定的,而不会渲染忽快忽慢。
- 提供了一种抽象试图的方法和思路
- 为跨平台提供了很好的方案,比如taro3也是采用了虚拟dom的方式,在小程序平台也有了良好的表现
对React的理解
React是一个网页UI框架,通过组件化的方式解决图层开发复用的特点,本质上是一个组件化框架。具有声明式、组件化和通用性的优点。
- 声明式的优点主要是直观和组合
- 组件化的优点主要是将视图拆分和模块复用,做到了高内聚低耦合
- 通用性主要体现在一次学习,随处编译。无论是react native,小程序等,都可以使用react,这主要依靠虚拟dom实现
- 以上特性可以让react在多平台使用,web,native,小程序等
- 但作为一个视图层框架,劣势也很明显,主要是没有提供一揽子的完整解决方案,在碰到一些框架问题时,需要求助于社区,不过也促进了社区的繁荣
- React提供了一系列的优化方法供用户选择,可以提升项目的性能,比如usecallback, usememo, react.memo等
React16架构变化导致的生命周期注意点
- componentWillmount、componentWillupdate,已经被废弃,因为react16的异步更新机制,会导致此函数被重复触发
- shouldcomponentupdate,仍然保留,用于性能优化
- componentwillunmount,很重要,在此函数中对定时器,事件监听的清除,否则会导致内存泄漏等问题
- react的请求一般放在componentdidmount函数中,其实constructor函数和componentwillmount函数内都可以放,但是constructor一般用来初始化变量,和业务逻辑无关,而且因为类静态属性的流行,这个函数使用频率已经变低。componentwillmount已经被废弃
React Fiber
核心思想就是任务拆分,可中断重启
React快速响应制约因素
快速响应是React的宗旨之一,制约快速响应的主要原因有两个
- CPU影响,js是单线程,当组件比较复杂,页面逻辑很复杂的时候,就会出现CPU计算时间长,页面得不到响应,卡顿的情况
- 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。在浏览器的一帧中,各个事件的执行顺序如下:
- 接受输入时间,比如click
- 执行事件回调,比如settimeout
- 开始一帧,比如window.resize, scroll, media query change
- RAF
- 布局和绘制
- RIC RIC最为特殊,是前面任务执行完一帧有空闲时间的时候才会执行,并且部分浏览器没这个方法,所以React进行了pollyfill。同时将高优先级的任务放置于RAF中,将低优先级的放于RIC中执行,这就是Scheduler
React Fiber架构总结
老的React中Reconciler是递归遍历,容易同步阻塞,解决方法就是异步和任务拆分。React fiber就是为了任务拆分诞生的。 节点树遍历变成了具有链表和指针的单链表遍历算法,每个单元都记录上一步和下一步。链表遍历算法(其实是一种DFS算法)如下:
- 首先通过不断遍历子节点,到达树末尾
- 遍历sibling兄弟节点
- 返回父节点,返回第二步
- 直到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:将变化的部分渲染到视图上 具体步骤
- jsx根据babel词法分析,调用React.createElement方法,生成jsx对象,也就是虚拟dom
- 不管是在首次渲染还是更新的过程中,刚开始都会有scheduler这一步,具有协调调度任务的作用。比如当前有一个任务A在执行,突然有一个优先级更高的任务B需要执行,那么会中断A,先执行完B,再执行A。实现这一作用的核心API就是RIC和RAF。同时,在初始化任务时,会给每个任务一个过期时间,优先级越高,过期时间越短。在React 17中,引入了赛道(lane)的概念来表示优先级,lane其实是二进制的形式形象的展现了优先级,有点类似css选择器的权重概念。Scheduler分配时间片给需要渲染的任务,如果一个时间片没完成,那么从当前的fiber节点暂停,执行权交还给浏览器,等浏览器有空闲的时候,再从暂停的fiber节点继续执行渲染任务
- Reconciler阶段。对于首次渲染和更新的处理略有不同,主要是双缓冲树的处理以及fiber节点中更新tag的区别。在首次渲染中,根据jsx对象生成workinprogressfiber,所有的fiber节点中更新tag记录为placement(插入),再讲这些有副作用的fiber节点加入effectlist的副作用链表中。对于更新阶段,react会根据最新状态的jsx对比currentfiber,再生成workinprogressfiber,这个对比就是diff算法。对比的过程中,给fiber节点打上更新标记(placement, update, delete),再讲这些有副作用的fiber加入effectlist链表
- 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个限制,以进行优化:
- 同级节点的diff,如果前后两次跨越了层级,那么react不会再去复用它
- 同级不同类型的节点,会销毁之前的节点及其子节点,再新建新的节点及其子节点
- 通过key可以控制哪些节点不需要改变
diff算法的分治思想
diff算法将节点分为两种,比较方法也略有不同
- 子节点为Object、number、string等这样的单节点
- 子节点为Array这样的多节点
单节点
- 首先判断currentFiber中是否存在对应的节点,如果不存在就直接创建新节点
- 如果存在,首先比较key是否相同,如果不相同,将该节点删除,再创建新节点
- 如果key相同,再比较type,如果不同,就调用deleteRemainingChildren方法,删除节点及子节点,再创建新节点
- 如果key相同,type也相同,复用该节点
多节点
多节点的情况相对复杂,需要使用两轮遍历。
第一轮遍历可以复用的节点,直到最后一个可以复用的位置。
第二轮遍历对不可复用节点再进行处理。
第一轮遍历结束,可能有4种结果(假设旧节点树为old,新节点树为new):
- old和new同时遍历结束,说明一样,整体都能复用
- old遍历完,new还没结束,new剩下节点全部新增插入
- old遍历完,new还没结束,new剩下节点全部删除
- 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
可以看出,应该尽量避免节点往前移动的操作
为什么React元素有一个$$typeof对象
可以防止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上。
优缺点
优点
- 提升代码抽象能力
- 避免直接操作DOM元素
- 避免XSS攻击
- 提供了一个大规模操作DOM的性能下限
- 低成本实现跨平台开发
- 由于服务端没有dom的概念,所以可以利用虚拟DOM完成服务端渲染
缺点
- 内存占用较大,因为实际DOM体积大了
- 对于有高性能要求的应用,比如google earth,虚拟DOM并不适用
其他应用场景
可以记录虚拟DOM的变化,用于埋点和数据记录
- rrweb: 页面录制与回放
React性能优化手段
React状态管理
Redux中connect函数的原理
React Hooks
组件间通信
类组件和函数组件
HOC
React Router
React 17变化
H5
link标签预处理
合理利用Link标签的rel值
JS
画出以下代码中的原型链
class A {}
class B extends A {}
const b = new B();
实现js的抽象方法
匿名函数使用场景
- IIFE立即执行函数
(function () {})()
- 回调函数
setTimeout(function() {}, 1000)
- 对象中的属性
{
a: funciton() {}
}
- 函数表达式
var a = function() {}
- 作为函数返回值
funciton a() { return function() {} }
改变原数组和不改变原数组方法
- 改变 sort, push, pop, shift, unshift, reverse, splice, copyWithin, fill
- 不改变 很多就不列举了
Ajax, Fetch和Axios的优缺点
es6有哪些变化
实现私有变量
- #标志
- 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判断类型
浏览器
浏览器渲染原理
Webpack
网络
安全
冷门知识
htmlcollection和nodelist区别
nodelist是静态的,节点变化不会变化,相当于快照 htmlcollection是动态的,节点变化会变化