vue-router 的实现

vue-router 的作用:通过改变 URL,在不重新请求页面的情况下 更新视图

vue-router 的两种模式

一、hash 模式

vue hash 模式路由,在 URL 上带有 # 号,hash 路由 通过 改变 # 号后面的路由来更新视图。# 号后面的URL 改变 不会触发浏览器重新发起请求。

hash 模式的原理是根据 onhashchange 来监听 URL 变化,通过 window.location.hash 来获取当前 url 值

二、history 模式 (基于 HTML5 history API)

history 模式 url 上不会带有 # 号,通过 pushState 、replaceState 、popState实现跳转页面且不重新发送请求。

使用 pushState 跳转 url 不会向服务器发起新的文档请求,并且它只能实现当前域的跳转,并且会添加一条访问记录
popState 可以监听当前url 是否改变,history.back, history.forward,history.go 都会触发 popState

history.pushState 和 history.replaceState 不会触发 popState 事件

使用 history 路由 虽然也可以实现像 hash 路由那样实现跳转 url 不重新发起文档请求,但用户在刷新页面时还是会重新发起请求,为了避免这种情况,所以需要服务端配合重定向返回 html。

三、浏览器的 history.pushState 和 框架中的 history.pushState 的区别

路由框架的pushState与history.pushState是不一样的,路由框架的pushState不仅调用了history.pushState改变了url,更重要的是它还多了一步操作,即根据这个url销毁了旧组件,渲染了新组件;至于state里面的key值,则是为了兼容hashHistory。

Vuex 的底层实现

一、vuex 的核心流程
vue component 接受交互行为,调用 dispatch 方法触发 action 相关处理,若页面状态需要改变,则调用 commit 方法提交 mutation 修改 state,通过 getter 获取到 state 新值,重新渲染 vue component

二、vuex 的底层原理

  • state: 提供一个响应式数据,通过 new vue 实现
      
    1
    2
    3
    this._vm = new Vue({
    $state: state
    })
  • getter: 借助 vue 的计算属性 computed 来实现缓存
  • mutation: 更改 state,通过 commit 修改 state,是唯一修改state 的地方,并且是同步的
  • action: 触发 mutation 方法, action 可以包含些异步操作
  • module: Vue.set 动态 添加 state 到响应式数据中去。实际是根据 store 配置递归建立相应的 module 和 module间的父子关系,在根据 namespace 来分割模块,使得 commit/dispatch 时需要制定 namespace

Vue provide 和 inject

provide、inject 是 vue 组件通信的一种方式,允许祖先组件向其所有的子孙后代 注入一个依赖,不论组件层次有多深

原理

  • 依赖注入,其核心原理就是通过 $parent 向上查找祖先组件的 provide,找到则赋值给 inject ,未找到给其 default 值。依赖注入原理和 javascript 中 instanceof 操作符原理类型,instance of 中,通过 proto 向原型链中查找,如果 __proto__和 构造函数的 prototype 相等 则返回true

provide 和 inject

  • provide、inject 的初始化阶段是在 beforeCreate 和 Create 之间,所以日常开发中 可以在 Create 中访问到
  • 各类型初始化阶段:
    initInject: 首先初始化 inject 的注入内容
    initState: 初始化 Vue 各项资源,data、props、methods、computed、watch 等
    initProvide: 初始化 provide
  • 所以在 data、props 中 可以访问到 inject,在 provide 中 可以访问到data、props 等其自身的方法和属性
  • provide 中可以提供自身的方法和属性给后代组件,并且是具有响应式的,但如果注入的是一个 普通对象 那么就不具备响应式。

Vue EventBus

用于兄弟组件之间进行通信,是组件传递消息的一种方式

EventBus 原理:
实际上就是发布-订阅者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class EventBus {
constructor() {
this.event = null
}

on(name, fn) {
if (!this.event[name]) {
this.event[name] = []
}
this.event[name].push(fn)
}

emit(name, args) {
this.event[name] && this.event[name].forEach(fn => {
fn(...args)
})
}
}

vite 原理

vite 新一代前端构建工具,基于现在浏览器都已普遍支持 ESM 这一特性实现构建。

vite 在编译项目时,会将项目 分为 依赖 + 源码 两个部分

依赖:指的是一些第三方库。这些第三方库会使用不同的模块化, 所以 vite 会将这些依赖统一转换处理成 ESM

  • vite 通过 ESBuild 来对这些依赖进行处理,我们将这个过程称为 依赖预购建。
  • 依赖预构建:
    1. 将依赖中不同模块转换成 ESM。
    2. 性能:为了提高性能,vite 会将多个 ESM 模块 合并成 一个,减少请求数。
    3. 缓存:vite 会为预构建的请求进行强缓存,并将预构建的依赖项缓存在 node_modules/.vite 中,它会根据package.json、NODE_ENV等文件是否变更来决定是否重新构建依赖。

源码: 指的是项目中的代码,包括 JS 文件和需要转换的文件,如.vue 文件

Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Last-Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

vite 的热更新:基于 ESM 能力,实现 import.meta.host

为什么 vite 开发环境使用 ESBuild, 生产环境仍需要 Rollup

“尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)”
“Vite 目前的插件 API 与使用 esbuild 作为打包器并不兼容。尽管 esbuild 速度更快,但 Vite 采用了 Rollup 灵活的插件 API 和基础建设,这对 Vite 在生态中的成功起到了重要作用。目前来看,我们认为 Rollup 提供了更好的性能与灵活性方面的权衡。”

vite 作者尤雨溪在 vite 官网上解释过这个问题,大致意思是: ESBuild 还是处于开发阶段,对于代码分割、TreeSharking 方面还不太完善。所以在生产环境为了用户体验、网站性能方面考虑,仍需要对项目进行打包。而 RollUp 和 webpack 一样是个成熟的构建工具,rollup 会更灵活。未来,esbuild 可能会成为主流构建。

rollup 有以下几个优点:
css 代码分割,如果某个异步模块引入了一些 css 代码,vite 会自动将这些 css 抽取出来 生成单独的文件,提高线上产物的缓存复用率

自动预加载,vite 会自动为入口 chunk 的依赖自动生成 预加载标签 <link rel="modulePreload" > 这种适当的预加载会让浏览器提前下载好资源,优化页面性能。

vite 和 webpack 对比

  1. vite 中分为 2 个模块:依赖和源码;
    webpack 中一切皆模块,不管是依赖还是源码都需要进行处理。

  2. vite 是基于浏览器已经普通支持 ESM 语法这一特性,所以在构建本地服务时,不需要打包
    webpack 完全没有考虑这一特点,只要是模块都需要用 loader 进行处理

  3. vite 在启动时,不考虑编译问题,只考虑模块间的依赖,当浏览器中访问指定页面时,浏览器才开始按需加载当前页面,并会借助浏览器的缓存功能
    webpack 在构建本地服务时要对所有的模块和依赖进行编译,无论当前页面是否被加载,webpack 的特点是先编译在运行,所以项目越大 运行速度越慢

  4. vite 支持 TS,只负责把 TS 代码进行转换,对于 TS 类型校验的事交给了 IDE 去做
    webpack 在构建 TS 项目时,需要安装 typescript 运行, TSC会对 TS 代码进行校验和编译,所以慢

  5. vite 在 HMR 热更新时 也不进行编译,基于 ESM import.meta 实现 hot 属性
    webpack 在热更新时会重新编译,所以也很慢。

Tree-Sharking 原理

ESM 引入进行静态分析,编译时会判断引入了哪些模块,判断哪些模块的变量未被使用,从而删除对应的代码

babel 怎么做 polyfill

babel 只做语法转换,将 ES6 转为 ES5,但是如果 在 ES5 中,有些对象方法在实际浏览器中可能不被支持,比如 Promise、array.prototype 等,这时候就需要通过 Babel-polyfill 来解决

Babel-polyfill 虽然可以通过模拟浏览器解决不存在对象方法的事,但存在2个问题:

  1. 直接修改内置的原型,造成全局污染
  2. 无法按需引入

所以社区中出现了 Babel-runtime, 不仅解决了 对象方法不被浏览器所支持的问题,也实现按需引入,不再修改内置原型,而是通过替换的方式 来解决。

v-if 和 v-show 的区别

v-if 和 v-show 的作用都是用来控制 DOM 的展示和隐藏,不同的是
1、控制手段不同:v-show 是通过 css 来切换,v-if 是新建和删除 DOM 来控制
2、编译过程不同:v-show 是基于 css 进行切换,v-if 切换有一个局部的编译/卸载的过程

v-show 和 v-if, 虽然从表面看 类似,都是控制组件的显示和隐藏。但内部实现差距还是很大的

v-if 它在切换的过程中,条件块内部的事件监听器和子组件会被适当的重建和整合。满足条件后会触发对应组件的更新。
v-if 渲染的节点,由于新旧节点的vnode冲突, 在核心 diff 算法对比过程中,会移除旧节点 创建新节点。那么就会创建新的组件,经历组件自身初始化、渲染 vnode、patch等过程。

对于 v-show 渲染的节点,在初始化阶段时 它会先生成两个条件的组件,所以在后续的条件渲染中,由于新旧 vnode 一致,它只需 patchVnode 即可,在 patchVNode 的过程中,内部会执行 v-show 指令对应的回调更新,根据 v-show 指令绑定的值来设置它作用的 DOM 元素 style.display 的值

因此:相比 v-if 不断删除和创建新的DOM,v-show 只在更新现有 DOM 上的显隐值。所以 v-show 的支出比 v-if 小很多。

v-show 适用于多次切换的场景,v-if 适用于少数切换的场景。

v-show 相比 v-if 的性能优势时组件在更新阶段,如果在初始化阶段,v-if 性能优于 v-show.
因为 v-if 它本身只会渲染一个分支, v-show 把两个分支都渲染了。
在使用 v-show 时,所以分支内部组件都会渲染,对应的生命周期钩子函数都会执行,而使用 v-if 时 没有命中的分支内部组件是不会渲染的(包含内部的生命周期函数)

Vue 事件绑定原理

  1. vue 通过解析模板的 html 提取 DOM 上的所有属性
  2. 通过正则匹配出事件名和事件处理方法
  3. 根据得到的事件名和事件处理方法,vue 会(调用 gen 回调)生成事件处理函数,这个函数中维护具体的事件名、对应的处理方法和修饰符等信息
  4. 将事件处理函数 通过 调用原生 DOM API addEventListener 注入到虚拟 DOM 中。

Vue 响应式原理

Vue2 的响应式原理

Vue 2 中是采用 Object.defineProperty 来实现响应式的,通过劫持对象的属性,使得访问和修改对象属性时触发 相应的 setter和getter 函数。

在一开始组件被实例化时,如果模板上有绑定插值变量,视图中会关联一个 watcher,用来观测依赖更新,
这时就会触发 getter 去收集依赖,并将依赖维护在一个 dep 数组中。

修改对象属性就会触发 setter,setter 就会通知依赖更新,watcher 收到依赖更新后 就会更新视图(这中间还有一层 patch 新旧节点对比)

Object.defineProperty 的缺陷

  1. 因为它劫持的是对象的属性,所以它无法检测到对象属性的新增和删除,vue 提供了api 方法 vm.update,vm.delete
  2. 由于数组是通过一系列的数组方法来改变数组内(.push,.pop)Object.defineProperty 主要用于检测对象的,所以对于数组长度的变化无法检测到

Vue2 中怎么重写数组的

由于数组是通过一系列的数组方法来改变数组内(.push,.pop等)所以对于数组的长度无法检测到。
vue2 中为了可以检测到数组的变化 通过拦截数组的原型 去实现检测 数组的变化。(这也暴露了一个缺陷,使用下标修改数组 无法被检测到)
同样还是通过 getter 去收集依赖,将收集到的依赖 保存在 Observe 实例中。通过Observe 去检测依赖的更新。
observe 中 有一个 ob 属性 用于记录该数据是否是响应式

Vue.$set

Vue.$set 挂载在 vue 原型上的方法,用于更新对象的新增属性,使新增属性也具有响应式。
原理是先判断该属性是否响应式属性,通过 ob 判断, 如果不是 调用 defineReactive 方法 使其变为响应式

Vu.$delete

Vue.$delete 挂载在 vue 原型上的方法,用于删除对象属性。

实现方法比较粗暴: 直接使用 delete obj.name this.dep.notify()

Vue3 响应式原理

Vue3 中怎么实现数据的响应式

Vue3 通过 Proxy 和 Reflect 搭配使用 实现响应式原理

Proxy : 可以劫持整个对象,并且会返回一个新对象,Proxy 可以拦截并重新定义一个对象的基本操作,对于一些复合操作,proxy 无法调用

但由于对象中的一些方法都是范性的,不能直接调用。于是 Reflect 就登场了

Reflect 用于执行对象的默认操作
Proxy 中对对象的代理方法 和 Reflect 一一对应。所以无论通过 Proxy 怎么修改 都可以通过 Reflect 去获取对象的默认行为。

使用 Reflect 的好处: 1. 方法更具有语义化,并且直接使用 Reflect API 不会报错

Reflect 接收三个参数
Reflect(target, key, receiver) 其中 receiver 就是 代理对象,可以用于改变 this 指向。

Vue3 中先使用 Proxy 进行对对象拦截操作,然后在用 track 和 trigger 收集依赖和触发依赖的更新

Proxy API 不能监听到对象内部深层次的属性变化,它在 getter 中去递归响应式,按需实现响应式

Vue3 浅层响应式

props 是浅层响应,因为 props 是由父组件传递给子组件的,它的深层响应 应该由 父组件来保证

Vue3 中 的 Ref 和 Reactive

ref 和 reactive 都是 vue3 中用于实现数据的响应式的,其不同是

  1. reactive 用于复杂数据类型、ref 用于 基本数据类型
  2. reactive 基于 Proxy 实现的,ref 基于 reactive 的基础上实现的

reactive 的实现原理

reactive 是基于 Proxy 实现,Proxy 用于对整个对象进行代理 并返回一个新对象,所以当我们使用 reactive 去绑定一个对象,如果修改了对象的原数据,那么这个 reactive 就失去响应式

使用解构 reactive的对象也会让其失去响应式

ref 的实现原理

ref 是基于 reactive 实现的,由于 proxy只能对对象进行代理,所以在实现基本数据类型的响应式时,是将基本数据类型包装成对象。

ref = reactive({value: target}) 这也是我们在使用 ref 时 为什么要用 .value 去访问的原因。

由于在实现响应式前 会先对数据进行一层包装,所以重新赋值一个新对象给 ref 不会失去响应式

toRef

toRef 使一个对象的属性具有响应式 – 对象属性具有响应式

toRefs

toRefs 将一个d对象变为响应式变量 – 整个对象都具有响应式

watch

用于监听响应式变量的变化,在组件初始化时不执行

watchEffect

用于监听响应式变量的变化,会在组件初始化时执行

watch 和 watchEffect 的区别

  1. watch 不会在组件初始化时马上执行,watchEffect 会
  2. watch 支持深度监听和立即执行回调函数,watchEffect 不支持
  3. watch 适合监听特定数据的变化后执行异步操作等
  4. watchEffect 适用于需要自动依赖收集的场景。