vue3核心技术揭秘

vue3核心技术揭秘

vue 的特点(优点)

  • vue 关注视图层,用数据操作的方式代替了 dom 操作
  • vue 通过响应式的数据绑定实现了数据和视图之间的更新交互
  • vue 通过组件化开发让工程结构更加明确,更易于维护
  • vue 通过虚拟 DOM,优化了 DOM 操作,实现了性能提高
  • vue 拥有自己的 vue-cli/vite 脚手架工具,对良好的工程化性能

vue3 生命周期

  • onBeforMount -> vue 实例挂载之前执行
  • onMounted -> vue 实例挂载完成执行
  • onBeforeUpdate -> 组件内数据发生变化之前执行
  • onUpdated -> 组件内数据发生变化之后执行
  • onBeforeUnmount -> 组件销毁之前
  • onUnmounted -> 组件销毁之后
  • onActivated -> keep-alive 组件激活时执行
  • onDeactivated -> keep-alive 组件销毁时执行
  • onErrorCaptured -> 捕获错误
  • onRenderTracked -> dev 组件更新时跟踪所有变量和方法
  • onRenderTriggered -> dev 触发渲染时调用,返回变化新旧值

vue3 相比于 vue2 有哪些不同?

  • 组合式(composition)api
    • vue2 是选项式(option)api
  • 响应式原理
    • vue2 响应式原理基础是 Object.defineProperty
      • 深层嵌套递归数据响应式
      • 缺点:无法监听对象或数据新增、删除的元素
      • 解决方案:针对数组原型方法 push、pop、shift、unshift、splice、sort 等进行 hack 处理,提供 Vue.set 监听对象/数组新增属性
      • tips:Object.defineProperty 可以监听数组已有元素,vue2 没有提供是因为性能问题
    • vue3 是 Proxy(配合 Reflect)
      • 兼容性:放弃了 IE11 以下
      • 动态属性增删都可以拦截
      • 使用 Reflect 可以修正 Proxy 的 this 指向问题
      • vue3 使用 Proxy 并不能监听对象内部深层次的属性变化,处理方式是在 getter 中递归响应式,只有真正访问内部属性时才会变成响应式,节约性能
  • 生命周期的变化
    • vue3 需要添加 on ,使用上需要先引入,vue 可以直接调用
    • 移除了 beforeCrete、created
  • 多根节点
    • vue3 支持多根节点
  • 异步组件
    • vue3 提供 Suspense 组件,通过 fallback 插槽提供异步组件渲染兜底的内容,如 loading 等
  • Teleport
    • vue3 提供 Teleport 组件可将部分组件移动到指定 dom 节点位置,如 Dialog 组件
  • css 变量
    • 支持在 style 标签中使用 v-bind,给 css 绑定 js 变量
  • 代码打包体积
    • vue3 的 api 可以被 tree-shaking,使用了 es6module,tree-shaking 依赖于 es6 模块的静态结构特性
  • 虚拟 dom
    • vue3 静态提升:保存静态节点(pathchflag 为 -1)直接复用,添加更新类型标记 pathchflag(为 1 是动态绑定的元素)
    • 事件缓存,可以在第一次渲染后缓存事件,vue2 每次渲染都会传递一个新函数
  • diff 算法
    • vue2 双端比较
    • vue3 最长递归子序列

defineProperty 和 Proxy 的区别?

  • Object.defineProperty 是 Es5 的方法,Proxy 是 Es6 的方法

  • defineProperty 是劫持对象属性,Proxy 是代理整个对象;

  • defineProperty 不能监听(重置可以)到对象新增属性和修改新增属性的变化,Proxy 可以

  • defineProperty 不能监听根据自身数组下标修改数组元素的变化(所以 vue2 提供了 Vue.$set和Vue.$delete)

  • defineProperty 不兼容 IE8,Proxy 不兼容 IE11

  • defineProperty 不支持 Map、Set 等数据结构

  • defineProperty 只能监听 get、set,而 Proxy 可以拦截多达 13 种方法;

  • Reflect 是为了在执行对应的拦截操作的方法时能传递正确的 this 上下文

    • Proxy handler 中的 receiver 指向
      • 正常情况下,receiver指向的是当前的代理对象
      • 特殊情况下,receiver指向引发当前操作的对象(obj)
        • Object.setPrototypeOf(obj, proxy),访问 obj.name 时如果没有 name 就会根据原型链查找
  • Proxy 兼容性相对较差,且无法通过 pollyfill 解决;所以 Vue3 不支持 IE11 以下;

  • Proxy 返回的是一个新对象

  • Proxy 也是不能对对象进行深层次响应的,实现动态递归响应式,只有在使用(get)的时候才会做响应式处理

vue3 响应式原理

  • activeEffect 解决匿名函数问题
  • WeakMap、Map、Set 存储对象属性的相关副作用函数
  • track()实现依赖收集、层级依赖追踪、依赖清理(解决嵌套副作用)
  • trigger()当某个依赖值发生变化时,通过执行副作用函数获得与依赖变化后对应的最新值

v-if 和 v-for 的优先级?

v-if 的优先级高于 v-for

ref 和 reactive 定义响应式数据

ref 原理

  1. ref 内部封装一个 RefImpl 类,并设置 get/set,当通过.value 调用就会触发劫持,从而实现响应式
  2. 当接受的对象或数组时,内部仍然是 reactive 去实现的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 源码路径:packages/reactivity/src/ref.ts

class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
public readonly __v_isRef = true

constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}

get value() {
trackRefValue(this)
return this._value
}

set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}

reactive 原理

  1. 使用 Proxy 代理传入对象实现响应式
  2. Proxy 拦截数据的更新和获取操作,使用 Reflect 完成原本的操作(get/set)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers,
proxyMap
) {
if (!shared.isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target['__v_raw' /* RAW */] &&
!(isReadonly && target['__v_isReactive' /* IS_REACTIVE */])
) {
return target;
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// only specific value types can be observed.
const targetType = getTargetType(target);
if (targetType === 0 /* INVALID */) {
return target;
}
const proxy = new Proxy(
target,
targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers
);
proxyMap.set(target, proxy);
return proxy;
}

vue3 中 watch 和 watchEffect 有什么区别?

  • watch 显式指定依赖源,依赖源变化时执行回调函数

    • 第一个参数为不同形式的数据源
      • 单个 ref
      • 计算属性
      • getter 函数(要有返回值)
      • 响应式对象(默认时深层遍历),不能直接侦听响应式对象的属性,应该用一个返回该属性的 getter 函数
      • 以上类型的值组成的数组
    • 第二个参数是数据发生变化时执行的回调函数
      • 接收三个参数:新值、旧值、清理副作用的回调函数(例如清除无效的副作用,等待中的异步请求)
    • 第三个参数是一个可选对象
      • immediate:在侦听器创建时立即触发回调
      • deep:深度遍历
      • flush:回调的触发时机
        • pre:默认,dom 更新前调用
        • post:dom 更新后调用
        • sync:sync 同步调用
      • onTrack/onTrigger: 用于调试的钩子,在依赖收集和回调函数触发时被调用
    • 其他:
      • watch 的返回值是一个用来停止该副作用的函数
      • 使用同步语句创建的侦听器,会自动绑定到宿主组件实例实例上,并在宿主组件卸载时自动停止
      • 异步回调(setTimeout 等)创建的侦听器,则不会绑定到当前组件上,必须手动停止,以防止内存泄漏
  • watchEffect 自动收集依赖源,依赖源变化时重新执行自身

    • 接收两个参数
      • 第一个参数是一个回调函数
        • 回调函数的参数为一个 onCleanup 函数,用来清除副作用
      • 第二个参数是一个可选对象
        • flush:回调的触发时机
          • pre:默认,dom 更新前调用
          • post:dom 更新后调用
          • sync:sync 同步调用
    • watchEffect 的回调函数会立即执行,即{immediate: true}
    • computed 其实类似一个带输出的同步版本 watchEffect
    • watchEffect 仅会在同步执行期间才会追踪依赖,使用异步回调时,只有在第一个 await 之前访问到的依赖才会被追踪
    • watchEffect 无法访问侦听数据的新值和旧值
    • 动态新增的依赖也会被收集
  • 使用场景:

    • 大部分时候使用 watch 显示指定依赖以避免不必要的重复触发,也避免在后续代码修改或重构时不小心引入新的依赖
    • watchEffect 使用于逻辑相对简单,依赖源和逻辑强相关的场景

vue 中动态引入的图片为什么要是 require?

因为动态添加 src 被当作静态资源处理了,而动态 src 编译过后的地址与图片编译后的资源地址不一致导致无法正确引入资源,而使用 require 返回的资源文件就是编译后的文件地址,所以可以正确的引入资源

1
2
3
4
5
6
7
8
9
10
11
// vue文件中使用require动态的引入一张图片
<template>
<div class="home">
<!-- 使用require动态引入图片 -->
<img :src="require('../assets/logo.png')" alt="logo">
</div>
</template>

//最终编译的结果
//这张图片是可以被正确打开的
<img src="/img/logo.6c137b82.png" alt="logo">
  • 什么是静态资源?

    静态资源就是直接放在项目中的资源,不需要发送请求获取
    动态资源就是需要发送请求获取资源(数据库连接数据处理)

  • 为什么静态引入图片,没有使用 require 返回的依然是编译后的文件地址?

    因为 webpack 编译 vue 文件时,遇见 src 等属性会默认使用 require 引入资源路径

    • url(./image.png) 会转为 require('./image.png')
    • <img src='./image.png' /> 会被编译为 h('img',{attrs: {src: require('./image.png')}})
  • 动态引入图片,src 后面的属性值 webpack 会认为是一个变量,根据 v-bind 指令去解析 src 的属性值,并不会通过 require 引入资源路径

  • 引入 public 下面静态资源的时候,也会默认使用 require 引入吗?

    官方:
    任何放置在public文件夹下的静态资源会被简单的复制,而不经过webpack,你需要通过绝对路径来引用它们

    答:不会,使用 require 引入资源的前提是该资源是 webpack 解析的模块,而 public 下的文件压根不会走编译,所以不会使用 require

  • 为什么使用 public 下的资源一定要用绝对路径?

    答:public 文件不会被编译返回的是代码中定义的文件地址,src 下的文件被编译,编译后生成的文件目录(dist)下会找不到对应目录

Vue2、Vue3、React 三者 diff 算法有什么区别?

  • 严格的 Tree diff 算法的时间复杂度是 O(n*3)
  • vue、react 框架对 tree diff 进行了优化 O(n)
    • 只比较同一层级,不跨级比较
    • tag 不同则删除重建
    • 子节点通过 key 区分
      • vdom diff 算法会根据 key 判断元素是否需要删除
      • 匹配了 key,则只移动元素 - 性能较好
      • 未匹配 key,则删除重建 - 性能较差
  • 区别
    • react 仅右移
    • vue2 双端比较
    • vue3 最长递增子序列

KeepAlive 组件实现原理

KeepAlive 是一个内置组件,主要用于组件缓存,它包裹的组件在切换后不会被销毁,而是保留在内存中,避免重复渲染 DOM,include/exclude 用于包含/排除组件,max 用于限制最大缓存实例个数,使用 LRU 缓存算法(最大最小使用缓存):超过最大数量时移除最久没被访问的实例

Vue-router 三种模式(React-router 也一样)

  • Hash
  • WebHistory
  • MemoryHistory(v4 之前叫做 abstract history)

如何统一监听 Vue 组件报错?

  • window.onerror

    • 全局监听所有 js 错误
    • try…catch 中的错误无法被监听到
    1
    2
    3
    4
    5
    6
    7
    window.onerror = function (msg, source, line, column, error) {
    console.log(msg, source, line, column, error);
    };
    // or
    window.addEventListener('error', (event) => {
    console.log(event);
    });
  • errorCaptured 生命周期

    • 监听所有下级组件的错误
    • 返回 false 会阻止向上传播
  • errorHandler 配置

    • Vue 全局错误监听,所有组件错误都会汇总到这里
    • 但 errorCaptured 返回 false,不会传播到这里
    1
    2
    3
    app.config.errorHandler = (err, vm, info) => {
    console.log(error, vm, info);
    };
  • 异步错误

    • 异步回调里面的错误,errorHandler 监听不到
    • 需要使用 window.onerror

vue-router 路由钩子

const router = createRouter({});

  • 全局前置守卫
    • router.beforeEach((to, from)=>{})
  • 全局解析守卫
    • router.beforeResolve:和 router.beforeEach 类似,因为它在 每次导航时都会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用
  • 全局后置钩子
    • router.afterEach((to, from)=>{})
  • 路由独享守卫
    • beforeEnter:(to, from)=>{}
  • 组件内守卫
    • onBeforeRouteLeave
    • onBeforeRouteUpdate

pinia 原理浅析

  • 通过 createPinia 创建 pinia 实例,在 app.use 的时候执行 pinia 内部的 install 方法
    • install 方法通过 vue 的 provide 将当前 pinia 实例注入到每个 vue 组件实例中
  • 在业务中使用 useXxx 的时候调用 defineStore 方法,该方法在正真调用的时候才会初始化对应模块的数据参数
    • defineStore 首先创建一个 store 对象,将 options 上面的 state、getters 利用 vue 的响应式 composition API 进行处理和转换,使之成为响应式数据并挂载在 store 对象上
    • 通过 Object.assign 对这个 store 进行一些扩展 api(reset、$patch 等)
    • 返回 store 对象作为 defineStore 方法的返回值

评论