Skip to content

vue3.0

Vue 2 和 Vue 3 的区别

1. 响应式系统

  • Vue 2
    • 使用 Object.defineProperty 实现响应式
    • 缺点:无法检测对象属性的动态添加/删除,需用 Vue.set/Vue.delete;对数组的索引修改和长度变化不敏感。
  • Vue 3
    • 基于 Proxy 重构响应式系统
    • 优势:支持动态属性增删、数组索引修改;性能更高;减少边界情况处理。

2. Composition API

  • Vue 2
    • 使用 Options API(选项式)data, methods, computed 等分离的选项)
    • 缺点:逻辑分散,大型组件维护困难。
  • Vue 3
    • 引入 Composition API(组合式)setup 函数 + 响应式函数)
    • 优势
      • 逻辑复用更灵活(类似 React Hooks);
      • 代码按功能组织,而非分散到选项中;
      • 更好的 TypeScript 支持。

3. 性能优化

  • Vue 3 改进
    • Tree-shaking 支持:核心库体积更小(约 10KB gzipped),未使用的功能不打包。
    • 虚拟 DOM 重写
      • 编译时优化:标记静态节点,跳过 diff 过程;
      • 动态节点标记(Patch Flags):仅追踪动态内容。
    • SSR 优化:提升服务端渲染性能。

4. 新特性

  • Fragment(片段)
    • Vue 2:组件必须单根节点。
    • Vue 3:支持多根节点组件。
  • Teleport(传送门)
    • 将组件渲染到 DOM 任意位置(如模态框、通知)。
  • Suspense(异步组件)
    • 优雅处理异步组件加载状态(显示占位内容)。

5. TypeScript 支持

  • Vue 2:通过 vue-class-component 支持,类型推断较弱。
  • Vue 3完全用 TypeScript 重写,提供更完善的类型定义。

生命周期

js
vue2           ------->      vue3

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

Vue 渲染流程

Vue3 的渲染流程是一个高效且优化的过程,结合了响应式系统、虚拟 DOM 和编译时优化。以下是其详细步骤:

1. 应用初始化

  • 创建应用实例:通过 createApp 方法创建应用实例,配置全局组件、指令、插件等。
  • 挂载根组件:调用 app.mount('#app'),指定挂载的目标 DOM 元素。
js
import { createApp } from "vue";
import App from "./App.vue";

// 创建应用实例
const app = createApp(App);

// 配置全局资源
app.component("MyComponent", MyComponent);
app.directive("focus", FocusDirective);

// 挂载根组件
app.mount("#app");

2. 组件实例化

  • 1、创建组件实例对象
  • 2、解析 props 和 slots
  • 3、执行 setup() 函数(Composition API 入口)
  • 4、合并 Options API(data/computed/methods)
  • 5、建立渲染上下文(包含访问代理)
js
// 伪代码展示组件实例创建
function createComponentInstance(vnode) {
  const instance = {
    uid: uid++,
    vnode,
    type: vnode.type,
    props: {},
    setupState: null,
    ctx: {}, // 渲染上下文
    isMounted: false,
  };

  // 初始化 props 和 slots
  initProps(instance, vnode.props);
  initSlots(instance, vnode.children);

  return instance;
}

3. 响应式系统建立

  • 1、通过 Proxy 实现细粒度响应式
  • 2、渲染函数作为 Reactive Effect 注册
  • 3、依赖收集:数据访问时触发 getter 收集依赖
  • 4、触发更新:数据修改时通过 setter 通知更新

4. 编译阶段优化

  • 静态提升:将静态节点提取为常量
  • 补丁标志:动态节点标记更新类型
  • 区块树:动态节点组织为树结构
  • 预字符串化:连续静态内容转为字符串
js
// 编译前模板
<div>
  <span>静态内容</span>
  <span>{{ dynamicText }}</span>
</div>;

// 编译后渲染函数
import { createVNode as _createVNode } from "vue";

const _hoisted_1 = _createVNode("span", null, "静态内容");

function render() {
  return _createVNode("div", null, [
    _hoisted_1, // 静态提升
    _createVNode("span", null, dynamicText, 1 /* TEXT */),
  ]);
}

5. 生成虚拟 DOM(VNode)

  • 执行渲染函数:运行组件渲染函数(用户编写或编译生成),生成 VNode 树。
  • Block Tree 优化:动态节点按区块(Block)组织,更新时仅对比区块内节点,减少遍历范围。

6. DOM 挂载与更新

  • 首次渲染(Mount):递归将 VNode 转换为真实 DOM,插入挂载目标。
  • Diff 算法(Patch)
    • 对比新旧 VNode:通过高效的 Diff 算法(基于补丁标志)识别差异。
    • 最小化更新:仅对动态部分进行 DOM 操作,复用静态节点。
  • 异步更新队列:数据变化触发的更新被批量处理,避免重复渲染(通过 nextTick 调度)。

7. 响应式更新循环

  • 触发更新:响应式数据变化时,通知关联的渲染 effect
  • 重新生成 VNode:执行渲染函数生成新 VNode 树。
  • Patch 对比更新:对比新旧 VNode,高效更新 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 做代理
  • 3、如果是数组 includes, indexOf, lastIndexOf 会被拦截,并且让数组的每一项索引去收集 effect

设置/发布 set

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

vue2 vs vue3 代理对比

  • vue3 针对的是对象来进行劫持,不用改写原来的对象,如果是嵌套,当取值的时候才会代理
  • vue3 可以对不存在的属性进行获取,也会走 get 方法,Proxy 支持数组
  • vue2 针对的是属性劫持,改写了原来对象,一上来就递归的
  • 懒递归 当我们取值的时候才去做递归代理,如果不取默认值代理一层

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

  • ref 是一个类在实例化时会存储原始的值,通过属性访问器 set、get 来实现响应式的能力

  • 当在取 state.value 时,触发 get 让当前的实例去收集 effect

  • 当在给 value 赋值时,会触发 set 拦截通知 effect 触发更新

  • 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

    1、ref 可以让普通的值具备响应式的能力

    2、ref 是通过属性访问器 get、set 拦截处理更新

  • reactive

    reactive 只能代理对象,并且使用的 Es6 的 Proxy

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 监听

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

watchEffect

  • 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 计算属性

  • 计算属性是一个类,在初始化时,首先会初始化缓存的状态dirty,会创建一个 lazy的 effect 不会立马执行,并且把 getter 控制权交给 effect
  • 当在取 state.value 时,触发 get 拦截,如果没有缓存会调用 effect 获取的计算属性的值,响应式数据会通过track收集当前的计算属性effect,同时会让计算实例收集渲染的effect
  • 当响应式数据发生变化时,触发 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 处理

  • 子组件在创建 createVNode 时,如果父组件有传递数据,会将数据存储在 Vnode props 对象中
  • 子组件在创建组件实例对象时,会把自己接受的 props 数据,存储在 实例 propsOptionse 对象中
  • 子组件在渲染的时候,会调用 initProps,如果父组件传递的数据在子组件中有定义存储在组件实例props中
  • 如果是未接收的属性,将全部存储在组件实例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 原理

TIP

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 实例原理

TIP

将子组件属性或者函数暴露出去,让父组件可以通过 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 插槽 原理

  • 1、模版在编译的时候,会把 slot 插槽转换成一个对象
  • 2、如果是匿名插槽会 default 会作为对象的 属性,如果是具明插槽 插槽的名字作为属性
  • 3、在构建虚拟节点的时候,如果子节点是一个对象,会把子节点标记成插槽
  • 4、在创建组件实例的时候,如果 Vnode 上有children子节点会执行initSlots(instance,children)
  • 5、如果 children 是插槽类型,会把 children 对象,存储在组件实例的 slots 上,并且 slots 会传递到上下文对象中
  • 6、让组件在渲染的时间,可以直接调用上下文中的 slots 进行渲染
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

TIP

provide or inject 必须在组件中使用

provide 和 inject 类似于内容提供者和消费者任何下级的组件都可以消费父组件的提供的 provide

  • 1、当一个组件在创建组件实例时,会存储自己的父组件是谁,如果父组件有 provides,就会将父组件的 provides 存储在当前组件实例 provides 上,最终会形成类似一个原型链
  • 2、provide:当组件中调用 provide 时,会去父组件实例上查找 provides,如果有会进行合并,存储在当前组件实例的 provides 上
  • 3、inject:当组件中调用 inject 时,会去父组件实例上查找 provides,如果有返回父组件提供的,如果没有返回默认值
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 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 单指针的方式进行比较。

  • 优化策略有 3 种,头头、尾尾、乱序

  • 都是先比较是否是相同节点,相同节点比较属性,并复用老节点,比较儿子节点

  • 1、头头对比新老虚拟节点进行头头对比,如果 tag、key 相同,复用老的节点,比较儿子节点,头指针索引递增

  • 2、尾尾开始对比,如果 tag、key 相同新老尾指针递减

  • 3、如果头指针超过其中有任何一个尾指针,证明有一方已经对比完毕,停止循环

  • 4、如果老的虚拟节点没有对比完全部删除

  • 5、如果新的虚拟节点没有对比完讲新的插入(头指针索引递增)

  • 6、如果新老虚拟节头头、尾尾对比之后都没对比完成,可能中间的节点可以复用

  • 7、乱序对比,需要尽可能复用,用新的虚拟列表做成一个映射表

  • 8、遍历老的虚拟列表,如果如果 key 相同标记已复用,如果不相同,删除老的节点

  • 9、最后插入新的元素,移动复用的元素到正确的位置

1、头头对比

diff

2、尾尾对比

diff

3、乱序对比

diff

life cycle(生命周期)

  • 通过发布/订阅来实现收集、触发

  • 1、vue3 生命周期必须在 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

Vue3 的 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、模版在解析的时候,如果有 v-model 属性,会转换成input输入事件,同时把绑定的值,赋值给 input value 属性
  • 2、当用户输入内容时,会触发 input 输入事件来更新绑定的数据

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 对首屏做服务端渲染。