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
3this._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 | class EventBus { |
vite 原理
vite 新一代前端构建工具,基于现在浏览器都已普遍支持 ESM 这一特性实现构建。
vite 在编译项目时,会将项目 分为 依赖 + 源码 两个部分
依赖:指的是一些第三方库。这些第三方库会使用不同的模块化, 所以 vite 会将这些依赖统一转换处理成 ESM
- vite 通过 ESBuild 来对这些依赖进行处理,我们将这个过程称为 依赖预购建。
- 依赖预构建:
- 将依赖中不同模块转换成 ESM。
- 性能:为了提高性能,vite 会将多个 ESM 模块 合并成 一个,减少请求数。
- 缓存: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 对比
vite 中分为 2 个模块:依赖和源码;
webpack 中一切皆模块,不管是依赖还是源码都需要进行处理。vite 是基于浏览器已经普通支持 ESM 语法这一特性,所以在构建本地服务时,不需要打包
webpack 完全没有考虑这一特点,只要是模块都需要用 loader 进行处理vite 在启动时,不考虑编译问题,只考虑模块间的依赖,当浏览器中访问指定页面时,浏览器才开始按需加载当前页面,并会借助浏览器的缓存功能
webpack 在构建本地服务时要对所有的模块和依赖进行编译,无论当前页面是否被加载,webpack 的特点是先编译在运行,所以项目越大 运行速度越慢vite 支持 TS,只负责把 TS 代码进行转换,对于 TS 类型校验的事交给了 IDE 去做
webpack 在构建 TS 项目时,需要安装 typescript 运行, TSC会对 TS 代码进行校验和编译,所以慢vite 在 HMR 热更新时 也不进行编译,基于 ESM import.meta 实现 hot 属性
webpack 在热更新时会重新编译,所以也很慢。
Tree-Sharking 原理
ESM 引入进行静态分析,编译时会判断引入了哪些模块,判断哪些模块的变量未被使用,从而删除对应的代码
babel 怎么做 polyfill
babel 只做语法转换,将 ES6 转为 ES5,但是如果 在 ES5 中,有些对象方法在实际浏览器中可能不被支持,比如 Promise、array.prototype 等,这时候就需要通过 Babel-polyfill 来解决
Babel-polyfill 虽然可以通过模拟浏览器解决不存在对象方法的事,但存在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 事件绑定原理
- vue 通过解析模板的 html 提取 DOM 上的所有属性
- 通过正则匹配出事件名和事件处理方法
- 根据得到的事件名和事件处理方法,vue 会(调用 gen 回调)生成事件处理函数,这个函数中维护具体的事件名、对应的处理方法和修饰符等信息
- 将事件处理函数 通过 调用原生 DOM API addEventListener 注入到虚拟 DOM 中。
Vue 响应式原理
Vue2 的响应式原理
Vue 2 中是采用 Object.defineProperty 来实现响应式的,通过劫持对象的属性,使得访问和修改对象属性时触发 相应的 setter和getter 函数。
在一开始组件被实例化时,如果模板上有绑定插值变量,视图中会关联一个 watcher,用来观测依赖更新,
这时就会触发 getter 去收集依赖,并将依赖维护在一个 dep 数组中。
修改对象属性就会触发 setter,setter 就会通知依赖更新,watcher 收到依赖更新后 就会更新视图(这中间还有一层 patch 新旧节点对比)
Object.defineProperty 的缺陷
- 因为它劫持的是对象的属性,所以它无法检测到对象属性的新增和删除,vue 提供了api 方法 vm.update,vm.delete
- 由于数组是通过一系列的数组方法来改变数组内(.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 中用于实现数据的响应式的,其不同是
- reactive 用于复杂数据类型、ref 用于 基本数据类型
- 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 的区别
- watch 不会在组件初始化时马上执行,watchEffect 会
- watch 支持深度监听和立即执行回调函数,watchEffect 不支持
- watch 适合监听特定数据的变化后执行异步操作等
- watchEffect 适用于需要自动依赖收集的场景。