Skip to content

vue3.0

vue2/3 区别

  • 项目架构上做对比 Vue3采用 monorepo 的方式可以让一个项目下管理多个项目,每个包可以单独发布和使用
  • Vue3 基于 TS compositionApi tree-shaking 支持比较友好, OptionsApi 缺陷就是不能 tree-shaking
  • Vue2 后期引入 RFC , 使每个版本改动可控 rfcs

vue3 内部代码优化

  • Vue3 劫持数据采用 Es6 proxy,Vue2 劫持数据采用 defineProperty。
    • defineProperty 有性能问题和缺陷,初始化时会递归遍历每一个属性加 set,get 拦截,Vue3 在取值的时候才会进行递归
  • Vue3 中对模板编译进行了优化,编译时 生成了 Block tree,可以对子节点的动态节点进行收集,可以减少比较,并且采用了 patchFlag 标记动态节点
  • Vue3 中 采用了 compositionApi 实现了方便代码的复用 (解决了 mixin 的问题- 命名冲突 数据来源不明确)
  • Vue3 diff 算法 内部用的是 最长递增子序列 + 暴力的递归比对 (全量比对浪费性能)
  • 增加了 Fragment,Teleport,Suspense 组件

生命周期

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 元素。

2. 组件实例化

  • 初始化组件实例:创建根组件实例,处理 propssetup 函数、data 等选项。
  • 执行 setup 函数:在组件实例化过程中,优先执行 Composition API 的 setup 函数,返回的响应式对象合并到渲染上下文。
  • 生命周期钩子:触发 beforeCreatecreated 钩子(注意:setupbeforeCreate 之前执行)。

3. 响应式系统建立

  • 数据响应式处理:使用 reactiveref 等 API 将数据转换为 Proxy 对象,建立依赖追踪。
  • 依赖收集:渲染函数作为副作用(effect)被追踪,访问响应式数据时触发 getter,收集依赖关系。

4. 编译阶段(可选)

  • 模板编译:若使用模板(如单文件组件),Vue3 在构建时通过 vue-loader 将模板编译为优化后的渲染函数。
  • 静态提升:编译时标记静态节点,提升为常量,避免重复创建。
  • 补丁标志(Patch Flags):动态节点标记不同更新类型(如文本、类名),优化 Diff 过程。

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。

8. 高级特性处理

  • Teleport:生成 VNode 时记录目标容器,在 Patch 阶段将内容渲染到指定 DOM。
  • Suspense:管理异步组件,先渲染占位内容,异步加载完成后更新。
  • Fragment:支持多根节点模板,VNode 处理为片段节点。

关键优化点:

  • Proxy 响应式:替代 Object.defineProperty,支持数组和对象属性的全功能监听。
  • 静态提升与补丁标志:大幅减少 Diff 计算量。
  • Block Tree:动态节点追踪,缩小对比范围。
  • 组合式 API:逻辑复用更灵活,提升代码组织性。

流程图概览:

初始化应用 → 组件实例化 → 响应式处理 → 编译模板 → 生成 VNode → 挂载/Patch → 响应式更新循环

通过以上流程,Vue3 实现了高效的渲染和更新机制,兼顾开发体验与运行时性能。

Vue 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;
}

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

TIP

keepAlive 主要的作用就是将组件进行缓存,提高渲染性能

  • 1、默认渲染 keep-alive 中插槽内容, 检测插槽是不是组件
  • 2、如果是组件就把组件和对应的 key 做成一个映射表,缓存起来
  • 3、卸载组件挂载新的组件,此时会命中插槽的更新, 卸载老组件的时候不是真的卸载,而是缓存到 dom 中。 加载新的组件
  • 4、下次访问的是已经访问过的组件了,那么此时需要复用组件的实例,并且不要在初始化了
  • 5、初始化的时候 会在缓存区中将 dom 拉取到容器中 “缓存的是 dom”
  • 6、缓存策略可以采用 lru 算法,实现删除头部,最新访问的放在尾部
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 对首屏做服务端渲染。