Skip to content

S05-06 JS高级-进程、任务队列、事件循环、错误处理、Storage、正则、手写

[TOC]

浏览器进程、线程

进程和线程

线程和进程是操作系统中的两个概念:

  • 进程process):计算机已经运行的程序,是操作系统管理程序的一种方式;

  • 线程thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中

听起来很抽象,这里还是给出我的解释:

  • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);

  • 线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;

  • 所以我们也可以说进程是线程的容器;

再用一个形象的例子解释:

  • 操作系统类似于一个大工厂;

  • 工厂中里有很多车间,这个车间就是进程;

  • 每个车间可能有一个以上的工人在工厂,这个工人就是线程;

操作系统-进程-线程

image-20230620150750570

操作系统的工作方式

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;

  • 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;

  • 对于用户来说是感受不到这种快速的切换的;

你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程:

image-20230620150803432

浏览器中的JavaScript线程

我们经常会说*JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程**:浏览器或者**Node

浏览器是一个进程吗,它里面只有一个线程吗?

  • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;

  • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;

JavaScript的代码执行是在一个单独的线程中执行的:**

  • 这就意味着JavaScript的代码,在同一个时刻只能做一件事

  • 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞

所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:

  • 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;

  • 比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可;

浏览器的事件循环

如果在执行JavaScript代码的过程中,有异步操作呢?

  • 中间我们插入了一个setTimeout的函数调用;

  • 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行;

image-20230620150832286

image-20230620150844676

宏任务、微任务队列

宏任务和微任务

但是事件循环中并非只维护着一个队列,事实上是有两个队列:

  • 宏任务队列(macrotask queue):ajaxsetTimeoutsetIntervalDOM监听UI Rendering

  • 微任务队列(microtask queue):Promise的then回调Mutation Observer API、*queueMicrotask()*等

那么事件循环对于两个队列的优先级是怎么样的呢?

  • 1.main script中的代码优先执行(编写的顶层script代码);

  • 2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行

    • 也就是宏任务执行之前,必须保证微任务队列是空的;
    • 如果不为空,那么就优先执行微任务队列中的任务(回调);

下面我们通过几到面试题来练习一下。

Promise面试题

Promise面试题

image-20230620150913872

image-20230620150922534

image-20230901171805611

promise async await 面试题

image-20230620150931234

image-20230620150938885

image-20230907144738577

事件循环

Node的事件循环

浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

这里我们来给出一个Node的架构图:

  • 我们会发现libuv中主要维护了一个EventLoop和worker threads(线程池);

  • EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等

libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方;

image-20230620150957682

Node事件循环的阶段

我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:

  • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;

  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;

但是一次完整的事件循环Tick分成很多个阶段:

  • 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。

  • 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。

  • idle, prepare:仅系统内部使用。

  • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;

  • 检测(check):setImmediate() 回调函数在这里执行。

  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。

Node事件循环的阶段图解

image-20230620151031746

Node的宏任务和微任务

我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;

  • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;

但是,Node中的事件循环不只是 微任务队列和 宏任务队列:

  • 微任务队列:

    • next tick queue:process.nextTick;
    • other queue:Promise的then回调、queueMicrotask;
  • 宏任务队列:

    • timer queue:setTimeout、setInterval;
    • poll queue:IO事件;
    • check queue:setImmediate;
    • close queue:close事件;

Node事件循环的顺序

所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

  • next tick microtask queue;

  • other microtask queue;

  • timer queue;

  • poll queue;

  • check queue;

  • close queue;

Node执行面试题

image-20230620151123639

image-20230620151129694

throw、try catch

错误处理方案

开发中我们会封装一些工具函数,封装之后给别人使用:

  • 在其他人使用的过程中,可能会传递一些参数;

  • 对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果;

很多时候我们可能验证到不是希望得到的参数时,就会直接return:

  • 但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined;

  • 事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道函数内部报错了;

如何可以让一个函数告知外界自己内部出现了错误呢?

  • 通过throw关键字,抛出一个异常

throw语句:

  • throw语句用于抛出一个用户自定义的异常

  • 当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行);

如果我们执行代码,就会报错,拿到错误信息的时候我们可以及时的去修正代码。

image-20230907151315727

image-20230907151319262

throw关键字

throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息:

image-20230620151503399

throw关键字可以跟上哪些类型呢?

  • 基本数据类型:比如number、string、Boolean

  • 对象类型:对象类型可以包含更多的信息

但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类:

image-20230620151513806

image-20230907152324425

Error类型

事实上,JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象:

image-20230620151524499

Error包含三个属性:

  • messsage:创建Error对象时传入的message;

  • name:Error的名称,通常和类的名称一致;

  • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;

Error有一些自己的子类:**

  • RangeError:下标值越界时使用的错误类型;

  • SyntaxError:解析语法错误时使用的错误类型;

  • TypeError:出现类型错误时,使用的错误类型;

异常的处理

我们会发现在之前的代码中,一个函数抛出了异常,调用它的时候程序会被强制终止

  • 这是因为如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传递到上一个函数调用中

  • 而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行

我们先来看一下这段代码的异常传递过程:

  • foo函数在被执行时会抛出异常,也就是我们的bar函数会拿到这个异常;

  • 但是bar函数并没有对这个异常进行处理,那么这个异常就会被继续传递到调用bar函数的函数,也就是test函数;

  • 但是test函数依然没有处理,就会继续传递到我们的全局代码逻辑中;

  • 依然没有被处理,这个时候程序会终止执行,后续代码都不会再执行了;

image-20230620151542173

image-20230620151548898

异常的捕获

但是很多情况下当出现异常时,我们并不希望程序直接退出,而是希望可以正确的处理异常:

  • 这个时候我们就可以使用try catch

image-20230620151559919

image-20230620151614405

在ES10(ES2019)中,catch后面绑定的error可以省略。

当然,如果有一些必须要执行的代码,我们可以使用finally来执行:

  • finally表示最终一定会被执行的代码结构;

注意:如果try和finally中都有返回值,那么会使用finally当中的返回值;

Storage

认识Storage

WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式:

  • localStorage:本地存储,提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留;

  • sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除;

image-20230620151632534

image-20230620151646589

localStorage和sessionStorage的区别

我们会发现localStorage和sessionStorage看起来非常的相似。

那么它们有什么区别呢?

  • 验证一:关闭网页后重新打开,localStorage会保留,而sessionStorage会被删除;

  • 验证二:在页面内实现跳转,localStorage会保留,sessionStorage也会保留;

  • 验证三:在页面外实现跳转(打开新的网页),localStorage会保留,sessionStorage不会被保留;

Storage常见的方法和属性

Storage有如下的属性和方法:

属性:

  • Storage.length:只读属性
    • 返回一个整数,表示存储在Storage对象中的数据项数量;

方法:

  • Storage.key(index):该方法接受一个数值n作为参数,返回存储中的第n个key名称;

  • Storage.getItem(key):该方法接受一个key作为参数,并且返回key对应的value;

  • Storage.setItem(key, value):该方法接受一个key和value,并且将会把key和value添加到存储中。

    • 如果key已经存在,则更新其对应的值;
  • Storage.removeItem(key):该方法接受一个key作为参数,并把该key从存储中删除;

  • Storage.clear():该方法的作用是清空存储中的所有key;

封装-Cache

基础封装

image-20230907173656557

image-20230907173654255

优化:存储对象类型

原生方法

image-20230907174001581

image-20230907174218915

image-20230907174248358

▸优化:兼容local和session

image-20230907174517196

image-20230907174540612

正则表达式

什么是正则表达式?

我们先来看一下维基百科对正则表达式的解释:

  • 正则表达式(英语:Regular Expression,常简写为regex、regexp或RE),又称正则表示式正则表示法规则表达式常规表示法,是计算机科学的一个概念;

  • 正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串**。**

  • 许多程序设计语言都支持利用正则表达式进行字符串操作。

简单概况:正则表达式是一种字符串匹配利器,可以帮助我们搜索、获取、替代字符串

在JavaScript中,正则表达式使用RegExp类来创建,也有对应的字面量的方式:

  • 正则表达式主要由两部分组成:模式(patterns)和修饰符(flags)

image-20230620151743818

正则表达式使用方法

有了正则表达式我们要如何使用它呢?

  • JavaScript中的正则表达式被用于 RegExp的 exec 和 test 方法;

  • 也包括 String 的 match、matchAll、replace、search 和 split 方法;

image-20230620151803217

正则常用方法

  • RegExp
  • RegExp.prototype.exec()返回:array
  • RegExp.prototype.test(str)返回:boolean,检测某个字符串是否符合正则的规则
  • String
  • String.prototype.match()返回:
  • String.prototype.matchAll()返回:
  • String.prototype.search()返回:
  • String.prototype.replace()返回:
  • String.prototype.split()返回:

示例: test

image-20230908163503949

image-20230908163517047

示例: exec

image-20230908165552934

image-20230908165540893

示例: match

image-20230908165808719

image-20230908165800912

示例: matchAll

image-20230908165953103

image-20230908165941393

示例: replace / replaceAll

image-20230907181511081

示例: split

image-20230908170156776

image-20230908170142477

示例: search

image-20230908170333353

image-20230908170232765

修饰符

常见的修饰符:

image-20230620151813052

需求:

  • 获取一个字符串中所有的abc;

  • 将一个字符串中的所有abc换成大写;

image-20230620151828358

规则

字符类

字符类(Character classes) 是一个特殊的符号,匹配特定集中的任何符号。

  • \d,匹配任何数字 (阿拉伯数字)。相当于 [0-9]
  • \s,匹配单个空白字符,包括空格、制表符、换页符、换行符和其他 Unicode 空格。相当于 [\f\n\r\t\v]
  • \w,匹配基本拉丁字母中的任何字母数字字符,包括下划线。相当于 [A-Za-z0-9_]
  • .,匹配除行终止符之外的任何单个字符:\n, \r
  • \D,匹配任何非数字 (阿拉伯数字) 的字符。相当于[^0-9]
  • \S,匹配除空格以外的单个字符。相当于 [^\f\n\r\t\v]
  • \W,匹配任何不是来自基本拉丁字母的单词字符。相当于 [^A-Za-z0-9_]
  • \t,匹配水平制表符。
  • \r,匹配回车符。
  • \n,匹配换行符。
  • \,指示应特殊处理或“转义”后面的字符。

image-20230620151851429

断言

边界类断言

  • ^,匹配输入的开头。如果多行模式设为 true,^ 在换行符后也能立即匹配,
  • $,匹配输入的结束。如果多行模式设为 true,$ 在换行符前也能立即匹配,
  • \b,匹配一个单词的边界
  • \B,匹配非单词边界。

其他断言

  • x(?=y)先行断言: x 被 y 跟随时匹配 x。
  • x(?!y)先行否定断言: x 没有被 y 紧随时匹配 x。
  • (?<=y)x后行断言: x 跟随 y 的情况下匹配 x。
  • (?<!y)x后行否定断言: x 不跟随 y 时匹配 x。

符号 ^ 和符号 $ 在正则表达式中具有特殊的意义,它们被称为“锚点”。

  • 符号 ^ 匹配文本开头;

  • 符号 $ 匹配文本末尾;

image-20230620151910531

词边界(Word boundary)

  • 词边界 \b 是一种检查,就像 ^ 和 $ 一样,它会检查字符串中的位置是否是词边界。

  • 词边界测试 \b 检查位置的一侧是否匹配 \w,而另一侧则不匹配 “\w”

在字符串 Hello, Java! 中,以下位置对应于 \b:

image-20230620151922256

匹配下面字符串中的时间:

image-20230908180902646

image-20230908180920001

转义字符串

如果要把特殊字符作为常规字符来使用,需要对其进行转义:

  • 只需要在它前面加个反斜杠;

常见的需要转义的字符:

image-20230620151944976

  • 斜杠符号 / 并不是一个特殊符号,但是在字面量正则表达式中也需要转义;

示例: 匹配所有以.js或者.jsx结尾的文件名

image-20230620152001287

在webpack当中,匹配文件名时就是以这样的方式。

示例: 只匹配.

image-20230912154348961

image-20230912154415382

集合、范围

有时候我们只要选择多个匹配字符的其中之一就可以:

  • 在方括号 […] 中的几个字符或者字符类意味着“搜索给定的字符中的任意一个”;

集合(Sets)

  • 比如说,[eao] 意味着查找在 3 个字符 ‘a’、‘e’ 或者 ` ‘o’ 中的任意一个;

范围(Ranges)

  • 方括号也可以包含字符范围;

  • 比如说,[a-z] 会匹配从 a 到 z 范围内的字母,[0-5] 表示从 0 到 5 的数字;

  • [0-9A-F] 表示两个范围:它搜索一个字符,满足数字 0 到 9 或字母 A 到 F;

  • \d —— 和 [0-9] 相同;

  • \w —— 和 [a-zA-Z0-9_] 相同;

示例: 匹配手机号码

image-20230620152027674

js
//  手机号
/^1[3-9]\d{9}$/

排除范围:除了普通的范围匹配,还有类似[^…]的“排除”范围匹配;

量词

假设我们有一个字符串 +7(903)-123-45-67,并且想要找到它包含的所有数字。

  • 因为它们的数量是不同的,所以我们需要给与数量一个范围;

  • 用来形容我们所需要的数量的词被称为量词( Quantifiers )

语法:

  • {m} : 确切的位数:{5}

  • {m,n} : 某个范围的位数:{3,5}

注意: {m,n} 此处逗号后面不能加空格

缩写:

  • +:代表“一个或多个”,相当于 {1,}

  • ?:代表“零个或一个”,相当于 {0,1}。换句话说,它使得符号变得可选;

  • \*:代表着“零个或多个”,相当于 {0,}。也就是说,这个字符可以多次出现或不出现;

示例: 匹配开始或结束标签

image-20230620152045806

贪婪模式、惰性模式

如果我们有这样一个需求:匹配下面字符串中所有使用《》包裹的内容

image-20230620152055911

image-20230912163254643

贪婪模式( Greedy)

默认情况下的匹配规则是查找到匹配的内容后,会继续向后查找,一直找到最后一个匹配的内容,这种匹配的方式,我们称之为贪婪模式(Greedy)

惰性模式( lazy)

懒惰模式中的量词与贪婪模式中的是相反的。

  • 只要获取到对应的内容后,就不再继续向后匹配

  • 我们可以在量词后面再加一个问号 ‘?’ 来启用它

  • 所以匹配模式变为 *? 或 +?,甚至将 '?' 变为 ??

image-20230620152104313

捕获组

模式的一部分可以用括号括起来 (...),这称为“捕获组(capturing group)”。

作用:

  • 它允许将匹配的一部分作为结果数组中的单独项;

  • 它将括号视为一个整体;

方法 str.match(regexp),如果 regexp没有 g 标志,将查找第一个匹配并将它作为一个数组返回:

  • 在索引 0 处:完全匹配。

  • 在索引 1 处:第一个括号的内容。

  • 在索引 2 处:第二个括号的内容。

  • …等等…

示例: 匹配到HTML标签,并且获取其中的内容

image-20230620152118636

image-20230912164931110

image-20230912164955229

示例: 将捕获组作为整体

image-20230912165337638

image-20230912165326853

命名组:

  • 用数字记录组很困难。

  • 对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号起个名字。

  • 这是通过在开始括号之后立即放置?<name>来完成的。如(?<name>xxx)

image-20230912165701636

image-20230912165716137

非捕获组:

  • 有时我们需要括号才能正确应用量词,但我们不希望它们的内容出现在结果中

  • 可以通过在开头添加?:来排除组。如 (?:xxx)

image-20230620152200110

or:

or是正则表达式中的一个术语,实际上是一个简单的“”。

  • 在正则表达式中,它用竖线|表示;

  • 通常会和捕获组一起来使用,在其中表示多个值

image-20230912170217506

image-20230912170242399

or和集合的区别

  • or可以给多个字符分类:(abc|cba|nba)表示abccbanba
  • 集合只能给单个字符分类:[abc]表示abc

案例练习-歌词解析

歌词解析:http://123.207.32.32:9002/lyric?id=167876

image-20230912170807483

image-20230620152218711

案例练习-时间格式化

时间格式化:从服务器拿到时间戳,转成想要的时间格式

image-20230912175928056

image-20230620152254651

**更多正则查询:**https://c.runoob.com/front-end/854/

防抖、节流

简介

防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中

  • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。

  • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题

但是很多前端开发者面对这两个功能,有点摸不着头脑:

  • 某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);

  • 某些开发者可以区分,但是不知道如何应用;

  • 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;

接下来我们会一起来学习防抖和节流函数:

  • 我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;

  • 并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;

防抖函数

防抖函数(debounce)

我们用一副图来理解一下它的过程:

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

  • 当事件密集触发时,函数的触发会被频繁的推迟

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

image-20230620152349381

应用场景:

防抖的应用场景很多:

  • 搜索联想oninput,输入框中频繁的输入内容,搜索或者提交信息;

  • 频繁点击事件onclick,频繁的点击按钮,触发某个事件;

  • 浏览器滚动事件onscroll,监听浏览器滚动事件,完成某些特定操作;

  • 浏览器缩放事件onresize,用户缩放浏览器的resize事件;

示例: 搜索联想

我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容

image-20230620152423458

比如想要搜索一个MacBook:

  • 当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;

  • 当继续输入ma时,再次发送网络请求;

  • 那么macbook一共需要发送7次网络请求;

  • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;

但是我们需要这么多次的网络请求吗?

  • 不需要,正确的做法应该是在合适的情况下再发送网络请求;

  • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;

  • 比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;

  • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;

这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数

节流函数

节流函数(throttle)

我们用一副图来理解一下节流的过程

  • 当事件触发时,会执行这个事件的响应函数;

  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数

  • 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;

image-20230620152442909

应用场景:

  • 页面滚动事件:监听页面的滚动事件;

  • 鼠标移动事件

  • 频繁点击事件:用户频繁点击按钮操作;

  • 游戏某些设计:游戏中的一些设计,如发射子弹;

很多人都玩过类似于飞机大战的游戏

在飞机大战的游戏中,我们按下空格会发射一个子弹:

  • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;

  • 比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;

  • 但是事件是触发了10次的,响应的函数只触发了一次;

image-20230620152511039

生活中的例子

生活中防抖的例子:

比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。

如果在五分钟的时间内,没有同学问我问题,那么我就下课了;

  • 在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;

  • 如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);

生活中节流的例子:

比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;

如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;

案例准备

我们通过一个搜索框来延迟防抖函数的实现过程:

  • 监听input的输入,通过打印模拟网络请求

测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:

image-20230620152534787

image-20230620152544034

underscore

Underscore库的介绍

事实上我们可以通过一些第三方库来实现防抖操作:

  • lodash

  • underscore

这里使用underscore

  • 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;

  • 但是目前我看到underscore还在维护,lodash已经很久没有更新了;

Underscore的官网: https://underscorejs.org/

安装:

Underscore的安装有很多种方式:

  • 下载Underscore,本地引入;

  • 通过CDN直接引入;

  • 通过包管理工具(npm)管理安装;

这里我们直接通过CDN:

html
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>

Underscore实现防抖和节流

image-20230620152632696

手写题

手写-防抖函数

我们按照如下思路来实现:

  • 防抖基本功能实现:可以实现防抖效果
  • 优化一:优化参数和this指向
  • 优化二:优化取消操作(增加取消功能)
  • 优化三:优化立即执行效果(第一次立即执行)
  • 优化四:优化返回值

1、基本实现

image-20230913170819191

2、优化:参数和this绑定

this指向

image-20230913171801374

参数

image-20230913172010545

image-20230913172141908

3、优化:取消功能

image-20230913173224970

image-20230913173251166

4、优化:第一次立即执行

  • immediate:控制否时启用立即执行功能
  • isInvoke:控制函数是否已经立即执行一次了

image-20230913175323462

image-20230913175525645

5、优化:返回值

image-20230914120206319

image-20230914120006631

image-20230914120016985

手写-节流函数

我们按照如下思路来实现:

  • 节流函数的基本实现:可以实现节流效果
  • 优化一:绑定this和参数
  • 优化二:控制立即执行,节流最后一次也可以执行
  • 优化三:优化添加取消功能
  • 优化四:优化返回值问题

1、基本实现

image-20230914145011970

image-20230914144933945

2、优化:绑定this和参数

image-20230914145650570

image-20230914145254694

3、优化:控制立即执行

image-20230914150800082

image-20230914145957177

4、优化:控制执行最后一次

思路一: 给每次点击时添加一个定时器,延迟时间设为waitTime,当再次点击时取消上次的定时器,重新添加一个。

思路二: 在每个执行fn函数的节点,添加一个延迟时间为waitTime的定时器,当用户在fn函数执行节点的时间上也点击了一次就取消该定时器(使用中

image-20230914161141810

4、优化:取消功能

image-20230914173427834

image-20230914173431437

5、优化:返回值

image-20230914174045131

image-20230914173857111

手写-深拷贝函数

前面我们已经学习了对象相互赋值的一些关系,分别包括:

  • 引用赋值:指向同一个对象,相互之间会影响;

  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;

  • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;

深拷贝实现方式:

  • JSON.parse
  • 第三方库:underscore、lodash
  • 自己实现

前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse

  • 这种深拷贝的方式其实对于函数Symbol等是无法处理的;

  • 并且如果存在对象的循环引用,也会报错的;

js
const obj = JSON.parse(JSON.stringify(info))

自定义深拷贝函数:

  • 1.自定义深拷贝的基本功能;

  • 2.对Symbol的key进行处理;

  • 3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;

  • 4.对循环引用的处理;

工具函数:判断对象

image-20230915090846425

1、基本实现

image-20230915091537083

image-20230915091526589

2、优化:区分数组和对象

image-20230915092041870

image-20230915093627617

3、优化:其他类型-处理set

image-20230915095300660

image-20230915095231434

4、优化:其他类型-处理map

5、优化:其他类型-处理function

function: 不需要深拷贝

image-20230915095547885

6、优化:其他类型-处理Symbol为值

image-20230915102436519

image-20230915102117655

7、优化:其他类型-处理Symbol为key

image-20230915102503264

image-20230915102523200

8、优化:处理循环引用

方案一:将每次新创建的对象保存到Map中,每次遍历前判断之前是否已经保存过了该对象

问题:需要在deeCopy外部定义一个map,并且每次拷贝完成后map依然会形成对对象的强引用,没有销毁

image-20230915104349385

方案二(推荐):使用WeakMap替代Map;将map放入参数中并设置一个默认值new WeakMap()

image-20230915105909339

手写-事件总线

自定义事件总线属于一种观察者模式,其中包括三个角色:

  • 发布者(Publisher):发出事件(Event);

  • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);

  • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;

当然我们可以选择一些第三方库

  • Vue2默认是带有事件总线的功能;

  • Vue3中推荐一些第三方库,比如mitt

当然我们也可以实现自己的事件总线:

  • 事件的监听方法on;

  • 事件的发射方法emit;

  • 事件的取消监听off;

1、基本实现

image-20230915114603806

image-20230915114736411

2、优化:绑定参数

image-20230915114132235

3、优化:移除监听