JS面试题

DOM事件和event loop

  • 都是回调函数,但是触发的方式和时机不同,一个是设置固定时间,一个是用户点击后触发

  • JS是单线程的

  • 异步(setTimeout(定时),ajax(网络请求返回时)等)使用回调,基于event loop

  • DOM(事件触发时,如点击,鼠标滚动,键盘事件) 事件也是基于回调,基于event loop,但dom事件不是异步执行细节:当代码运行到$('#btn1').click()(这些代码浏览器立刻执行的),同样会把回到函数function(e){log("")}它放到webAPIs里,等到同步代码执行完毕,尝试渲染dom(Event Loop开始轮询),用户触发点击事件时,会把回调函数(function(e){log("")})放到callback Queue中,然后Event Loop再把回调函数放到调用栈callStack中 执行

Promise有哪三种状态

  • 三种状态

    • pending resolved rejected
    • pending=>resolved 或 pending=>rejected
    • 变化不可逆
  • 状态的表现和变化

    • pending状态promise,不会触发then和catch

    • resolved状态promise,会触发后续的then回调函数

    • rejected 状态promise,会触发后续的catch回调函数

      const p1 = new Promise((resolve,reject)=>{
      
      })
      console.log('p1',p1);//pending
      const p2 = new Promise((resolve,reject)=>{
          setTimeout(()=>{
              resolve()
          })
      })
      console.log('p2',p2);//pending 一开始打印时
      setTimeout(()=>console.log('p2-setTimeout',p2))//resolved
      
      
      const p3 = new Promise((resolve,reject)=>{
          setTimeout(()=>{
              reject()
          })
      })
      
      console.log('p3',p3);//pending 一开始打印时
      setTimeout(()=>console.log('p3-setTimeout',p3))//rejected
      
      const p1 = Promise.resolve(100)//返回resolved状态的promise
      console.log('p1',p1);
      p1.then(data=>{
          console.log('data',data);
      }).catch(err=>{
          console.error('err',err);
      })
      
      const p2 = Promise.reject('err')//rejected
      // console.log('p2',p2);
      p2.then(data=>{
          console.log('data2',data);
      }).catch(err=>{
          console.error('err2',err);
      })
      
  • then和catch对状态的影响

    • ​ thencatch改变状态
      • then没有报错返回resolved状态的promise,接着会触发后边的then回调,里面有报错则返回rejected状态的promise,
        会触发后边的catch回调
      • catch没有报错返回resolved状态的promise,接着会触发后边的then回调,里面有报错则返回rejected状态的promise,
        会触发后边的catch回调

总结

  • 三种状态,状态的表现和变化
  • thencatch对状态的影响(重要)
  • thencatch的链式调用(常考)

async/await

  • 异步回调callback hell
  • Promise then catch 链式调用,但是也是基于回调函数
  • Async/await 是同步语法,彻底消灭回调函数

async/await和Promise的关系

  • async/await是消灭异步回调的终极武器

  • 但是和Promise并不互斥

  • 反而两者相辅相成

  • 执行async函数,反回的是Promise对象

  • await相当于Promisethen

  • try...catch可捕获异常,代替了Promisecatch

    async function fn1() {
        // return 100;//封装成Promise对象返回  相当于return Promise.resolve(100)
        return Promise.resolve(200);
    };
    // const res1 = fn1(); //执行async函数,返回的是一个Promise对象
    // console.log('res1', res1);
    // res1.then(data => {
    //     console.log('data', data);
    // })
    
    
    // //await 后边跟promise
    // (async function(){
    //     const p1 = Promise.resolve(300);
    //     const data = await p1;//如果await后边跟的是promise,那么await 相当于 Promise 的then
    //     console.log('data',data);
    // })();
    
    // //await 后边跟数值
    // (async function(){
    //     const data1 = await 400;//如果await后边跟的是非Promise的值,那么会把它封装成Promise对象,相当于 await Promise.resolve(400)
    //     console.log('data1',data1);
    // })();
    
    // //await 后边跟async函数
    // (async function(){
    //   const data2 = await fn1();
    //   console.log('data2',data2);
    // })();
    
    
    
    // (async function(){
    //     const p4 = Promise.reject('err1');//rejected
    //     try {
    //         const res = await p4;
    //         console.log(res);
    //     } catch (ex) {
    //         console.error(ex);//try...catch 相当于promise的catch
    //     }
    // })()
    
    
    (async function(){
        const p4 = Promise.reject('err1');//rejected
        const res = await p4;
      //await 相当于 then ,但是这里的p4是rejected状态的promise所以不会执行到await,这种情况就要用到上边的try...catch
        console.log('res',res);
    
    })()
    

异步的本质

  • async/await是消灭异步回调的终极武器

  • JS还是单线程,还得是有异步,还得是基于event loop

  • async/await 只是语法糖,但是这颗糖真香

    //面试题
    async function async1(){
        console.log('async1 start');
        await async2(); //undefined
        //await的后面,都可以看做是callback里的内容,即异步,所以暂时不会打印。函数体执行完,接着会执行后边的script end,此时同步代码迎执行完,最后打印saync1 end
        //类似,event loop ,setTimeout(cb1)
        //setTimeout(function(){console.log('async1 end')})或者Promise.resolve().then(()=>{console.log('async1 end')})
        
        console.log('async1 end');
        await async3();
        console.log('async1 end 2');
    }
    async function async2(){
        console.log('async2');
    }
    
    async function async3(){
        console.log('async3');
    }
    
    
    console.log('script start');
    async1();
    console.log('script end');
    
    //1,script start   2.async1 start    3.async2   4.script end  5.async1 end    6,async3  7async1 end 2
    

for..of

  • for...in(以及forEach for)是常规的同步遍历
  • for...of常用于异步的遍历
function muti(num){
    return new Promise(resolve=>{
        setTimeout(()=>{
            resolve(num*num)
        },1000)
    })
}


const nums = [1,2,3];
//同步循环
//如果使用foreach遍历处理异步代码不会等待异步代码的执行,一次输出所有异步结果
// nums.forEach(async(i)=>{
//     const res = await muti(i);
//     console.log(res);
// })

//异步循环
//果使用for-of可以使异步按先后顺序执行
//运用async/await方法,当异步函数里执行resolve时,await接收到成功的指示,然后进行下一步
(async function(){
    for(let i of nums){
        const res = await muti(i);
        console.log(res);
    }
})();

async/await总结

  • async/await 只是语法糖,但是这颗糖真香
  • async/await和Promise的关系
  • for...of的使用

宏任务macroTask和微任务microTask

  • 什么是宏任务,什么是微任务
    • 宏任务:setTimeout,setInterval,Ajax,DOM事件
      • dom渲染后触发,如setTimeout
    • 微任务:Promise async/await
      • dom渲染前触发,如promise
    • 微任务执行时机比宏任务要早(记住)
  • event loop 和 DOM渲染
  • 宏任务和微任务的区别

event loop 和DOM渲染

  • 再次回顾一遍eventloop的过程
  • JS是单线程的,而且和DOM渲染共用一个线程
  • JS执行的时候,得留一些时机提供DOM渲染

回顾eventloop过程(增加dom渲染时机)

当同步代码执行完毕时,callstack会处于空闲状态(空的),这是浏览器会尝试dom渲染(如果代码中有操作),然后再第一次启动eventloop,如果callback queue中有回调函数,那么会把它放到callstack中去执行,执行完毕再尝试dom渲染,再启动eventloop.所以,每次eventloop轮询或者callstack执行完毕后都会尝试dom渲染

  • 每次Call Stack清空(即每次轮询结束),即同步任务完成

  • 都是dom重新渲染的机会,dom结构如有改变(修改dom的代码)则重新渲染

  • 然后再去触发下一次eventloop

    //证实callstack结束 之后会渲染dom
    const $p1 = $('<p>一段文字</p>');
    const $p2 = $('<p>一段文字</p>');
    const $p3 = $('<p>一段文字</p>');
    $('#container').append($p1).append($p2).append($p3);
    console.log('length',$('#container').children().length);
    alert('本次 call stack 结束,dom 结构已更新,但是尚未触发渲染');
    //alert 会阻断js执行,也会阻断dom渲染,便于查看效果
    
    //证实微任务是dom渲染前触发的,而宏任务是在dom渲染后触发的
    const $p1 = $('<p>一段文字</p>');
    const $p2 = $('<p>一段文字</p>');
    const $p3 = $('<p>一段文字</p>');
    $('#container').append($p1).append($p2).append($p3);
    
    //微任务:dom渲染前触发
    Promise.resolve().then(()=>{
        console.log('length1',$('#container').children().length);//3
        alert('Promise then');//dom渲染了吗?  --no
    })
    //宏任务:dom渲染后触发
    setTimeout(() => {
        console.log('length2',$('#container').children().length);//3
        alert('setTimeout');//dom渲染了吗?   --yes
    });
    

    从eventloop角度解释,为什么微任务执行的时机更早(根本区别) 

  • 微任务是ES6语法规定的

  • 宏任务是由浏览器规定的

  • Micro tast queue:微任务队列

  • callstack中执行到微任务代码时,会等待时机,然后直接把它放到微任务队列(Micro tast queue)

  • callstack清空之后->执行微任务->dom渲染->触发eventloop->把回调函数放到callstack执行宏任务

    //微任务宏任务面试真题
    console.log(100)
    setTimeout(() => {
        console.log(200)
    })
    Promise.resolve().then(() => {
        console.log(300)
    })
    console.log(400)
    // 100 400 300 200