1. JS解释编译简要过程

    源代码通过解析器生成AST(Abstract syntax tree),然后生成字节码(介于高级语言与机器语言之间的中间代码),再通过解释器逐行解释并执行,借助JIT(Just in time)技术监测代码执行过程,根据代码执行频率从高到低将代码标记为不同等级,低等级的通过基线编译器编译,高等级的通过优化编译器编译,之后将执行编译后的代码,而一旦优化后代码不再满足优化条件,则将其降级到基线编译器或解释器中去处理,这就是去优化;

  2. 对JS单线程非阻塞的理解

    单线程:JS代码一定是在单个线程中同步执行的(JS引擎线程);
    非阻塞:异步操作不会阻塞同步代码的执行,异步操作实际是由浏览器代理,然后基于Event Loop机制,异步操作完成后的回调函数会由JS引擎执行;

  3. 现代浏览器有哪些进程和线程

    浏览器包含以下进程:

    • 浏览器进程,控制整个浏览器,负责协调其他进程间的协同工作;
    • GPU进程,负责浏览器界面的渲染;
    • 网络进程,处理网络请求或响应;
    • 渲染进程,解析并渲染标签页内容;
    • 插件进程,运行浏览器插件;

    渲染进程包含以下线程:

    • GUI渲染线程,解析html和CSS、构建DOM树、CSSOM树、渲染树、绘制页面;
    • JS引擎线程,编译、解释JS代码,并进行优化或去优化,与GUI渲染线程互斥
    • 定时器线程,提供计时功能,如setTimeout、setInterval;
    • http线程,提供http处理能力;
    • 事件触发线程,控制Event Loop;
    • 合成线程,将GUI渲染线程生成的
  4. Browser Event Loop

    JS同步任务只在一个线程中执行,称之为主线程,主线程通过执行栈的机制顺序执行所有代码;

    异步代码就将其交由浏览器其他线程代劳,JS线程继续执行同步代码,期间如果异步操作完成,则将对应的回调函数加入任务队列,最后等执行栈中的函数都执行完,此时将任务队列中的回调函数加入执行栈中继续执行,构成一个循环,即Event Loop;

    如上所述,实际上异步操作会调用浏览器提供的WebAPI,由浏览器代理,浏览器通过其他线程(如HTTP线程,定时器线程等)处理异步操作,等到完成后再将异步操作的回调函数加入任务队列,注意,这个过程进行的同时,JS线程继续执行其他代码

    任务队列又分为两种——宏任务和微任务;

    1. 宏任务:<script/>的所有代码,setTimeout, setInterval, postMessage的回调;
    2. 微任务:Promise.prototype.then的回调,MutationObserver实例化时的回调;

    处理任务队列时,优先处理微任务,最后处理宏任务;

    一个宏任务处理完成后重新渲染UI,然后再处理下一个宏任务;

  5. Node.js Event Loop

    Node Event Loop与Browser一样,都是在单个线程中运行的,Node将异步操作交给系统处理,Browser则是交给浏览器处理,当操作完成时再去执行对应的回调函数,但机制又有所不同,Node Event Loop分为以下几个阶段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
       ┌───────────────────────────┐
    ┌─>│ timers │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    │ │ pending callbacks │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    │ │ idle, prepare │
    │ └─────────────┬─────────────┘ ┌───────────────┐
    │ ┌─────────────┴─────────────┐ │ incoming: │
    │ │ poll │<─────┤ connections, │
    │ └─────────────┬─────────────┘ │ data, etc. │
    │ ┌─────────────┴─────────────┐ └───────────────┘
    │ │ check │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    └──┤ close callbacks │
    └───────────────────────────┘
    1. Timers:setTimeout,setInterval;
    2. Pending callbacks:执行上一阶段被延迟的I/O回调;
    3. Idle & Prepare:Node内部使用;
    4. Poll:触发新的I/O事件,执行I/O相关的回调;
    5. Check:setImmediate
    6. Close callbacks:主线程结束时的回调;

    Event Loop将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列为空或超过最大回调数,然后进入下一阶段,如此循环;

    Poll阶段会同步执行队列中的所有回调,且每个回调都可能触发更多事件,引入更多回调,除非超过最大回调数,因此Poll阶段可能会执行很长时间,进而导致Timers阶段的回调函数不能在精确的时间点上触发;

    Poll阶段队列空时,如果有setImmediate回调,才会进入Check阶段,否则将一直在该阶段,等待queue注册进新回调函数并立刻执行;

    每轮循环之间,会检查是否还有需要等待的异步事件(I/O或定时器),如果没有则结束循环;

    process.nextTick(callback, arg)不属于Event Loop机制,它被确保在当前执行栈结束,进入下一个Tick之前执行,它不受当前Event Loop所在阶段限制;

Html解析过程

整体流程:

  1. 字节流转码成字符串;
  2. 字符串解析成Token;
  3. 根据Token 生成DOM 节点;
    a. 解析到script 元素,会阻塞DOM 节点的生成;
    b. 解析到link 元素,会加载CSS 资源;
  4. 构建DOM Tree,待CSS 加载完毕,同时开始构建CSSOM Tree(互不影响);
  5. 合并DOM Tree 和CSSOM Tree 为Render Tree;
  6. Layout;
  7. Paint;

优化策略:

  • script 元素设置属性async 或defer,避免阻塞DOM 构建(async 是异步加载,defer 是延迟执行);
  • link 元素设置rel 属性为preload,预加载其他页面可能用到的样式;
  • 减少回流和重绘:

Script 阻塞DOM 构建:解析到普通script 元素,需要等获取到资源且CSSOM 构建完成,才会立即执行脚本,这个过程会阻塞DOM 的构建;

Script async & defer:async 是异步加载,获取资源这个过程将不再阻塞DOM 构建,但是获取之后效果同普通script 元素是一样的,会阻塞DOM 的构建(本质是因为渲染线程与JS引擎线程互斥);无序加载脚本;
defer 是延迟执行,浏览器DOM 构建完毕,且JS 资源获取完毕之后再执行JS;可以有序加载脚本;

Layout & Paint:根据Render Tree 计算每个元素在页面上的位置、尺寸,称为Layout(布局)阶段;
将每个元素渲染到页面上,称为Paint(绘制)阶段;

Repaint:元素颜色、背景等改变,会重复进入Paint 阶段,称为重绘;

Re-layout:元素位置、尺寸改变,会重复进入Layout 阶段,称为回流,回流之后一定会发生重绘;

导致发生回流的操作:

  1. 添加、删除页面元素;
  2. 使用display 控制页面元素的显隐;
  3. 改变width,height,margin,border,padding,content;
  4. 窗口Resize;
  5. 读写offsetWidth/offsetHeight;
  6. 写style;

导致发生重绘的操作:改变color,background,outline,text-decoration,border-radius,box-shadow 等;

减少回流和重绘的方法:

  1. 动画使用requestAnimationFrame 控制;
  2. 尽量少读写offsetWidth 等属性;
  3. 使频繁重绘/回流的节点脱离文档流,避免其他节点受其影响也发生回流/重绘;