一名【合格】前端工程师的自检清单之js执行机制

Author Avatar
w4ctech 7月6日
  • 在其它设备中阅读本文章

最后更新于2019年07月06日; 如遇到问题,请留言及时通知站长

  1. 为何try里面放returnfinally还会执行,理解其内部机制

    • try-catch语句,作为 JavaScript 中处理异常的一种标准方式,我们会把所有可能会抛出错误的代码都放在 try语句块中,而把那些用于错误处理的代码放在 catch 块中。
    • 如果 try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行 catch 块。此时,catch 块会接收到一个包含错误信息的对象。与在其他语言中不同的是,即使你不想使用这个错误 对象,也要给它起个名字。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误 消息的 message 属性。
    • 虽然在 try-catch 语句中是可选的,但 finally 子句一经使用,其代码无论如何都会执行。换句话说,try 语句块中的代码全部正常执行,finally 子句会执行; 如果因为出错而执行了 catch 语句块,finally 子句照样还会执行。只要代码中包含 finally 子句,则无论 trycatch 语句块中包含什么代码——甚至是return语句,finally 子句照样还会执行。
  2. JavaScript如何实现异步编程,可以详细描述EventLoop机制

    • 异步编程的四种模式:

      1. 回调函数

        • 把函数当作参数,在程序中执行,将耗时的操作推迟执行。
      2. 事件监听

        • 任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
      3. 发布 / 订阅模式

        • 向“信号中心”订阅,当任务执行完成的时候,就向“信号中心”发布一个信号。这种方法的性质与 "事件监听" 类似,但是明显优于后者,可以监控程序的运行。
      4. Promises对象

        • Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。无论内部如何实现,这四种模式是目前常用的。
      5. EventLoop机制

        • 事件循环机制也是对 js 单线程的一个补充,他区别与异步编程。当程序中主线程的任务执行完之后,会一直循环查看存放在任务队列里的事件任务是否被触发。
        由于环境不同,`EventLoop`机制也不同,在`Node`中,`Node.js`还提供了另外两个与"任务队列"有关的方法:`process.nextTick`和`setImmediate`。

            1. `process.nextTick`方法可以在当前"执行栈"的尾部----下一次`Event Loop`(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。`setImmediate`方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次`Event Loop`时执行,这与`setTimeout(fn, 0)`很像。
            阮一峰的网络日志
  1. 宏任务和微任务分别有哪些

    1. 宏任务

      - 当前调用栈中执行的代码成为宏任务。
      
    2. 优先级

      - `主代码块` > `setImmediate` > `MessageChannel` > `setTimeout` / `setInterval`。
      
    3. 微任务

      - 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。
      
    4. 优先级

      - `process.nextTick` > `Promise` > `MutationObserver`。
      
      - 宏任务中的事件放在`callback queue`中,由事件触发线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。
      
    5. 运行机制

      - 在执行栈中执行一个宏任务。
      - 执行过程中遇到微任务,将微任务添加到微任务队列中。
      - 当前宏任务执行完毕,立即执行微任务队列中的任务。
      - 当前微任务队列中的任务执行完毕,检查渲染,`GUI`线程接管渲染。
      - 渲染完毕后,`js`线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。
      
  2. 可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法

    var fn1=function(){
        console.log('1n')
        setTimeout(()=>{
            console.log('2n')
            var p1=new Promise(
                resolve=>{
                    console.log('3n')
                    setTimeout(()=>{
                        console.log('4n')
                        resolve()
                    },2000)
                },
                reject=>{reject()}
            )
            p1.then(()=>{console.log('5n')})
            console.log('6n')
        },1000)
        console.log('7n');
        test();
        async function test(){
            for(var i=0;i < 3;i++){
                await new Promise(res=>{
                    setTimeout(()=>{
                        console.log('i')
                        res()
                    },1000)
                })
            }
        }
    }
    fn1();
    1n
    7n
    2n
    3n
    6n
    0
    1
    4n
    5n
    2
    • 主线程

      1. `1n=>7n`;主线程执行完毕,在执行任务队列的时候,会在控制台有一个提示为`<· undefined`,这个是主线与任务队列在浏览器控制台的一个分界线。
      
    • 任务队列

      1. 第一个定时器被放在任务队列的第一位,在此环境中,`2n=>3n=>6n`,可以看作是在第一个定时器内的“主线程”,在`Promise`中,只有在`.then`的时候才会执行第一个定时器内的“任务队列”。在此“任务队列”中的,`setTimeout`控制着异步任务的执行,也就是在`2000ms`之后执行`4n`,以及`5n`。同时,他也不回阻塞主任务队列的执行。
      
      2. `async`声明异步函数,`await`被放在主线程中,而不是任务队列中,但是`await`后出现的`setTimeout`也回随着`await`进入子任务队列中,也就是说在循环中的`await`会在所等待的代码块执行完之后才会执行下一个循环的`await`。`await`会阻塞主线程的执行,如果在第一次循环中`await`后面的定时器时间过长的话,循环会一直等待,直到代码执行完进入下一次循环。所以`0=>1`执行完之后`4n=>5n=>2`。定时器会在任务队列依次执行。
      
  3. 使用 Promise 实现串行

    • promise串行:必须等到上一个promise结果才会执行下一个promise;如果中间执行失败,会终止后续执行。
    • 在项目中的应用是,接口的依赖,下一个http的请求会依赖上一个http请求的返回结果,这个在node搭建中台是比较常见的。

      let promiseUtil = (data)=>{
          return new Promise((resolve, reject)=>{
              //执行http请求,请求成功执行resolve()
          }
      }
      let p = new Promise((resolve, reject)=>{
          console.log('开始串行 Promise...');
          resolve({
          url: 'http://www.baidu.com'
          });
      })
      // 执行Promise
      p.then(promiseUtil)
      .then(promiseUtil)
      .then(promiseUtil)
    • 还有一种方法是接受一个数组:

      function iteratorPromise(arr){
          (function iter(){
              if(arr.length)arr.shift()().then(iter)
          })()
      }
      //arr是一个数组,数组里的每一个元素是promise对象。
      //直接利用 Promise.resolve()。通过循环赋值,得到最终的结果
      function iteratorPromise(arr){
          let res = Promise.resolve();
          arr.forEach(fn=>{
              res = res.then(()=>fn()) // 关键是 res=res.then... 这个逻辑
          })
      }
  4. Node与浏览器EventLoop的差异

    • 在了解Node与浏览器的事件循环之前,先看一段话:

      > 
          event loop是一个执行模型,在不同的地方有不同的实现。浏览器和nodejs基于不同的技术实现了各自的event loop。  
          首先要确定好上下文,nodejs和浏览器的event loop是两个有明确区分的事物,不能混为一谈。 
          其次,讨论一些js异步代码的执行顺序时候,要基于node的源码而不是自己的臆想。 
          简单来讲, 
          nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。 
          libuv已经对event loop作出了实现,而html5规范中只是定义了浏览器中event loop的模型,具体实现留给了浏览器厂商。
          作者 youth7---不要混淆nodejs和浏览器中的event loop
      
- 浏览器的事件循环是为浏览器服务的,`Node`的事件循环是为了让`Js`可以适应后端的开发。

- 浏览器的`EventLoop`:

    1. 事件循环在浏览器中是抽象的,浏览器的不同可能会导致差异。
    2. 在浏览器中,`Eventloop`有两类,一类是针对上下文,一类是针对`worker(web Worker)`;它的目的是为了用户交互、`UI`渲染、网络请求、用户代理等等。
    3. 一个事件循环里有很多个任务队列`(task queues)`来自不同任务源,每一个任务队列里的任务是严格按照先进先出的顺序执行的,但是不同任务队列的任务的执行顺序是不确定的。按我的理解就是,浏览器会自己调度不同任务队列。
    4. 在事件循环中,用户代理会不断从`task`队列中按顺序取`task`执行,每执行完一个`task`都会检查`microtask`队列是否为空(执行完一个`task`的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有`microtask`。然后再进入下一个循环去task队列中取下一个`task`执行...

- `Node`的`EventLoop`:

    1. `Node`中的事件循环是有一个具体的流程,它的顺序是:
    外部输入数据–>轮询阶段(`poll`)–>检查阶段(`check`)–>关闭事件回调阶段(`close callback`)–>定时器检测阶段(`timer`)–>`I/O`事件回调阶段(`I/O callbacks`)–>闲置阶段(`idle, prepare`)–>轮询阶段(按照该顺序反复运行)…

        - `timers` 阶段:这个阶段执行 `timer(setTimeout、setInterval)`的回调;
        - `I/O callbacks` 阶段:处理一些上一轮循环中的少数未执行的 `I/O `回调;
        - `idle`, `prepare` 阶段:仅 `node` 内部使用;
        - `poll` 阶段:获取新的 `I/O` 事件, 适当的条件下 `node` 将阻塞在这里;
        - `check` 阶段:执行 `setImmediate()` 的回调;
        - `close callbacks` 阶段:执行 `socket` 的 `close` 事件回调;
        - 在`Node`中还有一个有趣的东西是`process.nextTick`,这个函数其实是独立于 `Event Loop`之外的,它有一个自己的队列,当每个阶段完成后,如果存在 `nextTick` 队列,就会清空队列中的所有回调函数,并且优先于其他 `microtask` 执行。
  1. 如何在保证页面运行流畅的情况下处理海量数据

    • 分页:分页是我们最常见处理海量数据的方式之一。
    • 上拉加载:首次加载可视范围内的数据,随着滚动,加载可视范围之外的数据。
    • DocumentFragmentrequestAniminationFrame结合:以通过 DocumentFragment 的使用,减少 DOM 操作次数,降低回流对性能的影响;通过 requestAniminationFrame 在页面重绘前插入新节点。
    (function() {
    const ulContainer = document.getElementById("list-with-big-data");
    // 防御性编程
    if (!ulContainer) {
        return;
    }
    
    const total = 100000; // 插入数据的总数
    const batchSize = 4; // 每次批量插入的节点个数,个数越多,界面越卡顿
    const batchCount = total / batchSize; // 批处理的次数
    let batchDone = 0; // 已完成的批处理个数
    
    function appendItems() {
        // 使用 DocumentFragment 减少 DOM 操作次数,对已有元素不进行回流
        const fragment = document.createDocumentFragment();
    
        for (let i = 0; i < batchSize; i++) {
        const liItem = document.createElement("li");
        liItem.innerText = batchDone * batchSize + i + 1;
        fragment.appendChild(liItem);
        }
    
        // 每次批处理只修改 1 次 DOM
        ulContainer.appendChild(fragment);
        batchDone++;
        doAppendBatch();
    }
    
    function doAppendBatch() {
        if (batchDone < batchCount) {
        // 在重绘之前,分批插入新节点
        window.requestAnimationFrame(appendItems);
        }
    }
    
    // kickoff
    doAppendBatch();
    
    // 使用 事件委托 ,利用 JavaScript 的事件机制,实现对海量元素的监听,
    //有效减少事件注册的数量
    ulContainer.addEventListener("click", function(e) {
        const target = e.target;
        if (target.tagName === "LI") {
        alert(target.innerText);
        }
    });
    })();

本文链接:https://w4ctech.cn/frontWeb/execute.html
This blog is under a CC BY-NC-SA 3.0 Unported License