减少HTTP请求:
优化图像:
延迟加载(懒加载):
浏览器缓存:
服务端优化:
使用字体图标:
异步加载JavaScript:
代码优化:
React性能优化
小程序优化
围绕 加载性能 跟 渲染性能
cookie和session
cookie 是网站用于标记用户的身份的一段数据(加密的字符串),session是另一种记录客户状态的机制
cookie和session的区别
cookie 被禁用了怎么办
保持登录的关键不是cookie,而是cookie保存的session id,所以还常用 HTTP 请求头来传输,但需要手动添加
session 弊端
token
token 的认证流程与 session 很相似,无本质区别,使用token的目的是为了减轻服务器的压力
token与cookie的区别
token 比 cookie 更安全,浏览器不会自动添加到headers里,需要开发者手动添加
token 支持跨域访问,cookie 不支持
token 在服务器不需要存储 session 信息,本身就包含用户信息,只需要在客户端存储
不依赖cookie,不需要防范CSRF
含义:
作用:
GC回收机制
基本思路:确定确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。
主要使用两种方式标记未使用的变量:标记清理和引用计数。
标记清理
当变量进入上下文(比如在函数内部声明一个变量),这个变量会被加上存在于上下文的标记。在上下文的变量,逻辑上讲永远不应该释放它的内存,因为只要代码在运行,就有可能用到。当变量离开上下文时,也会被加上离开标记。
GC回收运行的时候,会标记内存中存储的所有变量(标记方式有很多种,标记过程并不重要,关键是策略)。然后,它会将所有在上下文中的变量的标记去掉。在此之后再被标记的就是待删除的,原因是任何在上下文中的变量都访问不到它们了。随后GC做一次内存清理,销毁带标记的所有值并收回它们的内存。
引用计数
思路是对每个值都记录它的引用次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。如果对该值得引用得变量被其他值给覆盖了,那么引用数减1。当一个值得引用数为0时,就说明没办法再访问到这个值了,就可以安全的收回其内存了。
GET和POST是HTTP协议中的两种发送请求的方法, HTTP的底层是TCP/IP( 数据如何在万维网中通信的协议),所以GET和POST的底层也是TCP/IP 。 GET和POST能做的事情是一样的。给GET加上request body,给POST带上url参数,技术上是完全行的通的
总结: GET 与 POST 都有自己的语义,不能随便混用 。POST 请求比 GET 慢,因为 POST 要发两次包,GET 在 request body 中带数据有些服务器可能会忽略
答: 多个元素嵌套,有层次关系,这些元素都注册了相同的事件,如果里边的元素的事件触发了,外面元素的改事件也自动触发;
如果事件涉及到更新HTML节点或者添加HTML节点的时候,就会出现这样的一种情况,新更新的或者新添加的节点无法绑定事件,表现的行为是无法触发事件
比如:有一个需求,需要点击 ul
列表下的 li
标签触发事件,如果给每个 li
都绑定事件,会产生下面 2 个问题
li
数量非常大的话就会产生性能问题,甚至造成页面卡顿崩溃li
不能绑定事件如果用事件委托,则会很好的解决这两个问题, 用注册一个事件则能监听子节点的所有事件,所应用的就是事件的冒泡
一、相同点
两者都是外部引用CSS的方式
二、区别
BFC规定了内部的Block Box如何布局
定位方案:
满足下列条件之一就可触发BFC
Cookie一共十个属性
function newFn (Fn, params) {
// 创建一个新的空对象 instance
// const instance = {}
// 将 instance 的 __proto__ 属性指向构造函数的原型(Fn.prototype)
// instance.__proto__ = Fn.prototype
const instance = Object.create(Fn.prototype)
// 以 instance 来调用执行构造函数(借助 call/apply)
const result = Fn.apply(instance, params)
// 判断构造函数的返回值,返回 instance 或函数返回值(当构造函数返回值为 object 时)
return (result && (typeof result === 'object' || typeof result === 'function')) ? result : instance
}
优缺点
面向过程:
优点是性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。而Linux\Unix等一般采用面向过程开发,性能是最重要的因素。缺点是没有面向对象易维护,易复用,易扩展。可维护性差,不易修改。
面向对象:
优点是易维护,易复用,易扩展。由于面向对象由封装,继承,多态性的特性,可以设计出耦合度低的系统,使系统更加灵活,更加易于维护。 缺点是性能比面向过程低
组合继承
通过原型链和盗用构造函数实现
原型式继承
寄生式继承
寄生式组合继承
类继承
渲染层和逻辑层
一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发
为什么小程序不采用浏览器的设计模式?
知道了原理我们能干什么?
原型定义:每个对象都有一个名为__proto__的属性,该属性指向另一个对象(构造函数的prototype属性),这个另一个对象就是该对象的原型。
属性查找:当访问一个对象的属性时,首先会在该对象上查找这个属性。如果没有找到,它会沿着原型向上查找,直到找到该属性或者到达顶端,这种呈链式查找称为原型链。
浏览器缓存机制有两种:
强缓存:
通过响应头中的Cache-Control
属性判断 (优先级最高)
协商缓存:
两种缓存标识
协商缓存标识不生效时,状态码200,服务端返回body和header
在对比缓存标识生效时,状态码为304,并且报文大小和请求时间大大减少。原因是缓存标识生效只返回header部分,通过状态码通知客户端使用缓存,不再需要将报文主体部分返回给客户端
总结
所有任务可以分成两种,一种是同步任务, 另一种是异步任务
宏观任务和微观任务
先执行微观任务,再执行宏观任务
宏观任务主要包含:setTimeout、setInterval、script(整体代码)
微观任务主要包括:Promise、MutaionObserver、process.nextTick(Node.js 环境)
Node 事件循环
当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下六个循环阶段,nodejs事件循环和浏览器的事件循环完全不一样
阶段概览
如果event loop进入了 poll 阶段,且代码未设定timer,将会发生下面情况:
- 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
- 如果poll queue为空,将会发生下面情况:
- 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
- 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue,一旦到达就立即执行
如果event loop进入了 poll阶段,且代码设定了timer:
- event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue
http1.0:每次请求都需要重新建立tcp连接,请求完后立即断开与服务器连接,这很大程度造成了性能上的缺陷,http1.0被抱怨最多的就是连接无法复用。
http1.1 对比 1.0:
http2 对比 1.1
以 vue 项目 npm run serve 为例
通过Object.defineProperty
劫持所有data属性,一个属性创建一个 Dep
对象
解析器(Compile)解析模板中的 Directive(指令),获取到哪里用到了属性(订阅者),比如{{name}} {{message}},创建一个观察者watcher添加到对应的Dep 对象中,同时初始化view,在界面上显示
Watcher属于Observer和Compile桥梁,将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化
import Toast from './Toast'
export default {
install(Vue) {
// 1. 创建组件构造器
const toastConstructor = Vue.extend(Toast)
// 2. new 一个组件对象
const toast = new toastConstructor()
// 3. 手动挂载某一盒子上
toast.$mount(document.createElement('div'))
// 4. toast.$el 对应的就是 div
document.body.appendChild(toast.$el)
// 5. 原型上添加属性
Vue.prototype.$toast = toast
}
}
缺点:pinia 不支持调试
一、MVVM
在MVVM下视图和模型是不能直接通信的,只能通过ViewModel进行交互,它能够监听到数据的变化,然后通知视图进行自动更新,而当用户操作视图时,VM也能监听到视图的变化,然后通知数据做相应改动,这实际上就实现了数据的双向绑定。
优点:
二、MVC
分为:Model(模型)、View(视图)、Controller(控制器)。View和Model不直接联系,必须通过Controller来承上启下。
优点:
区别:
响应式原理不同
vue:会遍历data数据对象,使用Object.definedProperty()监听每个属性
react:通过setState()方法来更新状态,状态更新之后,组件也会重新渲染
监听数据变化的实现原理不同
vue:使用的是可变数据,通过 getter/setter以及一些函数的劫持,能精确知道数据变化
react:强调数据的不可变,通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染
Diff算法不同
vue对比节点,如果节点元素类型相同,但是className不同,认为是不同类型的元素,会进行删除重建,但是react则会认为是同类型的节点,只会修改节点属性。
vue的列表比对采用的是首尾指针法,而react采用的是从左到右依次比对的方式,当一个集合只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到最后一个,从这点上来说vue的对比方式更加高效。
数据流不同
vue:组件与DOM之间可以通过v-model双向绑定
react:一直不支持双向绑定,提倡的是单向数据流
组合不同功能的方式不同
vue:通过mixin(侵入太强)
react:通过HoC(高阶组件)
模板渲染方式不同
vue:在和组件JS代码分离的单独的模板中,通过指令来实现的
react:在组件JS代码中,通过原生JS实现模板中的常见语法,比如插值,条件,循环等,都是通过JS语法实现的
渲染过程不同
vue:可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树
react:应用的状态被改变时,全部子组件都会重新渲染
响应式
读取 computed 时会触发 get, 设置时会触发 set
如何控制
某个计算属性C依赖 data 中的 A,每次读取C,C就会去读取A,从而触发A的 get,如果没有缓存多次触发是很消耗性能的;
脏数据标记:dirty,是 watcher 的属性
依赖的data发生改变,computed 如何更新
<p>标签内容p>
<p>{{ msg }}p>
setState 在组件生命周期或React合成事件中更新数据是异步的,在setTimeout或者原生dom事件中更新数据是同步的。原因是返回了不同的值做更新判断,同步返回 Sync,批量处理返回 Batch
跟 shouldComponentUpdate 更新界面有关,内部进行的是浅层比较,如果直接在原数据上修改引用型数据类型,比较的时候内存地址是一样的导致不会更新视图,但实际上数据又发生了变化
情况一:对比不同类型的元素
当一个元素从
时,
树
会调用
树的 componentWillUnmount(),树的 componentDidMount() 方法
情况二:对比同一类型的元素
会保留 DOM 节点,仅比对及更新有改变的属性
- 会更新该组件的props
- 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归
情况三:对子节点进行递归
当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation
4.4 受控组件与非受控组件
在 HTML 中,表单元素(如、
受控组件
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”
非受控组件
不通过 React 控制,当提交表单时通过 ref 来从DOM节点中获取表单数据,这种表单元素叫做“非受控组件”
4.5 高阶组件(HOC)的意义
可以针对某些React代码进行更加优雅的处理。
- 早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
- Mixin 可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
- HOC也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
- HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;
4.6 SPA应用中的hash路由与history路由
优点:
- 用户体验好,用户操作更方便
- 完全的前端组件化
缺点:
- 首次加载大量资源 -->解决:按需加载
- 对SEO不友好 -->解决:服务端渲染 nuxt(next)
特点:当有不同的请求时,在同一个页面渲染不同的组件
原理:前后端分离(后端专注数据,前端专注交互与可视化)+ 前端路由4.6.1 hsah 路由
Hash 路由(也就是锚点#),本质是是改变location的hash属性
利用 URL 上的 hash,当 hash 改变不会引起页面刷新,可以触发相应的 hashchange 回调函数配置路由
window.onhashchange = function() { // 更新页面内容 switch (location.hash) { case '#/home': app.innerHTML = '首页' break case '#/about': app.innerHTML = '关于' break default: app.innerHTML = '' } }
优点:
- 兼容性好,老旧浏览器支持,不会发起新的HTTP请求,可以避免跨域问题
- 简单易用,无需服务器配置
4.6.2 history 路由
本质是通过h5新增的history.pushState()或history.replaceState()改变路径
pushState()、replaceState() 方法接收三个参数:state、title、url
history.pushState({color: 'red'}, '', '/red') // 设置状态,生成 /red window.onpopstate = function(event) { // 监听状态 if (event.state && event.state.color) { document.body.style.color = event.state.color // 更新页面内容 } }
4.6.3 history 对比 hash
优势:
pushState 设置的 url 可以是同源下的任意 url ;而 hash 只能修改 # 后面的部分,因此只能设置当前 url 同文档的 url
pushState 设置的新的 url 可以与当前 url 一样,这样也会把记录添加到栈中;hash 设置的新值不能与原来的一样,一样的值不会触发动作将记录添加到栈中
pushState 通过 stateObject 参数可以将任何数据类型添加到记录中;hash 只能添加短字符串
pushState 可以设置额外的 title 属性供后续使用
劣势:
history 在刷新页面时,如果服务器中没有相应的响应或资源,就会出现404。因此,如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面
hash 模式下,仅 # 之前的内容包含在 http 请求中。对后端来说,即使没有对路由做到全面覆盖,也不会报 404
4.7 react@16.4 + 的生命周期
- 挂载阶段:constructor、render、componentDidMount
- 更新阶段:componentDidUpdate
- 卸载阶段:componentWillUnmount
4.8 useEffect 和 componentDidMount 有什么差异?
useEffect 会捕获 props 和 state。所以即便在回调函数里,你拿到的还是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。
4.9 调用 setState 之后发生了什么?
- 为当前节点创建一个 updateQueue 的更新列队。
- 然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。
- 然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。
- 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。
- 当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。
- 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。
- 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素
4.10 React有哪些优化性能的手段?
- 使用· memo、PureComponent 包裹组件,优化组件渲染
- 使用 Suspense、lazy 按需加载组件
- 批量更新
- 按优先级更新,及时响应用户
- 利用debounce、throttle 避免重复回调
- 使用 useMemo、useCallback 缓存,稳定 props 值
- 发布者订阅者跳过中间组件 Render 过程
- 状态下放,缩小状态影响范围
- 列表项使用 key 属性
- Hooks 按需更新
4.11 React 18 新特性
一、自动批处理state更新
跳过批处理
import { flushSync } from 'react-dom'; function handleClick() { flushSync(() => { setCounter(c => c + 1); }); // React has updated the DOM by now flushSync(() => { setFlag(f => !f); }); // React has updated the DOM by now }
二、新的Suspense SSR架构
- server端无需等待所有数据(和HTML)都ready再响应客户端,相反地,当应用骨架准备好时你就可以发送给客户端显示,剩余的内容将在它们ready之后流式传输给客户端。
- client端来说,不再需要等待所有 JavaScript 加载完毕才能开始渲染。可以结合code spliting与SSR一起使用,在server端的HTML(片段)将被保留(在server),当相关代码加载时,React将对其进行合成。不再需要等待所有组件都加载完成才运行用户交互。相反,可以依靠Selective Hydration 来确定用户与之交互的组件的优先级。
三、startTransition 非紧急更新
React将状态更新分为两类:
紧急更新(Urgent updates):反映直接的交互,如输入、点击、按键按下等等。
过渡更新(Transition updates):将UI从一个视图过渡到另一个视图。setInputValue(input); // 标记为非紧急更新 startTransition(() => { React.setSearchQuery(input); });
和setTimeout的区别
- 一个重要区别是setTimeout是「延迟」执行,startTransition是立即执行的,传递给startTransition的函数是同步运行,但是其内部的所有更新都会标记为非紧急,React将在稍后处理更新时决定如何render这些updates,这意味着将会比setTimeout中的更新更早地被render。
- 另一个重要区别是用setTimeout包裹的如果是内大面积的更新操作会导致页面阻塞不可交互,直到超时。这时候用户的输入、键盘按下等紧急更新操作将被阻止。而startTransition则不同,由于它所标记的更新都是可中断的,所以不会阻塞UI交互。即使用户输入发生变化,React也不必继续渲染用户不再感兴趣的内容。
- 最后,因为setTimeout是异步执行,哪怕只是展示一个小小的loading也要编写异步代码。而通过transitions,React可以使用hook来追踪transition的执行状态,根据transition的当前状态来更新loading。
4.12 为何要在componentDidMount里面发送请求
- 跟服务器端渲染(同构)有关系,如果在componentWillMount里面获取数据,fetch data会执行两次,一次在服务器端一次在客户端
- 在componentWillMount中fetch data,数据一定在render后才能到达,如果忘记了设置初始状态,用户体验不好
- react16.0以后,componentWillMount可能会被执行多次
5.webpack
5.1 webpack中什么是chunk?什么是bundle?
- 首先告诉 Webpack 一个入口文件,如 index.js 为起点作为打包,将入口文件的所有依赖项引入进来,这些依赖会跟入口文件形成一个文件(代码块),这个文件(代码块)就是 chunk
- 将这个代码块(chunk)进行处理,比如把 less 文件编译成 css,js 资源编译成浏览器能识别的 js 语法等等操作,这些就叫做打包,将打包好的资源再输出出去,这个输出的文件就叫 bundle
5.2 Webpack 五个核心概念分别是什么?
- Entry: 入口(Entry)指示 Webpack 以哪个文件为入口起点开始打包,分析内部构件依赖图
- Output: 输出(Output)指示 Webpack 打包后的资源 bundles 输出到哪里去,以及如何命名
- Loader: Loader 能让 Webpack 处理非 JavaScript/json 文件(Webpack 自身只能处理 JavaScript/json )
- Plugins: 插件(Plugins)可以用于执行范围更广的任务,包括从打包优化和压缩到重新定义环境中的变量
- Mode: 模式(Mode)指示 Webpack 使用相应模式的配置,只有development(开发环境)和production(生产环境)两种模式
5.3 有哪些常见的Loader?它们是解决什么问题的?
- css-loader:将 css 文件变成 CommonJS 模块加载 js 中,里面内容是样式字符串
- style-loader:创建 style 标签,将 js 中的样式资源插入进行,添加到 head 中生效
- url-loader:在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
- ile-loader:打包其他资源(除了css/js/html 资源)
- html-loader:处理 html 文件中的 img
- babel-loader:把 ES6 转换成 ES5
- eslint-loader:通过 ESLint 检查 JavaScript 代码
5.4 有哪些常见的Plugin?它们是解决什么问题的?
- html-webpack-plugin:可以复制一个有结构的html文件,并自动引入打包输出的所有资源(JS/CSS)
- clean-webpack-plugin:重新打包自动清空 dist 目录
- mini-css-extract-plugin:提取 js 中的 css 成单独文件
- optimize-css-assets-webpack-plugin:压缩css
- uglifyjs-webpack-plugin:压缩js
- commons-chunk-plugin:提取公共代码
5.5 webpack 构建流程
- 根据 entry 配置项找出所有的入口文件
- 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块
- Loader 翻译完所有模块后,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
5.6 webpack的热更新是什么?
优化打包构建速度,一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块),极大提升构建速度
- 样式文件:可以使用HMR功能,因为style-loader内部实现了
- JS文件:默认没有HMR功能,需要修改js代码,添加支持HMR功能。入口文件做不了HMR功能,只能处理非入口js文件
- HTML文件:默认没有HMR功能,同时会导致 html 文件不能热更新(即修改没有任何反应)
HTML文件不用做HMR功能,因为只有一个html文件
5.7 webpack优化?
开发环境下:
- 开启HMR功能,优化打包构建速度
- 配置 devtool: ‘source-map’,优化代码运行的性能
生产环境下:
- oneOf 优化: 默认情况下,假设设置了7、8个loader,每一个文件都得通过这7、8个loader处理(过一遍),浪费性能,使用 oneOf 找到了就能直接用,提升性能
- 开启 babel 缓存: 当一个 js 文件发生变化时,其它 js 资源不用变
- code split 分割: 将js文件打包分割成多个bundle,避免体积过大
- 懒加载和预加载
- PWA 网站离线访问
- 多进程打包: 开启多进程打包,主要处理js文件(babel-loader干的活久),进程启动大概为600ms,只有工作消耗时间比较长,才需要多进程打包,提升打包速度
- dll 打包第三方库
- code split将第三方库都打包成一个bundle,这样体积过大,会造成打包速度慢
- 是将第三方库打包成多个bundle,从而进行速度优化
5.8 hash、chunkhash、contenthash三者的区别?
浏览器访问网站后会强缓存资源,第二次刷新就不会请求服务器(一般会定个时间再去请求服务器),假设有了bug改动了文件,但是浏览器又不能及时请求服务器,所以就用到了文件资源缓存(改变文件名的hash值)
- hash:不管文件变不变化,每次wepack构建时都会生成一个唯一的hash值
- chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题:js和css同属于一个chunk,修改css,js文件同样会被打包- contenthash:根据文件的内容生成hash值。不同文件hash值一定不一样
5.9 commonJS和ES6模块化的区别
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载
- CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,具体点就是指针指向不能变,类似const 。
5.10 webpack 和 vite 的区别
webpack是先打包再启动开发服务器,vite是直接启动开发服务器,然后按需编译依赖文件。
由于现代浏览器本身就支持ES Modules,会主动发起请求去获取所需文件。vite充分利用这点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack先打包,交给浏览器执行的文件是打包后的
由于vite使用的是ES Module,所以代码中不可以使用CommonJs
6.TypeScript
6.1 对比JS的优势
- 提早发现代码中的Bug
- 提高代码的可读性
- 减少了复杂的错误处理逻辑
6.2 什么是泛型
定义函数,接口或类时,不预先指定具体类型,而是在使用的时候指定具体类型
// 定义类型的时候 动态指定值 interface KeyPair<T,U> { key: T; value: U; } let kp1: KeyPair<number,string> = { key: 123, value: 'str' } let kp2: keypair<string,number> = { key: 'str', value: 123 }
6.3 type 跟 interface 的区别
相同点:
- 都可以用来定义对象或函数的形状
- 都支持继承,并且可以互相继承
不同点:
- type 可以定义基本类型
- type 可以声明联合类型
- interface 可以生命合并
6.4 never,void,unknown 类型
- never
- 一个从来不会有返回值的函数,即死循环(如:如果函数内含有 while(true) {})
- 一个总是会抛出错误的函数(如:function foo() { throw new Error(‘Not Implemented’) },foo 的返回类型是 never)
- void
- 表示没有任何返回值的函数
- 也可以声明一个变量为 void ,但只能将它赋值为 undefined 或 null
- unknown
- 用于描述类型不确定的变量
- 必须确定类型才能做后续操作
- 只能赋值给any和unknown类型
- unknown 除了与 any 以外, 与其它任何类型组成的联合类型最后都是 unknown 类型
6.5 交叉类型(&)和接口继承(extends)对比
7. 代码题
7.1 数组拉平
// 第一种方式 function myFlat(arr, newArr = []) { for (let x of arr) { if (Array.isArray(x)) { myFlat(x, newArr) } else { newArr.push(x) } } return newArr } console.log(myFlat([1, [2, 3], [4, [5, 6]]])) // 第二种方式 function flat(arr) { return arr.reduce((result, item) => { return result.concat(Array.isArray(item) ? flat(item) : item) }, []) } console.log(flat([1, [2, 3], [4, [5, 6]]]))
7.2 深拷贝
function deepCopy(obj) { if (typeof obj === 'object') { var result = Array.isArray(obj) ? [] : {} for (let i in obj) { result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i] } } else { var result = obj } return result } const arr = [1, [2, 3], [4, [5, 6]]] const newArr = deepCopy(arr) newArr[1][0] = 4 console.log(arr, newArr)
7.3 手写 bind
Function.prototype.bind = function (context, ...args) { context = context || window; // 如果没有提供上下文,默认使用 window 对象 const fnSymbol = Symbol('fn'); // 创建一个唯一的符号,作为临时存放函数的属性名 context[fnSymbol] = this; // 将要绑定的函数(即当前函数)赋值给上下文对象的一个临时属性 return function (..._args) { // 返回一个新的函数 args = args.concat(_args); // 将初始参数和新参数合并 context[fnSymbol](...args); // 调用上下文对象的临时属性(即原函数),并传入所有合并后的参数 Reflect.deleteProperty(context, fnSymbol); // 之后删除临时属性 } }
7.4 防抖与节流
- 防抖:在用户停止输入一段时间后执行。而在短时间内多次触发则只会保留最后一次的调用。
- 节流:在固定时间内允许执行一次,过多的触发会被忽略。
// 防抖 function debounce(fn, wait = 300) { let timer = null; // 初始化一个定时器变量 return function (...args) { timer && clearTimeout(timer); // 如果定时器存在,清除它 timer = setTimeout(() => { fn.apply(this, args); // 在延迟时间到后,调用fn,并保留上下文和参数 }, wait); // 设定延迟时间 }; } // 节流 function throttle(f, wait = 1000) { let timer = null; // 初始化一个定时器变量 let flag = true; // 初始化一个标志,表示函数能否执行 return (...args) => { if (timer) return; // 如果定时器存在,则直接返回,避免函数执行 if (flag) { f(...args); // 允许执行函数,调用它,传入参数 flag = false; // 设置标志为false,表示函数在等待期间不能执行 } timer = setTimeout(() => { flag = true; // 等待结束后,重置标志 timer = null; // 清空定时器 }, wait); }; } window.onclick = throttle(() => { console.log('点击') })
7.5 手写题Promise
const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class MyPromise { #handles = [] constructor(fn) { this.value = null // 结果 this.state = PENDING // 状态 try { fn(this.resolve, this.reject) } catch (error) { this.reject(error) } } resolve = value => { if (this.state === PENDING) { this.state = FULFILLED this.value = value this.#handles.forEach(({ onFulfilled, resolve, reject }) => { queueMicrotask(() => { try { const res = onFulfilled(value) if (res instanceof MyPromise) { res.then(resolve, reject) } else { resolve(res) } } catch (err) { reject(err) } }) }) } } reject = value => { if (this.state === PENDING) { this.state = REJECTED this.value = value this.#handles.forEach(({ onRejected, reject }) => { queueMicrotask(() => { try { if (onRejected) { const res = onRejected(value) reject(res) // Use the resolved value of onRejected if provided } else { reject(value) } } catch (err) { reject(err) } }) }) } } then = (onFulfilled, onRejected) => { const p = new MyPromise((resolve, reject) => { this.#handles.push({ onFulfilled: onFulfilled || (v => v), onRejected, resolve, reject }) if (this.state === FULFILLED) { queueMicrotask(() => { try { const res = onFulfilled(this.value) if (res instanceof MyPromise) { res.then(resolve, reject) } else { resolve(res) } } catch (err) { reject(err) } }) } if (this.state === REJECTED) { queueMicrotask(() => { if (onRejected) { try { const res = onRejected(this.value) reject(res) } catch (err) { reject(err) } } else { reject(this.value) } }) } }) return p } catch = onRejected => { return this.then(null, onRejected) } finally = onFinallyed => { return this.then(onFinallyed, onFinallyed) } static resolve = value => { if (value instanceof MyPromise) { return value } return new MyPromise(resolve => resolve(value)) } static reject = value => { return new MyPromise((undefined, reject) => reject(value)) } static race = (promises) => { return new MyPromise((resolve, reject) => { if (!Array.isArray(promises)) { return reject(new TypeError('Argument is not iterable')) } promises.forEach(p => { MyPromise.resolve(p).then(res => { resolve(res) }, err => { reject(err) }) }) }) } static all = (promises) => { return new MyPromise((resolve, reject) => { if (!Array.isArray(promises)) { return reject(new TypeError('Argument is not iterable')) } !promises.length && resolve(promises) const results = [] let count = 0 promises.forEach((p, index) => { MyPromise.resolve(p).then( res => { results[index] = res count++ count === promises.length && resolve(results) }, err => { reject(err) } ) }) }) } } // 测试 const p1 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('success') }, 2000) }) const p2 = new MyPromise((resolve, reject) => { setTimeout(() => { reject('fail') }, 1000) }) MyPromise.race([p1, p2]).then(res => { console.log(res) }, err => { console.log(err) })
7.6 手写发布订阅模式
class Event { // 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个) #handlers = {} // 事件添加方法,参数有事件名和事件方法 addEventListener(type, handler) { // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器 if (!(type in this.#handlers)) { this.#handlers[type] = [] } // 将事件存入 this.#handlers[type].push(handler) } // 触发事件两个参数(事件名,参数) dispatchEvent(type, ...params) { // 若没有注册该事件则抛出错误 if (!(type in this.#handlers)) { return new Error('未注册该事件') } // 便利触发 this.#handlers[type].forEach(handler => { handler(...params) }) } // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布) removeEventListener(type, handler) { // 无效事件抛出 if (!(type in this.#handlers)) { return new Error('无效事件') } if (!handler) { // 直接移除事件 delete this.#handlers[type] } else { const idx = this.#handlers[type].findIndex(ele => ele === handler) // 抛出异常事件 if (idx === -1) { return new Error('无该绑定事件') } // 移除事件 this.#handlers[type].splice(idx, 1) if (this.#handlers[type].length === 0) { delete this.#handlers[type] } } } }
7.7 设计控制并发请求任务队列
场景:前端页面zh
要求:
- 最多同时执行的任务数为10个
- 当前任务执行完成后,释放队列空间,自动执行下一个任务
- 所有任务添加到任务队列后,自动开始执行任务
function createTask(i) { return () => { // 模拟网络请求 return new Promise(resolve => { setTimeout(() => { resolve(i) }, 2000) }) } } class TaskQueue { constructor() { this.max = 10 // 最大数 10 this.taskList = [] // 存储任务,模拟队列先进先出 setTimeout(() => { // 自动执行 this.run() }) } addTask(task) { this.taskList.push(task) } run() { const length = this.taskList.length if (!length) return // 没有任务了 let count = Math.min(this.max, length) // 最大并发数 for (let i = 0; i < count; i++) { const task = this.taskList.shift() // 取出第一个任务 task().then(res => { console.log(res) }).finally(() => { count-- if (count === 0) { // 并发请求队列空了,继续下一轮 this.run() } }) } } } const taskQueue = new TaskQueue() for (let i = 0; i < 20; i++) { const task = createTask(i) taskQueue.addTask(task) }