企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### 并发(concurrency)和并行(parallelism)区别 **并发与并行的区别?** * 并发是宏观概念,我分别有任务A和任务B,在一段时间内通过任务间的切换完成了这两个任务。 * 并行是微观概念,假设CPU中存在两个核心,那么我就可以同时完成任务A、B。同时完成多个任务的情况称之为并行。 ### 回调函数(Callback) **什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?** ~~~ ajax(url,()=>{ //处理逻辑}) ~~~ 回调函数有个致命的弱点:容易写出回调地狱(Callback hell)。不利于阅读和维护。 ~~~ ajax(url, ()=>{ //处理逻辑 ajax(url1,()=>{ //处理逻辑 ajax(url2,()=>{ //处理逻辑 }) }) }) ~~~ 回调地狱的根本问题: 1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发动全身 2. 嵌套函数一多,就很难处理错误 回调函数还有其他缺点:不能使用try catch捕获错误,不能直接return。 ### Generator **你理解的Generator是什么?** Generator(生成器),ES6新特性。通过 `function*`来定义的函数称之为“生成器函数”,它的特点是可以中断函数的执行,每次执行yield语句之后,函数即暂停执行,直到调用返回的生成器对象的`next()`函数它才会继续执行。 Generator函数是一个状态机,封装了多个内部状态。执行Generator函数返回一个遍历器对象(一个指向内部状态的指针对象),调用遍历器对象的next方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,知道遇到下一个yield表达式(或return) ~~~ function *foo(x){ let y = 2 * (yield(x+1)); let z = yield( y/3); return ( x + y + z); } let it = foo(5); console.log(it.next());//{value:6,done:false} console.log(it.next(12)) //{value:8,done:false} console.log(it.next(13)) //{value: ,done:true} //13 +28 + 9 ~~~ 分析: 1. Generator函数调用和普通函数不同,它返回一个迭代器 2. 当执行第一次next时,传参会被忽略,并且函数暂停在`yield(x+1)`处,所以返回6; 3. 当执行第二次next时,**传入参数等于上一个yield的返回值**,**如果你不传参,yield永远返回undefined**。此时 `let y = 2* 12`,所以第二个yield等于 `2 *12 /3 = 8` 4. 当执行第三次next时,传入的参数会传递给`z`,所以 `z=13,x = 5, y=24`, Generator函数一般见到的不多,其实也于他有点绕的关系,并且一般会配合co库去使用。可以通过Generator函数解决回调地狱的问题。 ~~~ function *fetch(){ yield ajax(url, ()=>{}) yield ajax(url1, ()=>{}) yield ajax(url2,()=>{}) } let it = fetch(); let result1 = it.next(); let result2 = it.next(); let result3 = it.next(); ~~~ ### Promise Promise的特点是什么?优缺点?什么是Promise链?Promise构造函数执行和then函数执行有什么区别? Promise:承诺,在未来有一个确切的答复,并且该承诺有三种状态: * 等待中(pending) * 完成了(resolved) * 拒绝了(rejected) 从等待状态变成其他状态永远不能改变状态。 1. 当我们在构造`Promise`的时候,**构造函数内部的代码是立即执行的** ~~~ new Promise( (resolve,reject)=>{ console.log('new Promise') resolve('success') } ) console.log('finish') //输出:new Promise fininsh ~~~ 2. Promise实现了链式调用,每次调用then之后返回的都是一个Promise,并且是一个全新的Promise(原因是因为状态不可变) 3. 如果在then中使用了return,那么return的值会被Promise.resolve()包装 ~~~ Promise.resolve(1) .then( res => { consoel.log(res); return 2})//包装成Promise.resolve(2) .then(res =>{ console.log(res)} ) //2 ~~~ 4. Promise可以很好的解决了回调地狱的问题 ~~~ ajax(url) .then( res =>{ console.log(res); return ajax(url1) }) .then( res =>{ console.log(res); return ajax(url2) }) .then(res =>{ console.log(res)}) ~~~ 5. 缺点:无法取消Promise,**错误需要通过回调函数捕获**。?? ### async及await **async及await特点,优缺点?await原理?** 1. 一个函数如果加上async,那么该函数就会返回一个Promise ~~~ async function test(){ return "1" } console.log(test()) //Promise对象 test(),不是test ~~~ 2. async就是讲函数返回值使用Promise.resolve()包裹了下,和then中处理返回值一样,并且await只能配套async使用 ~~~ async function test(){ let value = await sleep() } ~~~ 3. async和await可以说是异步终极解决方案了, * 相比于Promise,优势在于处理then的调用链,能够清晰准确的写出代码,一堆then也不好 * 能优雅的解决回调地狱问题 * 缺点:await将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await,会导致性能上的降低。 ~~~ async function test() { // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式 // 如果有依赖性的话,其实就是解决回调地狱的例子了 await fetch(url) await fetch(url1) await fetch(url2) } ~~~ ~~~ let a = 0; let b = async() =>{ a = a + await 10 console.log("2",a) //2 11 } b(); //b是异步 a++;//a++是同步 console.log("1",a) // 1 1 ~~~ 解析: * 首先函数b先执行,在执行到`await 10`之前变量a还是0,因为**await内部实行了generator**,**generator会保留堆栈中东西,所以a = 0被保存下来**。 * 因为await是异步操作,后来的表达式不返回Promise的话,就会包装成Promise.reslove(返回值),然后去执行函数外的同步代码。 * 同步代码执行完毕或开始执行异步代码,将保存下来的值拿出来使用 0+10 await内部实行了generator,其实await就是generator加上Promise的语法糖,且内部实现了自动执行generator。 ### 常用定时器函数 **setTimeout、setInterval、requestAnimation各有什么特点?** 错误观点:setTimeout是延时多久,那就应该多久后执行 因为js是单线程执行的,如果前面的代码影响了性能,就会导致**setTimeout不会按期执行**。 我们可以通过代码去修正setTimeout,从而使定时器相对准确 ~~~ let period = 60 * 1000 * 60 * 2 let startTime = new Date().getTime() let count = 0 let end = new Date().getTime() + period let interval = 1000 let currentInterval = interval function loop() { count++ // 代码执行所消耗的时间 let offset = new Date().getTime() - (startTime + count * interval); let diff = end - new Date().getTime() let h = Math.floor(diff / (60 * 1000 * 60)) let hdiff = diff % (60 * 1000 * 60) let m = Math.floor(hdiff / (60 * 1000)) let mdiff = hdiff % (60 * 1000) let s = mdiff / (1000) let sCeil = Math.ceil(s) let sFloor = Math.floor(s) // 得到下一次循环所消耗的时间 currentInterval = interval - offset console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) setTimeout(loop, currentInterval) } setTimeout(loop, currentInterval) ~~~ setInterval,和setTimeout基本一致,每个一段时间执行一次回调函数 通常不建议使用setInterval:和setTimeout一样,不能保证在预期的时间执行任务;存在执行累积的问题 ~~~ function demo(){ setInterval(function(){ console.log(2) },1000); sleep(2000) } ~~~ 以上代码在浏览器中,如果定时器执行过程中出现耗时操作,**多个回调会在耗时操作结束以后同时执行**,这样可能就会带来性能上的问题。 **requestAnimationFrame**:是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是**按帧对网页进行重绘**。 目的:为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画,WebGLobal动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。 requestAnimationFrame的优势:充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz) requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行网页重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。 requestAnimationFrame是在主线程上完成。如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。 如果你有循环定时器的需求,其实完全可以通过requestAnimationFrame来实现 ~~~ function setInterval(callback, interval){ let timer; const now = Date.now; let startTime = now(); let endTime = startTime const loop = () =>{ timer = window.requestAnimationFrame(loop); endTime = now(); if( endTime - startTime >= interval){ startTime = endTime = now(); callback(timer); } } timer = window.requestAnimationFrame(loop) return timer } let a = 0; setInterval(timer =>{ console.log(1) a++; if(a ===3) cancelAnimationFrame(timer); },1000) ~~~ cancelAnimationFrame:用于取消重绘 ~~~ window.cancelAnimationFrame(requestID) //它的参数是requestAnimationFrame返回的一个代表任务ID的整数值。 ~~~ requestAnimationFrame:自带函数节流功能,基本可以保证在16.6毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题。 ### 手写Promise(*****) 手写一个符合Promise/A+规范的Promise来深入理解它, #### 简易版的Promise 1. 搭建构建函数的大体框架 ~~~ const PENDING ="pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; function MyPromise(fn){ const that = this; that.state = PENDING; that.value = null; that.resolvedCallbacks =[]; that.rejectedCallbacks = []; //待完善resolve和reject函数 //待完善执行fn函数 } ~~~ 分析: * 手写创建三个常量表示状态(便于开发和维护) * 创建that常量,因为代码异步执行,用于获取正确的this对象 * 一开始Promise的状态是pending * value变量用于报错resolve或者reject传入的值 * resolveCallbacks 和rejectedCallbacks用于保存then中的回调,因为当执行完Promise时状态可能还是等待中,这时候应该把then中的回调保存起来,用于状态改变时使用。 2. 接下来完善resolve和reject函数,添加在MyPromise函数内部 ~~~ function resolve(value){ if(that.state === PENDING){ that.state = RESOLVED; that.value = value; that.resolveCallbacks.map(cb=> cb( that.vale)); } } function reject(value){ if(that.state == PENDING){ that.state = REJECTED; that.value = value; that.rejectedCallbacks.map(cb=>cb(that.value)) } } ~~~ 解析: * 两个函数**都得判断当前状态是否为等待中**,因为规范规定只有等待态才可以改变状态。 * 将当前状态更改为对应状态,并且将传入的值给value * 遍历回到组并执行。 3. 完成以上两个函数后,我们就该实现**如何执行Promise中传入的函数**了 ~~~ try{ fn(resolve, reject); }catch(e){ reject(e); } ~~~ 解析: * 执行传入的参数,并且将之前的两个函数当做参数穿进去 * 注意,可能执行函数过程中会遇到错误,需要捕获错误并且执行reject函数 4. 实现较为复杂的then函数 ~~~ MyPromise.prototype.then = function(onFulfilled, onRejected){ const that = this; onFulfilled = typeof onFulfilled ==='function' ? onFulfilled :v =>v onRejected = typeof onRejected === 'function' ? onRejected:r=>{throw r}; if( that.state === PENDING){ that.resolvedCallbacks.push(onFulfilled); that.rejectedCallbacks.push(onRejected); } if( that.state === RESOLVED){ onFulfilled(that,value) } if(that.state === REJECTED){ onRejected(that.value) } } ~~~ * 首先判断两个参数是否为函数类型,因为两个参数是可选参数 * 当参数不是函数类型时,需要创建一个函数赋值给对应的参数,同时也实现了透传 ~~~ //该代码目前在简单版中会报错 //只是作为一个透传的例子 Promise.resolve(4).then().then((value)=>{console.log(value)}) ~~~ * 接下来是一系列判断状态的逻辑,当状态不是等待态是,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中push函数,比如如下代码就会进入等待态的逻辑 ~~~ new MyPromise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },0) }).then(value =>{console.log(value)}) ~~~ ~~~ const PENDING ="pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; function MyPromise(fn){ const that = this; that.state = PENDING; that.value = null; that.resolvedCallbacks =[]; that.rejectedCallbacks = []; //待完善resolve和reject函数 function resolve(value){ if(that.state === PENDING){ that.state = RESOLVED; that.value = value; that.resolvedCallbacks.map(cb=> cb( that.value)); } } function reject(value){ if(that.state == PENDING){ that.state = REJECTED; that.value = value; that.rejectedCallbacks.map(cb=>cb(that.value)) } } //待完善执行fn函数 try{ //将resolve和reject暴露出来 fn(resolve, reject); }catch(e){ reject(e); } } MyPromise.prototype.then = function(onFulfilled, onRejected){ const that = this; onFulfilled = typeof onFulfilled ==='function' ? onFulfilled :v =>v onRejected = typeof onRejected === 'function' ? onRejected:r=>{throw r}; if( that.state === PENDING){ that.resolvedCallbacks.push(onFulfilled); that.rejectedCallbacks.push(onRejected); } if( that.state === RESOLVED){ onFulfilled(that,value) } if(that.state === REJECTED){ onRejected(that.value) } } new MyPromise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },0) }).then(value =>{console.log(value)}) ~~~ 目前不支持链式操作 ### 实现一个符合Promise/A+规范的Promise 待复习 ## Event loop ### 进程与线程 **进程和线程的区别?JS单线程带来的好处?** 两个名词都是CPU工作时间片的一个描述 **进程**:描述了**CPU在运行指令及加载和保存上下文所需的时间**,放在应用上来说就代表了一个程序。 **线程**:是进程中的更小单位,描述了执行一段指令所需的时间。 浏览器中:当你打开一个Tab,创建了一个进程,一个进程可是有多个线程,比如渲染线程、js引擎线程、HTTP请求线程等。当发起一个请求,就是创建一个线程,请求结束后,该线程可能就会被销毁 JS引擎线程和渲染线程是**互斥**的:JS运行的时候可能会阻止UI渲染(JS可以修改DOM,如果在JS执行的时候UI线程还在工作,可能导致不能按期的渲染UI)(单线程好处) 单线程好处:达到节省内存,节约上下文切换时间,没有锁的问题的好处。 ### 执行栈 可以认为是一个存储**函数调用**的**栈结构**,遵循先进后出的原则,后执行的函数会先弹出栈。 当我们使用递归的时候,因为站可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题 ~~~ function bar(){ bar() } bar() ~~~ ### 浏览器中的Event Loop **异步代码执行顺序?什么是Event Loop?** 执行栈:当我们执行JS代码的时候,其实就是执行栈中放入函数。当遇到异步函数的代码时,**会被挂起并在需要执行的时候加入到Task(多种Task)队列中**,一旦执行栈为空,**Event Loop就从Task队列中拿出需要执行的代码并放入执行栈中执行**。从本质上来说,JS中的异步还是同步行为。 不同的任务源会被分配到不同的Task队列中,任务源可以分为**微任务(microtask)和宏任务(macrotask)**,ES6规范中,microtask成为jobs,macrotask成为task。 ~~~ console.log(' script start'); async function async1(){ await async2(); console.log('async1 end'); } async function async2(){ console.log("async2 end") } async1(); setTimeout(function(){ console.log('setTimeout') }) new Promise(resolve=>{ console.log('promise') resolve(); }) .then(function(){ console.log('promise1') }) .then(function(){ console.log('promise2') }) console.log('script end') // script start --> async2 end --> promise -->script end -->async1 end -->promise1-->promise2 -->setTimeout(新浏览器) //script start --> async2 end --> promise -->script end -->promise1-->promise2 -->async1 end -->setTimeout(旧浏览器) ~~~ async和await:调用async1函数时,会马上输出async2 end,并且函数返回一个Promise,接下来会**遇到await**的时候就**让出线程**开始**执行async1外的代码**,所以,我们完全可以把**await看成是让出线程的标志**。 接下来执行所以同步代码,执行完毕后, 去执行所以的异步代码,又回到await的位置执行返回的Promise的resolve函数,这又会吧resolve丢到微任务队列中,接下来去执行then中的回调,当两个then中的回调全部执行完毕以后,又会回到await的位置处理返回值,这个时候可以看成是`Promise.resolve(返回值).then()`,然后**await后的代码全部被包裹进了then的回到中** ~~~ async function async1(){ await async2(); console.log('async1 end'); } async function async2(){ console.log("async2 end") } ~~~ 等价于 ~~~ new Promise((resolve,reject)=>{ console.log('async2 end') //Promise.resolve()将代码插入微任务队列尾部 //resolve再次插入微任务队列尾部 resolve(Promise.resolve()) }).then(()=>{ console.log('async1 end') }) ~~~ 也就是说,如果await后面跟着Promise的话,async1 end需要等待三个tick才能执行到,性能还是略慢的,V8团队借鉴了Node 8中的一个bug,在引擎底层将三个tick减少到二次tick。 **Event Loop执行顺序:** 1. 首先执行同步代码,属于宏任务 2. 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行 3. 执行所以微任务 4. 当执行完所有微任务后,如有必要渲染页面 5. 然后开始下一轮Event Loop,执行宏任务中的异步代码,也就是setTimeout中的回调函数。 **微任务:process.nextTick,promise,MutationObserver 宏任务:script,setTimeout,setInterval,setImmediate,I/O,UI rendering**。 错误观点:微任务快于宏任务(宏任务包括了script,浏览器会先执行一个宏任务,接下来异步代码的话才回先执行微任务) ![](https://box.kancloud.cn/13d8b4237236276b77645ac1ad4d9b5a_696x332.png) ## Node中的Event loop 和浏览器完全不同的东西