💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 回调函数 1.自己定义的函数 2.回调函数没有亲自调用,但是最终应该执行(例如```settimeout```) 分为:1. 同步回调函数 2.异步回调函数 1. 同步回调函数 ``` const arr = [1, 2, 3] arr.forEach(item => { //遍历回调函数 不会放入队列中,一上来就要执行完 console.log(item) ) console.log('执行函数') //问:两个函输出谁先执行? ``` 2. 异步回调函数 ``` setTimeout(() => { //异步回调函数,会放入队列中 将来执行 console.log('timeout cb') }, 0) console.log('回调函数') ``` ## js中的[Error](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error) 1. 错误类型 Error: 所有错误的父类型 ReferenceError: 引用的变量不存在 TypeError: 数据类型不正确的错误 RangeError: 数据值不在其所允许的范围内 SyntaxError: 语法错误 2. 错误处理 捕获错误: try ... catch 抛出错误:throw error 3. 错误对象 message属性:错误相关信息 stack属性:函数调用栈记录信息 >link to errShow code ## Promise ### WHAT? **1.1 抽象表达 是js中进行异步编程的**新**的解决方案(旧的是啥? 就是纯的异步执行的方法)** **1.2. 具体表达:** 1)从语法上来说,Promise是一个构造函数 2) 从功能上来说Promise对象用来封装一个异步操作并可以获取其结果 **2.1 Promise的状态改变** 2.1.1 pending 变为resolved 2.1.2 pending变为 rejected 说明:只有这2种,且一个promise对象只能改变一次 无论变为成功还是失败,都共有一个结果数据 成功的结果数据一般称为value,失败的结果数据一般称为reason **3.1 Promise的流程** ![](https://img.kancloud.cn/bc/36/bc36cf455b7a08e537a3eb96d308b99a_3837x1486.png) >执行操作 使用一下 link to usePromise code ### Why? 用它自然是有其**优点**才会使用 1. 指定回调函数的方式更加灵活: 纯回调:必须在启动异步任务前指定 promise:启动异步任务 => 返回promise对象 => 给promise对象绑定回调函数(甚至可以在异步执行结束后再指定回调也可以) 2. 支持链式调用,可以解决回调地狱问题 回调地狱:回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调函数执行的条件 回调地狱的缺点?不便于阅读/不便于异常处理 解决方案: promise链式调用 终极解决方式:async/await >link to whyToUse code ### HOW? #### promise的API [API](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise#%E6%96%B9%E6%B3%95) #### promise的几个关键问题 1. 如何改变promise的状态? (1)resolve(value) :如果当前是pendding状态就会变为resolved (2)reject(reason) :如果当前是pendding状态就会变为rejected (3)抛出异常:如果当前是pendding就会变为rejected 2. 一个promise指定多个成功/失败回调函数,都会调用吗? 当promise改变为对应状态时都会调用 3. 改变promise状态和指定回调函数谁先谁后? (1)都有可能,正常情况下是先指定回调再改变状态,但也可以先改变状态再指定回调 (2)如何先改变状态再指定回调? ①在执行器中直接调用resolve() / reject() ②延迟更长时间才调用then() (3)什么时候才能得到数据? ①如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据 ②如果先改变的状态,那当指定回调时,回到函数就会调用,得到数据 4. promise.then()返回的新的promise的结果状态由什么决定? (1)简单表达:由then()指定的回调函数执行的结果决定 (2)详细表达: 如果抛出异常,新的proimise变为rejected,reason为抛出的异常 如果返回的是非promise的任意值,新promise变为resolved, value为返回的值 如果返回的是另一个新的promise,此promise的结果就会成为新的promise的结果 5. promise如何串联多个操作任务? (1)promise的then() 返回一个新的promise,可以看成then()的链式调用 (2)通过then的链式调用串联多个同步/异步任务 6. promise异常传透? (1)当使用promise的then链式调用时,可以再最后指定失败的回调 (2)面前任务操作出了异常,都会传到最后失败的回调中处理 7. 中断promise链 (1)当使用promise的then链式调用时,在中间中断,不再调用后面的回调函数 (2)办法:在回调函数中返回一个pendding状态的promise对象 ### **自定义Promise** 1. 定义整体结构 2. Promise构造函数的实现 3. promise.then() /catch()的实现 4. Promise.resolve() / reject() 的实现 //产生立即成功 立即失败的结果的 5. Promise.all / race()的实现 6. Promise.resolveDelay() / rejectDelay() //自定义延迟时间成功或失败的内容 7. ES5 fucntion版本 8. ES6 class版本 * [ 1. 基础架子出来 ] ![](https://img.kancloud.cn/30/97/3097aee815cb61efe70e92406d067814_779x1003.png) ***** ***** ***** ***** ## async和await [async函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function) [await表达式](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await) 1. async函数 函数的返回值为promise对象 promise对象的结果由async函数执行的返回值觉得 2. await表达式 await右侧的表达式一般为promise对象,但也可以是其他的值 如果表达式是promise对象,await返回的是promise成功的值 如果表达式是其他的值,直接将此值作为await的返回值 3. 注意 await必须在async函数中,但async函数中可以没有await 如果await的promise失败了,就回抛出异常,需要通过try...catch来捕获处理 ***** ## Js运行机制 ![](https://img.kancloud.cn/7f/ab/7fab624dd8b29a766417d532ffa16ccf_1080x608.png) Heap(堆)、Stack(栈)、Queue(队列)、Event Loop(事件轮询) ### 程序中的堆栈队列 **Heap(堆)** 堆, 是一种动态存储结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为**最大堆**,一种为**最小堆**,将根节点最大的堆叫做**最大堆**或**大根堆**,根节点最小的堆叫做**最小堆**或**小根堆**。堆是**线性数据结构**,相当于**一维数组**,有唯一后继。 ![](https://img.kancloud.cn/b8/d8/b8d8c5a4a327172e04d609c06ac02695_190x161.png) **栈(Stack)** 栈在程序中的设定是限定仅在表尾进行插入或删除操作的线性表。栈是一种数据结构,它按照**后进先出**(`LIFO: last-in-first-out`)的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈是只能在某一端插入和删除的特殊线性表。 ![](https://img.kancloud.cn/f2/1b/f21ba8516dc914eb50a3eb4c68beb8ba_616x282.png) **队列(Queue**) 队列特殊之处在于它只允许在表的前端(`front`)进行删除操作,而在表的后端(`rear`)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为**空队列**。 队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为**先进先出**(`FIFO: first-in-first-out`) ![](https://img.kancloud.cn/da/cf/dacfe4afd540fba44ca1f521951cfdc2_554x270.png) ### js中的堆栈队列 下面我解释下**JavaScript语言**中的堆、栈、队列。 **堆** 堆, 动态分配的内存,大小不定也不会自动释放,存放**引用类型**,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。可以简单理解为存储代码块。 堆的作用:存储引用类型值的数据 ``` let obj = { name: '北歌',     puslic: '前端自学驿站' } let func = () => { console.log('hello world') } ``` ![](https://img.kancloud.cn/a2/f8/a2f898aefcac314a81035572ba1458db_556x375.png)**栈** js中的栈准确来将应该叫调用栈(EC Stack),会自动分配内存空间,会自动释放,存放**基本类型**,简单的数据段,占据固定大小的空间。 栈的作用:存储基本类型值,还有一个很要的作用。**提供代码执行的环境** **队列** js中的队列可以叫做**任务队列**或**异步队列**,任务队列里存放各种异步操作所注册的回调,里面分为两种任务类型,宏任务(`macroTask`)和微任务(`microTask`)。 好,下面可以回到正题上来了。 ### 为什么会出现Event Loop 总所周知JS是一门单线程的非阻塞脚本语言,Event Loop就是为了解决JS异步编程的一种解决方案。 ![](https://img.kancloud.cn/fc/5b/fc5bb594f39a22b47bb8d083c010b17f_160x153.png) ### JS为什么是单线程语言,那它是怎么实现异步编程(非阻塞)运行的 第一个问题:JavaScript的诞生就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等),  设计成单线程的原因就是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果(两个线程修改了同一个DOM节点就会产生不必要的麻烦),这对于一种网页脚本语言来说这就太复杂了。 第二个问题:JavaScript是单线程的但它所运行的宿主环境—浏览器是多线程,浏览器提供了各种线程供Event Loop调度来协调JS单线程运行时不会阻塞。 >代码执行开启一个全局调用栈(主栈)提供代码运行的环境,在执行过程中同步任务的代码立即执行,遇到异步任务将异步的回调注册到任务队列中,等待同步代码执行完毕查看异步是否完成,如果完成将当前异步任务的回调拿到主栈中执行 ****** ## 进程和线程 进程:进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位) 线程:线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位) 对于进程和线程并没有确切统一的描述,可以简单的理解 >比如一个应用程序: 如QQ、浏览器启动时会开启一个进程,而该进程可以有多个线程来进行资源调度和分配,达到运行程序的作用。 更通俗的话讲:打开QQ应用程序开启了进程来运行程序(QQ), 有多个线程来进行资源调度和分配(多个线程来分配打开QQ所占用的运存),达到运行程序(QQ)的作用. 用操作系统来作个例子: ![](https://img.kancloud.cn/3f/ab/3fab72d40b6bd0dba629747477d76656_991x581.png) >线程依赖进程,一个进程可以有一个或者多个线程,但是线程只能是属于一个进程。 ### JS的单线程 js的单线程指的是javaScript引擎只有一个线程 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。js 引擎执行异步代码而不用等待,是因有为有任务队列和事件轮询。 * 任务队列:任务队列是一个先进先出的队列,它里面存放着各种任务回调。 * 事件轮询:事件轮询是指主线程重复从任务队列中取任务、执行任务的过程。 ### 浏览器的多线程 1. GUI 渲染线程 * 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等 * 页面重绘和回流 * 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新 3. JS 引擎线程 * 负责 JS 脚本代码的执行 * 负责准执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件 * 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染 5. 事件触发线程 * 负责将准备好的事件交给 JS 引擎线程执行 * 多个事件加入任务队列的时候需要排队等待(JS 的单线程) 7. 定时器触发线程 * 负责执行异步的定时器类的事件,如 setTimeout、setInterval * 定时器到时间之后把注册的回调加到任务队列的队尾 9. HTTP 请求线程 * 负责执行异步请求 * 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列的队尾等待执行 ****** ## Event Loop >事件轮询就是解决javaScript单线程对于异步操作的一些缺陷,让 javaScript做到既是**单线程**,又绝对**不会阻塞**的核心机制,是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。 ### 浏览器中的Eveent Loop执行顺序 Processing model\[1\]规范定义了`Eveent Loop`的循环过程: 一个Eveent Loop只要存在,就会不断执行下边的步骤: * 1.在tasks(任务)队列中选择最老的一个task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到下边的microtasks步骤。 * 2.将上边选择的task设置为正在运行的task\[2\]。 * 3.Run: 运行被选择的task。 * 4.将Eveent Loop的currently running task\[3\]变为null。 * 5.从task队列里移除前边运行的task。 * 6.Microtasks: 执行microtasks任务检查点\[4\]。(也就是执行microtasks队列里的任务) * 7.更新渲染(Update the rendering):可以简单理解为浏览器渲染... * 8.如果这是一个worker event loop,但是没有任务在task队列中,并且WorkerGlobalScope\[5\]对象的closing标识为true,则销毁Eveent Loop,中止这些步骤,然后进行定义在Web workers\[6\]章节的run a worker\[7\]。 * 9.返回到第一步。 Eveent Loopp会不断循环上面的步骤,概括说来: * `Eveent Loop`会不断循环的去取`tasks`队列的中最老的一个task(可以理解为宏任务)推入栈中执行,并在当次循环里依次执行并清空`microtask`队列里的任务。 * 执行完`microtask`队列里的任务,有**可能**会渲染更新。(浏览器很聪明,在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ(大约16.7ms每帧)的频率更新视图) ### 宏任务和微任务优先问题 >在任务对列(queue)中注册的异步回调又分为两种类型,宏任务和微任务。我们为了方便理解可以认为在任务队列中有宏任务队列和微任务队列。宏任务队列有多个,微任务只有一个 * 宏任务(macro Task) * script(整体代码) * setTimeout/setInterval * setImmediate(Node环境) * UI 渲染 * requestAnimationFrame * .... * 微任务(micro Task) * Promise的then()、catch()、finally()里面的回调 * process.nextTick(Node 环境) * ... > 个人理解的执行顺序: 1. 代码从开始执行调用一个全局执行栈,script标签作为宏任务执行 2. 执行过程中同步代码立即执行,异步代码放到任务队列中,任务队列存放有两种类型的异步任务,宏任务队列,微任务队列。 3. 同步代码执行完毕也就意味着第一个宏任务执行完毕(script) * 1、先查看任务队列中的微任务队列是否存在宏任务执行过程中所产生的微任务 1-1、有的话就将微任务队列中的所有微任务清空 2-2、微任务执行过程中所产生的微任务放到微任务队列中,在此次执行中一并清空 * 2、如果没有再看看宏任务队列中有没有宏任务,有的话执行,没有的话事件轮询第一波结束 2-1、执行过程中所产生的微任务放到微任务队列 2-2、完成宏任务之后执行清空微任务队列的代码 ![](https://img.kancloud.cn/d1/fc/d1fce889c2f4a2ee1d22efd18932d53d_1080x889.png) >所以是宏任务优先,在宏任务执行完毕之后才会来一次性清空任务队列中的所有微任务。 ***** ## 宏队列和微队列 ![](https://img.kancloud.cn/2a/14/2a1466f36f359a3da633f503a186091a_505x300.png) 图片中的mutation 监视dom 标签属性的改变 [mutation](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver/MutationObserver) js是单线程执行的 先执行完同步代码,再执行队列中的 1. js中用来存储待执行回调函数的队列包含2个不同特定的队列 2. 宏队列:用来保存执行的宏任务(回调), 比如:定时器回调/DOM时间回调/ajax回调 3. 微队列:用来保存执行的微任务(回调),比如:promise以及MutationObserver回调 4. js执行时,会区别2个队列 (1)js引擎首先必须先执行所有的初始化同步任务代码 (2)每次准备取出第一个宏任务前,都要将所有的微任务一个一个取出来执行 1.遇见await左侧先执行,右侧时候要跳出函数,等外部任务执行完再跳回 2.awati可以看成.then是一个微任务,放入微任务队列,和其他微任务一样,顺序执行 3.执行微任务时,产生微任务顺序放入当前微任务队列,顺序执行 4.遇见函数前面有async,awiat返回时,放入到Promise.then微任务队列中,然后执行微任务队列,如果前面没有async,await返回时候直接执行,下次遇见await时候,再跳出执行微任务队列 1.微任务中nextTick队列在Promise.next前 ****** ## 作业 ``` console.log('script start'); setTimeout(() => { console.log('北歌'); }, 1 * 2000); Promise.resolve() .then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); async function foo() { await bar() console.log('async1 end') } foo() asyncfunction errorFunc () { try { awaitPromise.reject('error!!!')   } catch(e) { console.log(e)   } console.log('async1'); returnPromise.resolve('async1 success') } errorFunc().then(res =>console.log(res)) function bar() { console.log('async2 end')  } console.log('script end'); ```