Skip to content

Vue 3.0 ​

导航目录 ​

Vue 2 和 Vue 3 的区别 ​

1. 响应式系统 ​

Vue 2:

  • 主要通过 Object.defineProperty 劫持对象已有属性的 getter 和 setter。
  • 初始化时需要递归遍历对象并转换属性。
  • 无法直接检测对象属性的新增和删除,需要使用 Vue.set 和 Vue.delete。
  • 无法直接检测通过索引修改数组元素或修改数组长度,因此需要重写部分数组方法。

Vue 3:

  • 对象类型主要通过 Proxy现响应式,类型通过 ref 的 getter 和 setter 实现。
  • Proxy 可以统一拦截属性读取、修改、添加、删除、遍历等操作。
  • 能够直接监听对象属性增删、数组索引和长度变化。
  • 支持 Map、Set、WeakMap 和 WeakSet 等集合类型。
  • 采用惰性代理,只有访问深层对象时才继续将其转换为响应式代理。

因此,Vue 3 响应式系统的主要优势是拦截能力更加完整,并减少了 Vue 2 中的响应式边界情况。

2. API 与逻辑组织 ​

Vue 2:

  • 主要使用 Options API,通过 data、methods、computed 和 watch 等选项组织代码。
  • 同一个业务功能的状态、方法和监听逻辑可能分散在不同选项中。
  • 逻辑复用主要依赖 mixin、高阶组件和作用域插槽,容易出现命名冲突或来源不明确的问题。

Vue 3:

  • 继续支持 Options API,同时原生提供 Composition API。
  • 可以使用 setup、ref、reactive、computed 和生命周期函数,按照业务功能组织代码。
  • 可以通过组合函数封装和复用有状态。
  • 对 TypeScript 的类型推友好。

Composition API 并不是 Options API 的替代品,开发者可以根据组件复杂度和项目规范进行选择。

  1. 编译与渲染性能

Vue 3译阶段提供了更多运行时优化信息:

  • 静态提升:将不会变化的节点或属性提升到渲染函数之外,重复创建。
  • Patch Flag:标记节点中可能变化的内容,例如文本、class、style 或 props。
  • Block Tree:收集模板中的动态节点,更新时优先遍历动态节点,而遍历整棵 VNode 树。
  • 事件处理函数缓存:避免每次渲染都创建新的事件处理。
  • Tree Shaking:未使用的运行时 API 可以在构建阶段被移除。

Vue 2 的更新主要以 VNode 层级为单位进行比较;Vue 3 结合编译器提供的信息,可以缩小运行时 Diff 的范围。

4. 组件与内置能力 ​

Vue 3 增加或改进了以下能力:

  • Fragment:组件可以拥有多个根节点。
  • Teleport:可以将部分组件内容渲染到当前组件 DOM 层级之外。
  • Suspense:协调组件树中的异步依赖,并在等待期间展示 fallback 内容。
  • 多个 v-model:一个组件可以声明多个双向绑定。
  • emits 选项:显式声明组件触发的事件。
  • 异步组件 API:通过 defineAsyncComponent 统一定义异步组件。

5. 应用实例与全局 API ​

Vue :

  • 通常通过 new Vue() 创建根实例。
  • Vue.use、Vue.mixin 和 Vue.component 等全局 API 会影响同一 Vue 构造函数创建的所有应用。
  • 多个应用之间的全局配置不容易完全隔离。

Vue 3:

  • 通过 createApp() 创建应用实例。
  • 插件、全局组件和全局配置都注册在具体应用实例上。
  • 同一页面中的多个应用可以拥有相互独立的配置。

6. TypeScript 支持 ​

Vue 2:

  • 可以使用 TypeScript,但 Options API 中的 this、组件属性和插件类型推导存在一定限制。
  • vue-class-component 是一种可选方案,并不是 Vue 2 使用 TypeScript 的必要条件。

Vue 3:

  • 核心代码使用 TypeScript 编写。
  • Composition API 减少了对组件 this 的依赖。
  • 对 propsits、模板引用和组合函数等场景提供了更完善的类型推导。

7. 生命周期变化 ​

Vue 3 保留了 Options API 生命周期,同时调整了部分名称:

  • beforeDestroy 改为 beforeUnmount。
  • destroyed 改为 unmounted。
  • Composition API 中通过 onMounted、onUpdated 和 onUnmounted 等函数注册生命周期钩子。
  • setup 在组件创建阶段执行,因此通常用于替代 Composition API 场景下的 beforeCreate 和 created 逻辑。

总结 ​

Vue 3 并不只是将 Object.define 换成了 Proxy。它还重构了响应式系统和虚拟 DOM,增加了静态提升、Patch Flag 和 Block Tree 等编译时优化;同时引入 Composition API、应用实例隔离、Fragment、Teleport 等能力,并改善了 TypeScript 支持和逻辑复用方式。

生命周期 ​

Vue 3 的生命周期钩子与 Vue 2 相比有所变化,主要是通过组合式 API 调用:

js
Vue 2                ------->      Vue 3

beforeCreate         -------->      setup(()=>{})
created              -------->      setup(()=>{})
beforeMount          -------->      onBeforeMount(()=>{})
mounted              -------->      onMounted(()=>{})
beforeUpdate         -------->      onBeforeUpdate(()=>{})
updated              -------->      onUpdated(()=>{})
beforeDestroy        -------->      onBeforeUnmount(()=>{})
destroyed            -------->      onUnmounted(()=>{})
activated            -------->      onActivated(()=>{})
deactivated          -------->      onDeactivated(()=>{})
errorCaptured        -------->      onErrorCaptured(()=>{})

Vue3 渲染流程 ​

Vue 3 的渲染流程可以分为两个阶段:编译时(Compile-time) 和 运行时(Runtime)。

编译时 (Compile-Time) ​

编译时的工作主要发生在构建阶段(例如使用 vue-loader 或 Vite),它的任务是将开发者编写的模板(<template>)编译成渲染函数(render function)。 编译过程主要包括 解析 (Parse)、转换 (Transform) 和 生成 (Generate) 三个阶段。

流程步骤如下:

  1. 解析 (Parse)

    • 输入:原始的 HTML-like 模板字符串。
    • 过程:解析器将模板字符串解析成一个树状结构,这个结构称为抽象语法树 (AST - Abstract Syntax Tree)。AST 的每个节点都代表了模板中的元素、属性、文本、指令等。
    • 输出:模板 AST。
  2. 转换 (Transform)

    • 输入:模板 AST。
    • 过程:这是一个非常关键的优化步骤。Vue 的编译器会遍历 AST 并对其进行转换和优化。例如:
      • 静态提升 (Static Hoisting):识别出永远不会改变的静态节点和静态属性,并将其提升到渲染函数之外。这样在每次重渲染时,Vue 可以直接复用旧的 VNode,完全跳过 Diff 过程。
      • 补丁标志 (Patch Flags):为动态节点标记其需要更新的类型(如 TEXT, CLASS, PROPS 等)。在运行时,Vue 可以根据这些标志进行更精准的 Diff,只比较需要变化的部分,极大提升性能。
      • 树结构优化 (Tree Flattening):将动态子节点编译到一个扁平数组中,大大减少了运行时需要遍历的 VNode 数量。
    • 输出:优化后的、携带了丰富元信息的 AST。
  3. 生成 (Generate)

    • 输入:优化后的 AST。

    • 过程:代码生成器将 AST 递归地转换为 JavaScript 代码字符串,这个字符串就是一个渲染函数。

    • 输出:渲染函数的代码字符串。例如,一个简单的模板 <div>Hello</div> 会被生成类似如下的代码:

      javascript
      import { createElementVNode as _createElementVNode } from "vue";
      
      export function render(_ctx, _cache) {
        return _createElementVNode("div", null, "Hello");
      }

运行时 (Runtime) ​

运行时是 Vue 在浏览器(或其它环境)中实际执行的过程。它基于 Vue 的核心库,负责执行渲染函数、处理响应式数据、更新 DOM 等。

流程步骤如下:

  1. 创建应用实例与响应式系统

    • 通过 createApp 创建应用实例。
    • 组件中的 data、computed、props 等会被 Vue 的响应式系统(reactive, ref)包裹,变成响应式对象。
  2. 执行渲染器 (Render Effect)

    • Vue 将一个组件的渲染函数包裹在一个副作用函数 (Effect) 中。这个特殊的 Effect 被称为渲染器。
    • 依赖追踪 (Dependency Tracking):当首次执行或后续重执行渲染函数时,会访问响应式数据中的属性。这会触发属性的 get 操作,响应式系统会精确地记录下“这个渲染 Effect 依赖了当前这个属性”。
  3. 生成虚拟 DOM (VNode)

    • 执行渲染函数,它会调用一系列 createElementVNode, createTextVNode 等运行时工具函数。
    • 这些函数会根据当前的组件状态和 Props,创建并返回一个虚拟 DOM (VNode) 树。VNode 是一个普通的 JavaScript 对象,它轻量地描述了真实 DOM 应该长什么样。
  4. 挂载 / 打补丁 (Mount / Patch)

    • 首次渲染 (Mount):
      • 将上一步生成的 VNode 树传入 patch 函数。
      • patch 函数会递归地遍历 VNode 树,并根据 VNode 的描述,调用浏览器的 DOM API(如 document.createElement, appendChild)来创建真实的 DOM 节点,并挂载到页面上指定的容器中(例如 app.mount('#app'))。
    • 更新渲染 (Update):
      • 当组件依赖的响应式数据发生变化时,响应式系统会触发它收集的所有 Effect(包括渲染 Effect)重新执行。
      • 渲染 Effect 再次执行,用新的数据生成一个新的 VNode 树。
      • 将新的 VNode 树和旧的 VNode 树一起传入 patch 函数。
      • patch 函数会使用高效的 Diff 算法 对比两棵树的差异。得益于编译时提供的 Patch Flags 和 静态提升,Diff 过程非常快速和精准。
      • 找出差异后,patch 函数只会调用必要的 DOM API,最小化地更新真实 DOM。这就是 Vue 性能高的关键所在。
  5. 卸载 (Unmount)

    • 当组件需要被销毁时(例如 v-if 为 false),Vue 会再次调用 patch 函数,但这次会传入一个空的 VNode。patch 函数会递归地触发组件实例和 DOM 元素的卸载钩子,并移除真实的 DOM 节点。

reactive 数据响应式处理 ​

reactive 是利用 ES6 的 Proxy 加上发布-订阅模式来处理响应式数据的。主要流程如下:

  1. reactive 在初始化时,首先判断目标是否是对象,不是对象就直接返回
  2. 如果已经被 reactive 代理过了,直接返回代理对象
  3. 如果该对象已经被缓存过,直接取缓存的结果返回
  4. 如果以上都未命中,创建 Proxy 代理对象,并且进行缓存
js
function createReactiveObject(target, isReadonly, baseHandlers, proxyMap) {
  if (!isObject(target)) {
    // 不是对象就直接跳过
    return target;
  }

  // 如果已经被代理过了,直接返回
  // reactive(reactive(obj))
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target;
  }
  // 如果某一个对象已经被代理过,下次在进行代理,直接取缓存的结果
  // let obj = { name: "hulei" };
  // let state = reactive( obj );
  // reactive( obj );
  const existingProxy = proxyMap.get(target); // 看目标是否被代理过
  // 如果代理过,直接返回代理过的对象
  if (existingProxy) {
    return existingProxy;
  }

  // 创建代理对象
  let proxy = new Proxy(target, baseHandlers);
  proxyMap.set(target, proxy); // 存到缓存中
  return proxy;
}

取值/订阅 get ​

  1. reactive 在取值时,会触发 get 操作,执行 track 让当前对象的属性去收集渲染的 effect
  2. 如果取出的值是一个对象,需要继续走 reactive 做代理

设置/发布 set ​

  1. reactive 数据在发生变化时,会触发 set 操作,执行 trigger 通知对应的 effect 更新视图
  2. 如果是数组比较特殊,因为数组在 push 操作时,会触发二次 set 拦截:
    • 第一次会给新的索引位置新增一个值
    • 第二次改变数组的 length 长度(会被屏蔽掉)
  3. 当数组在 push 操作的时候,会触发 set 操作,因为索引是新加的,没有依赖收集过,所以需要通过数组的 length 触发 effect 更新视图

Vue 2 vs Vue 3 代理对比 ​

Vue 3 优势:

  • 针对的是对象来进行劫持,不用改写原来的对象
  • 懒递归:如果是嵌套对象,当取值的时候才会代理,提升性能
  • 可以对不存在的属性进行获取,也会走 get 方法
  • Proxy 原生支持数组,无需特殊处理

Vue 2 局限性:

  • 针对的是属性劫持,改写了原来对象
  • 一上来就递归处理所有属性,性能较差
  • 对数组的索引修改和长度变化不敏感

ref(可以让一个普通值具备响应式的能力,如果是一个对象还是利用的 reactive) ​

ref 是一个类,在实例化时会存储原始的值,通过属性访问器 set、get 来实现响应式的能力。主要流程如下:

  1. ref 内部是通过属性访问器来实现的,在初始化时会通过 createRef 创建 Ref 实例,会把原始的值存储在当前实例上,返回当前的 Ref 实例对象
  2. 当在访问实例上的 value 属性时(xxx.value),会触发依赖收集,并且返回原始的值
  3. 当在给 value 赋值时,如果值不等同,会触发当前的依赖执行,并且赋值给原始的值
js
const convert = ( v ) => ( isObject( v ) ? reactive( v ) : v );
class RefImpl {
  public _value; // 原始的值
  public __v_isRef = true; // 表示他是一个ref
  constructor( public rawValue, shallow ) {
    // 如果不是浅的,并且是对象,走reactive代理拿到结果
    this._value = shallow ? this.rawValue : convert( this.rawValue )
  }

  get value() {
    track( this, TrackOpTypes.GET, "value" );
    return this._value
  }

  set value( newValue ) {
    if ( hasChanged( this.rawValue, newValue ) ) {
      this.rawValue = newValue;
      this._value = newValue;
      trigger( this, TriggerOrTypes.UPDATE, "value", newValue, this.rawValue );
    }
  }
}

let createRef = ( value, shallow ) => {
  return new RefImpl( value, shallow );
}
// 可以把普通值变成响应式数据
function ref( value ) {
  return createRef( value, false );
}

ref 和 reactive 的区别 ​

ref:

  • 可以让普通的值具备响应式的能力
  • 通过属性访问器 get、set 拦截处理更新
  • 需要通过 .value 访问和修改值

reactive:

  • 只能代理对象,并且使用的 ES6 的 Proxy
  • 直接访问和修改属性,不需要 .value
  • 对嵌套对象会自动进行代理

toRef 作用 ​

toRef 可以解析取出 reactive 某一个属性,并且不会失去响应式的能力(也是利用属性访问器)。主要特点:

  • toRef 是一个类,在实例化时会存储原始的 target 和 key
  • 当在取 state.value 时,触发 get 取原始的值
  • 当在给 value 赋值时,会触发 set 给原始的值赋值
js
let state = reactive({ name: "hulei" });
// 对象直接解析会失去响应式
let { name } = state;
// 通过toRef会通过get、set代理的方式去访问原对象上的属性,不会失去响应式的特性
let userName = toRef(state, "name");

console.log(userName.value);

toRefs ​

toRefs 底层是调用的 toRef,给每一个属性绑定一个 toRef,实现同时解构多个值而不失去响应式能力。

js
import { reactive, toRefs } from "vue";
export default {
  setup() {
    const user = reactive({
      name: "hu.lei",
    });
    // 如果不通过toRefs会失去响应式的能力
    return {
      ...toRefs(user),
    };
  },
};
js
export function toRef(targat, key) {
  return new ObjectRefImpl(targat, key);
}

class ObjectRefImpl {
  public __v_isRef = true;
  constructor(public targat, public key) {}

  get value() {
    return this.targat[this.key];
  }
  set value(value) {
    this.targat[this.key] = value;
  }
}

export function toRefs(targat) {
  let res = isArray(targat) ? new Array(targat.length) : {};
  for (const key in targat) {
    res[key] = toRef(targat, key);
  }
  return res;
}

vnode 中的 shapeFlag 位运算 ​

1. 位运算的核心作用 ​

位运算在 shapeFlag 属性中实现了 多类型标记的合并与快速判断。通过按位操作:

  • 合并类型:使用 |(按位或)将父节点类型和子节点类型合并到一个整数中
  • 判断类型:后续使用 &(按位与)快速检查节点类型

2. 具体实现分析 ​

(1) 节点初始类型标记 ​

javascript
let shapeFlag = isString(type)
  ? ShapeFlags.ELEMENT // 普通HTML元素
  : isObject(type)
  ? ShapeFlags.STATEFUL_COMPONENT // 有状态组件
  : 0;

假设枚举值:

javascript
const ShapeFlags = {
  ELEMENT: 1, // 二进制 0001
  STATEFUL_COMPONENT: 1 << 1, // 二进制 0010 (左移1位)
  TEXT_CHILDREN: 1 << 2, // 0100
  ARRAY_CHILDREN: 1 << 3, // 1000
  SLOTS_CHILDREN: 1 << 4, // 10000
};

(2) 子节点类型合并 ​

javascript
function normalizeChildren(vnode, children) {
  let type = 0;
  if (isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN; // 1000
  } else if (isObject(children)) {
    type = ShapeFlags.SLOTS_CHILDREN; // 10000
  } else {
    type = ShapeFlags.TEXT_CHILDREN; // 0100
  }
  // 关键位运算:合并父节点和子节点类型
  vnode.shapeFlag = vnode.shapeFlag | type;
}

(3) 位运算示例 ​

假设创建组件节点(STATEFUL_COMPONENT)带数组子节点:

javascript
初始 shapeFlag = STATEFUL_COMPONENT = 0010 (二进制)
子节点类型 = ARRAY_CHILDREN = 1000 (二进制)

合并操作:0010 | 1000 = 1010 (十进制10)

3. 位运算的优势 ​

(1) 高效类型检查 ​

后续代码可通过位运算快速判断类型:

javascript
// 检查是否是组件
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
  // 0010 & 1010 = 0010 (true)
}

// 检查是否有数组子节点
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  // 1000 & 1010 = 1000 (true)
}

// 检查是否是文本节点 (0100 & 1010 = 0000 false)

effect(渲染的 effect、计算属性的 effect、watch effect) ​

渲染的 effect ​

  1. 组件在挂载的时候,会把渲染的控制权交给 effect
  2. 组件在渲染的时候,会调用 render 生成 Vnode,如果有取值操作,会触发 get 拦截,收集当前的 effect
  3. 当响应式数据发生变化时,会触发 set 通知 effect 更新视图

watch effect ​

  1. 在调用 watch 时,首先会进行取值,取值的时候会收集 watch effect
  2. 当响应式数据发生变化时,会触发 set 通知 watch effect 执行监听的回调

Vue watch、watchEffect 监听 ​

watch 工作原理:

  1. watch 在初始化时,会执行 doWatch,构建一个 effect,将控制权交给 effect
  2. effect 会立即执行 watch 函数 获取到监听的值,在取值的时候会触发 get,收集当前的 effect
  3. 当响应式数据发生变化时,会触发 set 通知 effect 执行自定义的 schedular 函数中的 watch 监听的回调
  4. effect 更新是异步的,一个值同步修改多次,只会执行一次更新,利用事件循环机制

watchEffect ​

  • 没有监听的回调函数
  • 默认一定会执行一次
  • 值发生变化会直接执行 watch 函数
js
/**
 *
 * @param source 需要监听的数据(可能是函数,有可以是值)
 * @param cb 数据变化后的回调
 * @param options 配置项
 * @returns
 */

// watch( () => count, ( newValue, oldValue ) => {
// }, {
//   deep: true,
//   immediate: true
// } );
function watch(source, cb, options = {}) {
  return doWatch(source, cb, options);
}

// 默认会执行一次
function watchEffect(source) {
  return doWatch(source, null, {});
}

function doWatch(source, cb, options) {
  // 如果不是函数包装成函数,方便后续处理
  let sourceHandler = source;
  if (!isFunction(source)) {
    sourceHandler = () => source;
  }
  let oldValue = ""; // 老值
  // 自定义effect执行器
  let schedular = () => {
    if (cb) {
      // 或者最新的值
      let newValue = runner();
      // 如果新老值不一样,执行回调
      if (hasChanged(oldValue, newValue)) {
        cb(newValue, oldValue);
        oldValue = newValue;
      }
    } else {
      // watchEffect没有回调,直接执行source
      sourceHandler();
    }
  };
  let runner = effect(sourceHandler, {
    lazy: true,
    schedular: () => {
      // 如果数据同一时间多次变更,会触发effect多次执行,我们只需要执行一次即可
      queueJob(schedular);
    },
  });

  // 是否默认执行一次watch回调
  if (options?.immediate) {
    schedular();
  }
  // 或者监听的值,同时触发收集当前的effect,当监听的值发生变化时通知effect执行回调
  oldValue = runner();
}

Vue computed 计算属性 ​

计算属性是一个类,具有缓存特性。主要工作原理如下:

  1. 在初始化时,首先会初始化缓存的状态 dirty,会创建一个 lazy 的 effect(不会立马执行),并且把 getter 控制权交给 effect
  2. 当在取 state.value 时,触发 get 拦截,如果没有缓存会调用 effect 获取的计算属性的值,响应式数据会通过 track 收集当前的计算属性 effect,同时会让计算实例收集渲染的 effect
  3. 当响应式数据发生变化时,触发 set 通知 effect 执行自定义的 schedular,在 schedular 中取消缓存,通过执行 trigger 触发渲染的 effect 更新视图
js
function computed( handler ) {
  let getter;
  let setter;
  if ( typeof handler === 'function' ) {
    getter = handler
    setter = () => {
      console.warn( `computed not set` );
    }
  } else if ( isObject( handler ) ) {
    getter = handler.set
    setter = handler.get
  } else {
    return console.warn( `computed It must be a function or an object` );
  }
  return new ComputedRefImpl( getter, setter );
}

class ComputedRefImpl {
  public _value;
  public effect;
  public _dirty = true;
  constructor( public getter, public setter ) {
    this.effect = effect( getter, {
      lazy: true,// 在取值的时候,才执行getter
      schedular: () => {
        // 1、计算属性响应式数据发生改变后,取消缓存
        this._dirty = true
        // 2、通知渲染的effect更新视图
        trigger( this, TriggerOrTypes.UPDATE, "value", this._value );
      }
    } )
  }

  get value() {
    // 是否被缓存
    if ( this._dirty ) {
      // 用户在取值的时候,会通过effect调用计算属性的getter方法,让响应式数据去收集计算属性effect
      this._value = this.effect()
      // 缓存值
      this._dirty = false
    }
    // 计算属性收集渲染的effect
    track( this, TrackOpTypes.GET, "value" );
    return this._value
  }

  set value( newValue ) {
    this.setter( newValue )
  }
}

Vue Props 处理 ​

Vue 3 中 Props 的处理流程如下:

  1. 子组件在创建 createVNode 时,如果父组件有传递数据,会将数据存储在 Vnode props 对象中
  2. 子组件在创建组件实例对象时,会把自己接受的 props 数据,存储在实例 propsOptions 对象中
  3. 子组件在渲染的时候,会调用 initProps,如果父组件传递的数据在子组件中有定义,会存储在组件实例 props 中
  4. 如果是未接收的属性,将全部存储在组件实例attrs上
js

const VueComponent = {
  props: {
    name: String,
    age: Number,
  },
  // ...
};

let app = createApp({
  render: () => {
    return h(VueComponent, {
      name: "hulei",
      age: 18,
      add: "上海",
    });
  },
});
app.mount("#app");

// h=> createVNode( type, propsOrChildren, children );
function createVNode( type, props, children = null ) {
  const vnode = {
    __v_isVnode: true,
    type, // 此时是一个(VueComponent)组件
    props, // 子组件在创建createVNode时,如果父组件有传递数据,会将数据存储在Vnode props对象中
    children,
    component: null,
    el: null,
    key: props?.key,
    shapeFlag,
  };

  normalizeChildren( vnode, children );
  return vnode;
};

const instance = {
  uuid: uuid++,
  __v_isVNode: true,
  vnode, // 组件对应的虚拟节点
  subTree: null,
  type, // 组件对象
  propsOptions: vnode.type.props || {}, // 子组件在创建组件实例对象时,会把自己接受的props数据,存储在 实例propsOptionse对象中
  ctx: {} as any,
  props: {}, // 组件的属性
  attrs: {},
  slots: {},
  setupState: {},
  isMounted: false,
  exposed: {},
  parent,
  provides:parent ? parent.provides : Object.create(null)
};
js
// 解析vnode将数据挂载到组件实例上
function setupComponent(instance) {
  let { props, children } = instance.vnode;
  // 如果子组件有接收props,将会实例在props上,否则全部在实例attrs上
  initProps(instance, props);
}
const initProps = (instance, userProps) => {
  const attrs = {};
  const props = {};
  const options = instance.propsOptions || {}; // 子组件上接受的props
  // 父组件传递的props
  if (userProps) {
    for (let key in userProps) {
      const value = userProps[key];
      // 如果子组件有接受作为props,否则作为attrs
      if (key in options) {
        props[key] = value;
      } else {
        attrs[key] = value;
      }
    }
  }
  instance.attrs = attrs;
  instance.props = reactive(props);
};

emits 原理 ​

emits 的作用主要是让子组件能够调用父组件的函数。工作原理如下:

  1. 子组件在初始化时,非 props 的接收的属性,将全部存储在子组件实例attrs上
  2. 当子组件触发 emit 函数时,通过函数名称在子组件实例attrs上去查找,如果有就执行父组件的函数(其实就是发布/订阅模式)
js
// 构建setup 下上文content
// setup(props, content) {
//    const { attrs, slots, emit, expose } = content
//  }
<Com onEvent="fn" />;
emit("event");
function createSetupContext(instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: (eventName, ...args) => {
      // event => onEvent
      let bindName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
      const handler = instance.attrs[bindName];

      if (handler) {
        let handlers = Array.isArray(handler) ? handler : [handler];
        handlers.forEach((handler) => handler(...args));
      }
    },
    expose: () => {},
  };
}

expose 实例原理 ​

expose 的作用是将子组件的属性或者函数暴露出去,让父组件可以通过 ref 去调用。工作原理如下:

  1. 当子组件在调用 expose 时,其实就是把需要提供出去的属性,存储在自己实例 exposed 属性上
  2. 父组件可以通过 ref 获取到子组件的实例,从而可以调用子组件暴露的函数和属性
js
setup(props, { attrs, slots, emit,expose }) {
  const sava = ()=>{
          alert('expose')
  }
  // 暴露自己的函数
    expose({
      sava
    })
}

// 构建setup 下上文content
// setup(props, content) {
//    const { attrs, slots, emit, expose } = content
//  }
function createSetupContext(instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: (eventName, ...args) => {
      let bindName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
      const handler = instance.attrs[bindName];

      if (handler) {
        let handlers = Array.isArray(handler) ? handler : [handler];
        handlers.forEach((handler) => handler(...args));
      }
    },
    expose(exposed) {
      // 主要用于ref ,通过ref获取组件的时候 在vue里只能获取到组件实例,但是在vue3中如果提供了
      // exposed 则获取的就是exposed属性
      instance.exposed = exposed;
    },
  };
}

slot 插槽原理 ​

Vue 3 中,插槽本质是父组件传给子组件的一组函数,调用后返回虚拟节点。

  1. 编译父组件模板时,编译器会把插槽转换为插槽函数,并组成 slots 对象。
  2. 默认插槽对应 default 属性,具名插槽以插槽名称作为属性。
  3. 创建组件 VNode 时,如果 children 是插槽对象,会通过 shapeFlag 标记为插槽子节点。注意,该判断针对组件 VNode。
  4. 创建组件实例时,会通过 initSlots 将 children 规范化并保存到 instance.slots。
  5. slots 会通过 setup 上下文暴露给子组件;组件更新时,还会通过 updateSlots 同步最新插槽。
  6. 组件渲染到插槽出口时,才调用对应插槽函数,获得 VNode 并参与 patch。
js
let { reactive, createApp, h } = VueRuntimeDOM;

const VueComponent = {
  setup(props, { slots }) {
    return () => {
      return h("div", [
        h("div", slots.default()),
        h("div", slots.header()),
        h("div", slots.main()),
        h("div", slots.footer()),
      ]);
    };
  },
};

let app = createApp({
  render: () => {
    return h(VueComponent, null, {
      default: () => {
        return h("a", "default");
      },
      header: () => {
        return h("a", "hello");
      },
      main: () => {
        return h("a", "vue");
      },
      footer: () => {
        return h("a", "hulei");
      },
    });
  },
});
app.mount("#app");
js
// 虚拟节点的子节点处理
function normalizeChildren(vnode, children) {
  let type = 0;
  if (children == null) {
  } else if (isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN;
  } else if (isObject(children)) {
    // 子节点是插槽,标记
    type = ShapeFlags.SLOTS_CHILDREN;
  } else {
    type = ShapeFlags.TEXT_CHILDREN;
  }
  vnode.shapeFlag = vnode.shapeFlag | type;
}
js
// 如果有插槽,将插槽存储在组件实例slots上
const initSlots = (instance, children) => {
  if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    // 包含插槽 则说明children 是插槽
    instance.slots = children; // 将用户的children 映射到实例上
  }
};

// 解析vnode将数据挂载到组件实例上
function setupComponent(instance) {
  let { props, children } = instance.vnode;
  initSlots(instance, children); // 插槽的解析
}
js
// 构建setup 下上文content
// setup(props, content) {
//    const { attrs, slots, emit, expose } = content
//  }
function createSetupContext(instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: (eventName, ...args) => {
      let bindName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
      const handler = instance.attrs[bindName];

      if (handler) {
        let handlers = Array.isArray(handler) ? handler : [handler];
        handlers.forEach((handler) => handler(...args));
      }
    },
    expose(exposed) {
      // 主要用于ref ,通过ref获取组件的时候 在vue里只能获取到组件实例,但是在vue3中如果提供了
      // exposed 则获取的就是exposed属性
      instance.exposed = exposed;
    },
  };
}

provide or inject ​

provide 和 inject 用于祖先组件与后代组件之间通信,可以避免一层一层地传递 props。

  • 祖先组件通过 provide 提供数据。
  • 任意后代组件通过 inject 获取数据。
  • 中间经过多少层组件都没有关系。

实现原理 ​

    1. 每个组件实例都有一个 provides 属性,用来保存祖先或当前组件提供的数据。
    1. 子组件创建时,最初会直接使用父组件的 provides。
    1. 当组件第一次调用 provide() 时,Vue 会创建一个新的 provides 对象,并让它继承父组件的 provides,然后把当前提供的数据存进去。
    1. 后代组件调用 inject() 时,会从父组件的 provides 中查找数据。如果父组件没有,就继续沿着原型链向上查找,直到找到某个祖先提供的数据。
    1. 如果多个祖先提供了相同的 key,后代组件会获取距离自己最近的值;如果始终没有找到,则返回默认值或 undefined。

另外,provide/inject 本身不会将普通数据变成响应式数据。如果需要数据变化后视图同步更新,应提供 ref 或 reactive。

js
// 组件实例
const instance = {
  // ....
  parent, // 标记当前组件的父亲是谁
  provides: parent ? parent.provides : Object.create(null),
};
js
import { currentInstance as instance } from "./component";
/**
 * 内容提供者
 * @param key 消费的key
 * @param value 消费的值
 * @returns
 */
export function provide(key, value) {
  if (!instance) return;
  let parentProvides = instance.parent && instance.parent.provides; // 父组件的provides
  let currentProvides = instance.provides; // 当前节点的provides

  if (currentProvides === parentProvides) {
    currentProvides = instance.provides = Object.create(parentProvides);
  }
  currentProvides[key] = value;
}

/**
 * 消费者
 * @param key 内容提供者key
 * @param defaultValue 默认值
 * @returns
 */
export function inject(key, defaultValue) {
  if (!instance) return;
  // 取组件提供的provide,如果没有返回默认值
  const provides = instance.parent?.provides;
  if (provides && key in provides) {
    return provides[key];
  } else {
    return defaultValue;
  }
}

Vue diff ​

Vue 3 的 Diff 用来比较同一层级的新旧子节点,不会直接跨层级比较。优化策略有 3 种:头头、尾尾、乱序。

比较流程:

    1. 从头比较:依次比较新旧列表头部。若节点的 type 和 key 相同,则复用并更新该节点,然后继续比较下一个。
    1. 从尾比较:依次比较新旧列表尾部。节点相同则复用并更新,然后继续向前比较。
    1. 新增节点:旧列表先遍历完,说明新列表剩余的节点都是新增节点,将它们挂载到正确位置。
    1. 删除节点:新列表先遍历完,说明旧列表剩余的节点已经无用,将它们卸载。
    1. 处理乱序节点:如果两边都有剩余节点,Vue 会为新节点建立 key → 索引 映射,再遍历旧节点:能找到对应新节点则复用并更新,找不到则删除。
    1. 减少移动:Vue 根据复用关系计算最长递增子序列。序列中的节点位置相对稳定,无须移动;其他节点移动到正确位置,新出现的节点则直接创建。

因此,Vue 3 Diff 的核心可以概括为:同层比较、首尾预处理、key 映射复用、最长递增子序列减少 DOM 移动。建议不要描述为“单指针”,因为首尾预处理实际使用了新旧列表的起止索引

1、头头对比 ​

diff

2、尾尾对比 ​

diff

3、乱序对比 ​

diff

生命周期 (life cycle) ​

Vue 3 的生命周期是通过发布/订阅模式来实现收集和触发的。主要特点:

  1. Vue 3 生命周期必须在 setup 中使用,并且生命周期 hook 存储在当前组件的实例上
  2. 组件在初始化时,如果有 setup 函数,会存储当前组件的实例,并且会导出
  3. 在执行 setup 函数时,如果有钩子函数,会进行分类,并且生命周期 hook 会存储在当前组件的实例上
  4. 当组件渲染到某一个阶段时,会从当前组件的实例上去查找对应的生命周期 hook 并且执行

内部执行流程:

  1. 首先调用 createHook 将函数进行分类,并且返回生命周期钩子函数
  2. 当生命周期钩子函数在执行时,会传递一个 hook 函数,还有一个默认值(当前组件实例)
  3. 此时会调用 injectHook 进行收集,并且会传递三个参数(生命周期类型、hook 函数、当前组件实例)
  4. injectHook 内部会看当前实例上,有没有同类型的 hook,如果没有先创建再新增,如果有直接新增
  5. 因为 hook 在执行时,始终需要获取到当前组件的实例,这里 hook 在保存的时候会进行 AOP 拦截,在 hook 执行前,设置正确的组件实例,当 hook 执行完成后,将当前的实例设置为 null
js
import { currentInstance, setCurrentInstance } from "./component";

const enum LifeCycles {
  BEFORE_MOUNT = "bm", // 组件渲染前
  MOUNTED = "m", // 组件渲染后
  BEFORE_UPDATE = "bu", // 组件更新前
  UPDATED = "u", // 组件更新后
}
// 生命周期只能在setup中使用
// 因为只有在调用setup时才会生成组件的实例,currentInstance,并且生命周期挂载组件实例上

// 订阅
function createHook( lifeCycle ) {
  /**
   * hook v钩子类型
   * currentInstance 当前组件的实例
   */
  return ( hook, target = currentInstance ) => {
    injectHook( lifeCycle, hook, target );
  };
}

function injectHook( lifecycle, hook, target ) {
  if ( !target ) return;
  // 一个组件同一类型生命周期可以多个
  const hooks = target[lifecycle] || ( target[lifecycle] = [] );
  const wrap = () => {
    setCurrentInstance( target );
    hook();
    setCurrentInstance( null );
  };
  hooks.push( wrap );
}

// 发布
export function invokeArrayFns( fns ) {
  fns.forEach( ( fn ) => fn() );
}

export const onBeforeMount = createHook( LifeCycles.BEFORE_MOUNT );
export const onMounted = createHook( LifeCycles.MOUNTED );
export const onBeforeUpdate = createHook( LifeCycles.BEFORE_UPDATE );
export const onUpdated = createHook( LifeCycles.UPDATED );

keepAlive ​

Vue 3 的 KeepAlive 组件通过缓存组件实例实现组件状态保留,其核心原理如下:

一、核心机制 ​

  1. 缓存对象

    • 内部维护一个 Map 缓存对象:key → VNode/ComponentInstance
    • 默认使用组件的 name + props 生成唯一 key,也可手动指定 key
  2. LRU 缓存策略(最近最少使用)

    • 当缓存数量超过 max(默认 10)时,自动移除最久未使用的实例
    • 每次访问缓存时,将当前实例移到最新位置

二、工作流程 ​

组件激活(切换显示) ​

KeepAlive1.png

组件失活(切换隐藏) ​

KeepAlive1.png

三、关键技术点 ​

  1. 虚拟节点标记

    • 给 VNode 打上 __isKeepAlive: true 标记
    • 渲染器识别此标记时执行特殊缓存逻辑
  2. 生命周期钩子

    javascript
    onActivated(() => {
      // 组件激活时调用(从缓存恢复)
    });
    
    onDeactivated(() => {
      // 组件失活时调用(进入缓存)
    });
  3. 缓存管理函数

    typescript
    // 伪代码实现
    const cache = new Map();
    const keys = new Set(); // LRU 队列
    
    function pruneCacheEntry(key) {
      const cached = cache.get(key);
      // 执行卸载钩子并销毁实例
      unmount(cached);
      cache.delete(key);
      keys.delete(key);
    }

四、缓存控制参数 ​

参数作用示例
include仅缓存匹配组件:include="['TabView']"
exclude排除缓存组件:exclude="['Admin']"
max最大缓存实例数(LRU 自动淘汰):max="5"
js
import { onMounted, onUpdated } from "./apiLifecycle";
import { getCurrentInstance } from "./component";
import { ShapeFlags } from "@vue/shared";

function resetFlag(vnode) {
  if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
    vnode.shapeFlag -= ShapeFlags.COMPONENT_KEPT_ALIVE;
  }
  if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    vnode.shapeFlag -= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
  }
}
export const KeepAlive = {
  __isKeepAlive: true,
  props: {
    max: {},
  },
  setup(props, { slots }) {
    // dom操作api都在instance.ctx.renderer上面
    const instance = getCurrentInstance();
    let { createElement, move } = instance.ctx.renderer;
    const keys = new Set(); // 缓存组件的key
    const cache = new Map(); // 缓存组件的映射关系
    const pruneCacheEntry = (vnode) => {
      const subTree = cache.get(vnode);
      resetFlag(subTree); // 移除keep-alive标记
      cache.delete(vnode);
      keys.delete(vnode);
    };
    let storageContainer = createElement("div");

    // 有缓存
    instance.ctx.active = (n2, container) => {
      move(n2, container);
    };

    instance.ctx.deactivate = (n1) => {
      // 组件卸载的时候 会将虚拟节点对应的真实节点,移动到容器中
      move(n1, storageContainer);
    };

    let pendingCacheKey = null;

    const cacheSubTree = () => {
      cache.set(pendingCacheKey, instance.subTree);
    };
    onMounted(cacheSubTree);
    onUpdated(cacheSubTree);

    return () => {
      let vnode = slots.default();
      // 不是组件就不用缓存了
      if (!(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)) {
        return vnode;
      }
      let comp = vnode.type;
      // 组件的名字  找 name,找key ,找组件本身
      let key = vnode.key == null ? comp : vnode.key;
      pendingCacheKey = key;
      let cacheVnode = cache.get(key);
      if (cacheVnode) {
        // 走到缓存里需要干什么?
        vnode.component = cacheVnode.component; // 复用组件
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE; // 组件走缓存的时候不要初始化
      } else {
        keys.add(key);
        let { max } = props;
        if (max && keys.size > max) {
          // 删除第一个元素 ,在最后增加
          // next 返回的是一个对象 {value,done}
          pruneCacheEntry(keys.values().next().value);
        }
        // 获取到了虚拟节点
      }
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE; // 用来告诉这个vnode稍后卸载的时候 应该缓存起来
      // 组件还是会重新创建, 会走mountComponent
      return vnode;
    };
  },
};
js
// -------------------组件----------------------
// 走缓存
function processComponent(n1, n2, container, parent) {
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      // 在第一次卸载的时候,我们已经将dom元素移动到内存中,这次渲染我们再将他拿回来
      parent.ctx.active(n2, container);
    } else {
      // 初次渲染
      mountComponent(n2, container, parent);
    }
  } else {
    console.log("组件更新");
  }
}
js
// 删除老的节点
function unmount(n1, parent) {
  let { shapeFlag } = n1;
  // 被缓存的组件不会真实卸载,而是存储在内存中,下次渲染的时候,直接在内存中取
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    parent.ctx.deactivate(n1);
  } else {
    // 如果是组件 调用的组件的生命周期等(组件情况)
    hostRemove(n1.el);
  }
}

nextTick ​

Vue 中视图更新是异步的,使用 nextTick 方法可以保证用户定义的逻辑在更新之后执行,可用于获取更新后的 DOM。主要特点:

  • 多次调用 nextTick 会被合并,避免重复执行
  • 在调用 nextTick 时,不会立马执行回调,而是存储在队列中
  • 同步任务全部执行完毕后,批量更新,DOM 更新的任务也是调用的 nextTick
js
// 简化版 Vue 3 的 nextTick 源码
const schedulerQueue = [];
let isFlushing = false;
function flushSchedulerQueue() {
  isFlushing = true;
  const currentQueue = schedulerQueue.slice();
  schedulerQueue.length = 0;
  isFlushing = false;

  for (const job of currentQueue) {
    job();
  }
}

function queueJob(job) {
  if (!schedulerQueue.includes(job)) {
    schedulerQueue.push(job);
    if (!isFlushing) {
      Promise.resolve().then(flushSchedulerQueue);
    }
  }
}

function nextTick(callback) {
  queueJob(callback);
}

// 示例用法
nextTick(() => {
  console.log("Next tick callback");
});
console.log("End of main script");

nextTick 一定会拿到更新之后的 DOM 属性吗 ​

答案是不确定的,这取决于 nextTick 的调用时机,是否在响应式数据发生改变之后。

原理:

  • 响应式数据发生改变后,会触发视图异步更新,底层也是调用的 nextTick 加入到队列中
  • 如果 nextTick 调用时机在响应式数据发生改变之前,会在视图更新之前执行,所以获取 domRef.value.innerHTML,获取到的还是老的值
js
const domRef = ref(null);
const state = ref(0);
nextTick(() => {
  console.log(domRef.value.innerHTML); // 0
});
state.value = 1;
nextTick(() => {
  console.log(domRef.value.innerHTML); // 1
});

vue2 为什么只能有一个根节点,vue3 可以多根节点? ​

  • 因为vue2vdom 是一个单根树形结构描述当前视图结构,patch 方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也是会转换成 vdom,所以也必须满足单根节点要求

  • 因为vue3引入了 fragment 概念,这是一个抽象的节点,如果发现组件是多根的会自动创建一个 fragment 节点,把多根节点视为自己的 children。在 patch 时,如果发现这是一个 fragment 节点,则直接遍历 children 创建或更新

Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异? ​

  • 主要是减少DOM的操作,响应式数据发生改变时,组件会生成新的虚拟 DOM,对比两株虚拟 DOM 树的变更差异将更新补丁作用于真实 DOM,以最小成本完成视图更新。

v-if&v-show ​

  • v-if如果条件不成立,不会渲染当前指令所在的 DOM,并且条件为false时组件内部的逻辑不会执行
  • v-show不管条件是否成立,都会渲染当前指令所在的 DOM,映射到 DOM 就是 displayblock、none,都会执行组件内部逻辑
js
// template
<div v-if="true">if的ast</div>
<div v-show="true">show的ast</div>

// template=>ast
(_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    true
      ? (_openBlock(), _createElementBlock("div", _hoisted_1, "if的ast"))
      : _createCommentVNode("v-if", true),
    _withDirectives(_createElementVNode("div", null, "show的ast", 512 /* NEED_PATCH */), [
      [_vShow, true]
    ])
  ], 64 /* STABLE_FRAGMENT */))
}

v-if、v-for 优先级 ​

  • 在 Vue2 中解析时,先解析v-for,在解析v-if。会导致先循环后在对每一项进行判断,浪费性能
  • 在 Vue3 中 vif 的优先级高于 v-for。

key 的作用 ​

  • vue 中 key 的主要作用就是,数据在发生变化后,可以通过 key 和 tag 节点类型,对比新老 Vnode,可以复用老的节点
  • 尽量不要使用索引到做 key

Vue.use 是干什么的 ​

  • 主要是提供 vue 插件功能,添加全局的组件、指令等等,
  • 在提供插件的时候,需要注册一个对象,并且需要有install函数

Vue 组件 data 为什么需要是一个函数 ​

  • 根实例对象 data 可以是对象也可以是函数单例,不会产生数据污染情况
  • 组件实例对象 data 必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染.所以需要通过工厂函数返回全新的 data 作为组件的数据源
js
function Vue() {}

Vue.extend = function (options) {
  function Sub() {
    this.data = this.constructor.options.data;
  }
  Sub.options = options;
  return Sub;
};

let Child = Vue.extend({
  data: { name: "hl" },
});

let child1 = new Child();
let child2 = new Child();

console.log(child1.data.name); // hl
child1.data.name = "lh";
console.log("child1", child1.data.name); // lh
console.log("child2", child2.data.name); // lh

v-once 的使用场景有哪些 ​

  • v-once是 vue 中内置指令,只渲染元素和组件一次,随后的重新渲染,元素/组件及其所有的子节点将被当作为静态节点直接跳过更新对比。这可以用于优化更新性能
  • vue3.2 之后,增加了 v-memo 指令,通过依赖列表的方式控制页面渲染

双向绑定实现原理 ​

  • 在 vue 中双向绑定靠的的v-model指令

    1. 模板编译时,Vue 根据元素类型生成属性绑定和事件监听。
    1. 数据变化后,响应式系统触发组件更新,将新值同步到表单元素。
    1. 用户操作表单时触发事件,事件处理函数把最新值写回响应式数据。
    1. 数据更新后再次驱动视图渲染,形成双向同步。

不同表单元素的转换方式不同:文本框通常使用 value + input,复选框和单选框使用 checked + change,下拉框使用 value + change。.lazy、.trim、.number 等修饰符会进一步改变事件或写入值。

Vue 中.sync 修饰符的作用 ​

TIP

vue3 中 sync 被移除

  • 在有些情况下,我们可能需要对一个 prop 进行双向绑定,这时可以使用.sync来实现。v-mode1 默认只双向绑定一个属性,这里就可以通过.sync 修饰符绑定多个属性

自定义指令 ​

  • 方便开发人员对指令的扩展

指令的生命周期 ​

  • bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中),
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前,指令的值可能发生了改变,也可能没有。
  • componentupdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind: 只调用一次,指令与元素解绑时调用。

Vue 中使用了哪些设计模式 ​

  • 单例模式-单例模式就是整个程序有且仅有一个实例 Vuex 中的 store。工厂模式-传入参数即可创建实例(createElement)
  • 发布订阅模式-订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
  • 观察者模式- watcher&dep 的关系
  • 代理模式-代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用
  • 中介者模式。中介者是一个行为设计模式通过提供一个统一的接口让系统的不同部分进行通信。 vuex。策略模式- 策路模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案, mergeOption。外观模式-提供了统一的接口,用来访问子系统中的一群接口。

Vue 中的性能优化有哪些 ​

  • 数据层级不易过深,合理设置响应式数据
  • 通过 Object.freeze()方法冻结属性
  • 使用数据时缓存值的结果,不频繁取值。
  • 合理设置 Key 属性
  • v-show 和 v-if 的选取
  • 控制组件粒度 -> Vue 采用组件级更新
  • 采用函数式组件->函数式组件开销低
  • 采用异步组件-> 借助 webpack 分包的能力
  • 使用 keep-alive 缓存组件 v-once
  • 分页、虚拟滚动、时间分片等策略

单页应用首屏加载速度慢的怎么解决 ​

  • 使用路由懒加载、异步组件,实现组件拆分,减少入口文件体积大小(优化体验骨架屏)
  • 抽离公共代码,采用 splitChunks 进行代码分割。
  • 组件加载采用按需加载的方式。
  • 静态资源缓存,采用 HTTP 缓存 (强制缓存、对比缓存) 、使用 localStorage 实现缓存资源。
  • 图片资源的压缩,雪碧图、对小图片进行 base64 减少 http 请求。
  • 打包时开启 gzip 压缩处理 compression-webpack-plugin 插件
  • 静态资源采用 CDN 提速。终极的手段
  • 使用 SSR 对首屏做服务端渲染。