# 原理分析

# 1 webpack 热更新原理

  • 当修改了一个或多个文件;
  • 文件系统接收更改并通知 webpack
  • webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
  • HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp
  • HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。

# 2 webpack 打包原理

webpack 是由 nodejs 编写的前端资源加载/打包工具,由 nodejs 提供了强大的文件处理,IO 能力。

webpack 的打包原理

  • 识别入口文件
  • 通过逐层识别模块依赖(Commonjs、amd 或者 es6 的 import,webpack 都会对其进行分析,来获取代码的依赖)
  • webpack 做的就是分析代码,转换代码,编译代码,输出代码。
  • 最终形成打包后的代码

loader

loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

  • 处理一个文件可以使用多个 loader,loader 的执行顺序和配置中的顺序是相反的,即最后一个 loader 最先执行,第一个 loader 最后执行

  • 第一个执行的 loader 接收源文件内容作为参数,其它 loader 接收前一个执行的 loader 的返回值作为参数,最后执行的 loader 会返回此模块的 Javascript 源码

plugin

在 webpack 运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

loader 和 plugin 的区别

loader, 它是一个转换器,将 A 文件进行编译成 B 文件,比如:将 A.less 转换为 A.css, 单纯的文件转换过程。

plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后, webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务

# 3 React 和 Vue 的 diff 时间复杂度 从 O(n^3) 优化到 O(n)的原理

它们做了三种优化来降低复杂度:

  • 如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染;
  • 如果子节点有变化,Virtual DOM 不会计算变化的是什么,而是重新渲染;
  • 通过唯一的 key 策略

# 4 Vue 中的 computed 是如何实现的

computed 本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的 getter, 而不是用户定义的方法。

computed 内部实现了一个惰性的 watcher,在实例化的时候不会去求值,其内部通过 dirty 属性标记计算属性是否需要重新求值。当 computed 依赖的任一状态(不一定是 return 中的)发生变化,都会通知这个惰性 watcher,让它把 dirty 属性设置为 true。 所以,当再次读取这个计算属性的时候,就会重新去求值。

# 5 NextTick 原理分析

$nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM

由于Vue DOM 更新是异步执行的, 即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick() 方法。

MutationObserver

MutationObserver 是 HTML5 新增的属性,用于监听 DOM 修改事件,能够监听到节点的属性、文本内容、子节点等的改动。

调用过程是要先给它绑定回调,得到 MO 实例,这个回调会在 MO 实例监听到变动时触发。 这里 MO 的回调是放在 microtask 中执行的。

  // 创建 MO 实例
  const observer = new MutationObserver(callback)

  const textNode = '想要监听的 DOM 节点'

  observer.observe(textNode, {
    characterData: true // 说明监听文本内容的修改
  })

在 Vue 2.4 之前都是使用的 microtasks,但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况, 但如果都使用 macrotasks 又可能会出现渲染的性能问题。所以在新版本中,又默认使用 microtasks, 但在特殊情况下会使用 macrotasks,比如:v-on

对于实现 macrotasks, 会先判断是否能使用 setImmediate, 不能的话降级为 MessageChannel, 以上都不行的话就使用 setTimeout

  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    macroTimerFunc = () => {
      setImmediate(flushCallbacks)
    }
  } else if (
    typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || MessageChannel.toString() === '[object MessageChannelConstructor')
  ) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.prot.onmessage = flushCallbacks
    macroTimerFunc = () => {
      post.postMessage(1)
    }
  } else {
    macroTimerFunc = () => {
      setTimeout(flushCallbacks, 0)
    }
  }

总结

以上就是 VuenextTick 方法的实现原理了,总结一下就是:

  • vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调后执行;
  • microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕;
  • 因为兼容性问题,vue 不得不做了 microtaskmacrotask 的降级方案;

或者这么回答:

原理就是使用宏任务或微任务来完成事件调用的机制,让自己的回调事件在一个 eventloop 的最后执行。宏任务或微任务根据浏览器情况采取不同的 api, 常见的 macro tasksetTimeoutMessageChannelpostMessagesetImmediate; 常见的 micro taskMutationObserverPromise.then

# 6 线程,进程,EventLoop

# 线程与进程

我们经常说 JS 是单线程执行的,指的是一个进程里只有一个主线程。

那到底什么是线程?什么是进程?

官方的说法是:进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位。

  • 进程好比工厂,有单独专属于自己的工厂资源。
  • 线程好比工人,多个工人在一个工厂协作。一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
  • 工人们共享工厂的空间。进程的内存空间是共享的,每个线程都可以用这些共享内存。
  • 多个工厂之间是独立存在的。

# 多进程与多线程

多进程

在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的。比如你可以听歌词的同事打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。

多线程

程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。允许单个程序创建多个并行执行的线程来完成各自的任务。以 chrome 为例,当你打开一个 tab 页面时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程,js 引擎线程, http 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

# 浏览器内核

浏览器内核是多线程,在内核控制下各线程相互配合保持同步,一个浏览器通常由一下常驻线程组成:

  • GUI 渲染线程
  • JavaScript 引擎线程
  • 定时触发器线程
  • 事件触发线程
  • 异步 http 请求线程

GUI 渲染线程

  • 主要负责页面的渲染,解析 HTMLCSS,构建 DOM 树, 布局和绘制等。
  • 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
  • 将线程与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 渲染会被挂起,当任务队列闲空时,主线程才会去执行 GUI 渲染。

JavaScript 引擎线程

  • 该线程当然是主要负责处理 JavaScript 脚本,执行代码。
  • 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS 引擎线程的执行。
  • 当然,该线程与 GUI 渲染线程互斥,当 JS 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。

定时触发器线程

  • 负责执行异步定时器一类的函数的线程,如:setTimeout, setInterval
  • 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行。

事件触发进程

  • 主要负责将准备好的事件交给 JS 引擎线程执行。 比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回到函数,或者用户触发点击事件时,该线程会将整装待发的时候依次加入到任务队列的队尾,等待 JS 引擎线程的执行。

异步 http 请求线程

  • 负责执行异步请求一类的函数的线程,如: Promise, axios, ajax等。
  • 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JS 引擎线程执行。

# 浏览器中的 Event Loop(事件循环)

Micro-Task 与 Macro-Task

浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。

  • 常见的 macro-task 比如:setTimeoutsetIntervalscript (整体代码)、I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: new Promise().then(回调)MutationObserver (html5 新特性)等。

Event Loop 过程解析

Event Loop 过程解析

  • 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空, macro 队列里有且只有一个 script 脚本(整体代码)。
  • 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入到各自的任务队列里。同步代码执行完了,script 脚本会被移除出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
  • 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时, 任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此。我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
  • 执行渲染操作,更新界面。
  • 检查是否存在 web workder 任务,如果有,则对其进行处理。
  • 上述过程循环往复,直到两个队列都清空。

我们总结一下,每一次循环都是一个这样的过程:

循环过程

当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,以此类推。

  Promise.resolve().then(() => {
    console.log('Propmise1')

    setTimeout(() => {
      console.log('setTimeout2')
    }, 0)
  })

  setTimeout(() => {
    console.log('setTimeout1')

    Promise.resolve().then(() => {
      console.log('Promise2')
    })
  }, 0)

最后输出结果是 Promise1, setTimeout1, Promise2, setTimeout2

  • 一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出 Promise1,同时会生成一个宏任务 setTimeout2
  • 然后去查看宏任务队列,宏任务 setTimeout1setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1
  • 在执行宏任务setTimeout1时会生成微任务 Promise2,加入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promisee2
  • 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2

# 7. encode/decode 编码解码

一张图看懂 encodeURIencodeURIComponentdecodeURIdecodeURIComponent 的区别

# 这四个方法的用处

# 1.用来编码和解码 URI 的

统一资源标识符,或叫做 URI, 是用来标识互联网上的资源(例如,网页或支付)和怎样访问这些资源的传输协议(例如,HTTPFTP)的字符串。除了 encodeURIencodeURIComponentdecodeURIdecodeURIComponent 四个字来编码和解码 URI 的函数之外 ECMAScript 语言自身不提供任何使用 URL 的支持。

# 2.URI组成形式

一个URI是由组件分隔符分割的组件序列组成,其一般形式:

Scheme : First / Second ; Third ? Fourth

其中斜体的名字代表组件; “:”, “/”, “;”,“?”是当作分隔符的保留字符;

# 3.有何不同?

encodeURIdecodeURI 函数操作的是完整的 URI;这俩函数假定 URI 中的任何保留字符都有特殊意义,所有不会编码它们。

encodeURIComponentdecodeURIComponent 函数操作的是组成 URI 的个别组件;这俩函数假定任何保留字符都代表普通文本,所以必须编码它们,所以它们(保留字符)出现在一个完整 URI 的组件里面时不会被解释成保留字符了。

以上说明摘自ECMAScript标准 (opens new window),为了容易读懂做了点编辑加工。

# 4.图解四个函数的不同:

ECMA对这四个函数还做了详细解释 (opens new window),可能是为了写的更逻辑化一些,采用了类似变量配合逻辑的写法来说明,但是让初学者看得云里雾里的特别绕,所以有必要把它写得更像是人读的东西……

图解

当 URI 里包含一个没在上面列出的字符或有时不想让给定的保留字符有特殊意义,那么必须编码这个字符。字符被转换成 UTF-8 编码,首先从 UT​​F-16 转换成相应的代码点值的替代。然后返回的字节序列转换为一个字符串,每个字节用一个“%xx”形式的转移序列表示。具体转换规则可以参考抽象操作Encode和Decode的说明 (opens new window)