Vue 原理篇

118 阅读20分钟

VDOM 如何生成

1.  编写组件模板 template

// 编写组件模板(Template)
<template>
    <div id="app">
        <p>{{ message }}</p>
        <button @click="updateMessage">更新</button>
    </div>
</template>

2.  组件模板编译成 render 函数

// 模板编译为Render函数
function render() {
    return h('div', { id: 'app' }, [
        h('p', null, this.message),
        h('button', { on: { click: this.updateMessage } }, '更新')
    ])
}
// h函数:创建VNode的辅助函数
function h(tag, props, children) {
    return { tag, props, children }
}

3.  挂载过程中调用 render 函数,返回的对象是虚拟 DOM

调用时机:

  • 组件初次挂载阶段,在 beforeMount 生命周期钩子之后、 mounted 之前首次调用

  • 响应式数据更新时

  • 强制更新时

  • 组件重新渲染的其他场景

// 组件实例
const vm = {
    message: 'Hello Vue',
    updateMessage() { this.message = 'Updated!' },
    render: render // 编译后的render函数
}
// 生成虚拟DOM
const vnode = vm.render.call(vm) // 执行render函数
/* 
vnode结构:
{
    tag: 'div',
    props: { id: 'app' },
    children: [
        { tag: 'p', props: null, children: 'Hello Vue' },
        { tag: 'button', props: { on: { click: fn } }, children: '更新' }
    ]
}
*/

4.  在 patch 过程中转换为真实 DOM,patch 函数作用:负责VNode到真实DOM的转换,包含初次渲染和更新阶段的diff逻辑

// 初次渲染:VNode -> 真实DOM
function patch(container, vnode) {
    const el = document.createElement(vnode.tag)
    // 设置属性
    if (vnode.props) {
        Object.keys(vnode.props).forEach(key => {
            if (key === 'on') {
                // 事件监听
                Object.keys(vnode.props.on).forEach(event => {
                    el.addEventListener(event, vnode.props.on[event])
                })
            } else {
                el.setAttribute(key, vnode.props[key])
            }
        })
    }
    // 递归处理子节点
    if (vnode.children) {
        vnode.children.forEach(child => patch(el, child))
    }
    container.appendChild(el)
}
// 挂载到真实DOM
const appContainer = document.getElementById('app-container')
patch(appContainer, vnode)
// 更新阶段:diff对比(简化版)
function patch(oldVnode, newVnode) {
    // 1. 标签不同:直接替换
    if (oldVnode.tag !== newVnode.tag) {
        return replaceNode(oldVnode.el, newVnode)
    }
    // 2. 文本节点:直接更新内容
    if (typeof newVnode === 'string') {
        oldVnode.el.textContent = newVnode
        return
    }
    // 3. 属性更新
    updateProps(oldVnode.el, oldVnode.props, newVnode.props)
    // 4. 子节点diff(核心逻辑)
    updateChildren(oldVnode.el, oldVnode.children, newVnode.children)
}

Vue 响应式过程

// 核心依赖管理类, 用于实现 依赖收集 和 依赖触发
class Dep {
    constructor() {
        // 用于存储某个响应式数据的所有依赖函数( Effect )
        this.effects = new Set(); 
    }
    // 将当前激活的 activeEffect(全局变量) 添加到 effects 集合中,完成「依赖收集」
    depend() {
        if (activeEffect) {
            this.effects.add(activeEffect);
        }
    }
    // 触发所有依赖的Effect
    notify() {
        this.effects.forEach(effect => effect.run());
    }
}

// 建立 对象-属性-Dep 三者之间的映射关系
const targetMap = new WeakMap();
function getDep(target, key) {
    // 1. 为对象创建映射(若不存在)
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
    }
    // 2. 为属性创建Dep(若不存在)
    let dep = depsMap.get(key);
    if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
    }
    return dep;
}

// 响应式系统
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            // 第一次依赖收集,执行 run 方法, 这里的 get 会执行两次,因为有两个属性
            const dep = getDep(target, key); // 获取属性对应的Dep实例,两个属性,会返回两个 Dep 实例,
            dep.depend(); // 触发依赖收集
            /*
                key: {name: 'zhangsan', age: 18}
                value: Map(2) {
                    'name' => Dep {effects: Set(1)}, 
                    'age' => Dep {effects: Set(1)}
                }
             */
            return target[key];
        },
        set(target, key, value) {
            target[key] = value;
            const dep = getDep(target, key); // 获取属性对应的Dep实例  Dep {effects: Set(1)}
            dep.notify(); // 通知依赖更新  执行回调函数
        }
    });
}


let activeEffect = null;
class Effect {
    constructor(fn) {
        this.fn = fn;
    }
    run() {
        activeEffect = this; // 标记当前激活的Effect
        this.fn(); // 执行Effect回调(会访问响应式数据触发getter)
        activeEffect = null;
    }
}

// 实际使用流程
const state = reactive({
    name: 'zhangsan',
    age: 18
});
// 2. 创建渲染Effect(模拟组件渲染)
const renderEffect = new Effect(() => {
    // 访问state.count触发依赖收集
    document.getElementById('root').innerHTML = `
                <div>
                    <p>姓名: ${state.name}</p>
                    <p>年龄: ${state.age}</p>
                </div>
            `;
});
// 3. 首次执行Effect(触发依赖收集)
renderEffect.run();
// 4. 修改响应式数据(触发更新)
state.age = 19;
// 5. 再次修改数据
state.name = 'lisi';

effect 函数

effect 函数主要用于执行 副作用函数 ,这些函数会在响应式数据变化时自动重新执行。具体包括以下几类核心场景:

  1. 渲染函数

    模板编译后生成的渲染函数会被包裹在 effect 中执行,形成 渲染副作用 。当响应式数据变化时, effect 会重新执行渲染函数,生成新的虚拟 DOM 并更新视图 。

effect(() => {
  const vnode = render(); // 执行渲染函数
  patch(prevVnode, vnode); // 更新 DOM
});
  1. 计算属性 计算属性的 getter 函数会被包装为 effect ,实现 缓存与自动更新 。只有当依赖的响应式数据变化时,才会重新计算。

  2. WatchEffect/Watch 回调 watchEffect 和 watch 的回调函数会直接作为 effect 的执行内容,用于监听数据变化并执行自定义逻辑

  3. 用户自定义副作用

effect(() => {
  // 自定义副作用逻辑(如数据持久化、日志记录等)
  localStorage.setItem('count', count.value);
});

响应式数据

  • vue2

    使用 object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现,多层对象是通过递归实现劫持。

let obj = { name: 'jw', age: 30, n: { num: 100 } };
function defineReactive(target, key, value) {
    observer(value)
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (value !== newValue) {
                value = newValue;
                observer(newValue)
            }
        }
    })
}
function observer(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }
    for (let key in data) {
        defineReactive(data, key, data[key])
    }
}
observer(obj)

缺点:

1.  新增属性和删除属性时无法监控变化,需要通过 set、delete实现

2.  对于 es6 中新产生的 Map、Set 这些数据结构不支持

3.  数组不采用 defineProperty 来进行劫持(浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理。

  • vue3

    在Vue3中,当使用Proxy处理嵌套对象时,并不是在初始创建时就递归地为每一个属性添加响应式。Vue3采用了一种"懒递归"的方式:只有当你实际访问嵌套对象的属性时,才会为该属性创建Proxy并添加响应式。这种按需处理的方式可以提高性能,特别是对于包含大量嵌套层级的复杂对象。

    例如,当你有一个对象 { a: { b: { c: 1 } } } ,Vue3在初始时只会为最外层对象创建Proxy。只有当你访问 obj.a 时,才会为 a 属性对应的对象创建Proxy;当你访问 obj.a.b 时,才会为 b 属性对应的对象创建Proxy,以此类推。

// 简化版响应式实现
function reactive(obj) {
  // 如果不是对象则直接返回
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  // 创建Proxy代理
  return new Proxy(obj, {
    get(target, key) {
      const result = target[key];
      console.log(`访问属性: ${key}`);

      // 懒递归:只有访问时才对嵌套对象进行代理
      if (typeof result === 'object' && result !== null) {
        console.log(`为属性 ${key} 创建代理`);
        // 将结果替换为代理对象并缓存
        target[key] = reactive(result);
      }

      return target[key];
    },
    set(target, key, value) {
      console.log(`修改属性: ${key} = ${value}`);
      target[key] = reactive(value); // 对新设置的值也进行代理
      return true;
    }
  });
}

// 测试代码
const data = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3
    }
  }
};

const proxy = reactive(data);
console.log('---初始状态---');
console.log('a是否为Proxy:', proxy.a instanceof Proxy); // false (基本类型)
console.log('b是否为Proxy:', proxy.b instanceof Proxy); // false (尚未访问)

console.log('\n---访问proxy.b---');
const b = proxy.b; // 触发get陷阱
console.log('b是否为Proxy:', b instanceof Proxy); // true
console.log('d是否为Proxy:', b.d instanceof Proxy); // false (尚未访问)

console.log('\n---访问b.d---');
const d = b.d; // 触发get陷阱
console.log('d是否为Proxy:', d instanceof Proxy); // true

Vue2 重写数组原型方法

Vue2 没有给数组每一个元素绑定响应式,而是数组元素有变化的时候,先调用原生的数组方法,然后显式调用 notify() ,才能将数组变化纳入响应式系统。

// 1. 保存原生数组原型
const arrayProto = Array.prototype;
// 2. 创建拦截器对象(原型链指向原生数组原型)
const arrayMethods = Object.create(arrayProto);
// 3. 需要重写的7个方法
const reactiveMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
// 4. 遍历重写方法
reactiveMethods.forEach(method => {
  // 缓存原生方法
  const originalMethod = arrayProto[method];
  // 重写方法
  arrayMethods[method] = function(...args) {
    // 执行原生方法
    const result = originalMethod.apply(this, args);
    // 5. 触发依赖更新(核心逻辑)
    // 数组的原生方法(如 push )不会触发 Object.defineProperty 的 setter ,因此 Vue 2 需要在重写的方法中显式调用 notify() ,才能将数组变化纳入响应式系统。
    this.__ob__.dep.notify();
    return result;
  };
});
// 6. 使目标数组原型指向拦截器
function makeArrayReactive(arr) {
  if (Array.isArray(arr)) {
    arr.__proto__ = arrayMethods;
  }
  return arr;
}
const arr = [1, 2, 3];
makeArrayReactive(arr);
// 调用重写后的push方法,会触发更新
arr.push(4); // 触发dep.notify()

Vue.set()

Vue 2.x的响应式系统基于 Object.defineProperty 实现,该方法只能劫持对象初始化时已存在的属性。直接为对象添加新属性(如 this.obj.newProp = value )无法触发视图更新,因为新属性未被劫持。
Vue.set() 底层通过 defineReactive 方法实现响应式转换,核心是为新属性创建 getter/setter 并触发依赖更新。

// 模拟 Vue 的依赖收集容器
const dep = {
  depend() { /* 收集依赖 */ },
  notify() { /* 通知更新 */ }
};

// 核心:将属性转为响应式
defineReactive = (obj, key, val) => {
  Object.defineProperty(obj, key, {
    get() {
      dep.depend(); // 收集依赖
      return val;
    },
    set(newVal) {
      if (val !== newVal) {
        val = newVal;
        dep.notify(); // 触发更新
      }
    }
  });
};

// 模拟 Vue.set() 实现
VueSet = (target, key, value) => {
  // 1. 如果是数组,通过 splice 触发响应式
  if (Array.isArray(target)) {
    target.splice(key, 1, value);
    return value;
  }
  
  // 2. 如果属性已存在,直接赋值
  if (target.hasOwnProperty(key)) {
    target[key] = value;
    return value;
  }
  
  // 3. 为新属性创建响应式
  const ob = target.__ob__; // 获取对象的观察者实例
  if (!ob) { // 如果对象不是响应式的,直接赋值
    target[key] = value;
    return value;
  }
  
  // 4. 核心:调用 defineReactive 转换属性
  defineReactive(ob.value, key, value);
  // 触发依赖更新,因为:仅创建 getter/setter 无法让已存在的依赖知道新属性的存在
  ob.dep.notify();
  return value;
};

Vue 3采用Proxy替代 Object.defineProperty 实现响应式,可直接监听对象新增属性,因此不再需要 Vue.set()

computed 和 watch 区别

Vue2 中有三种 watcher (渲染 watcher、计算属性 watcher、用户 watcher)
Vue3 中有三种 effect (渲染 effect、计算属性 effect、用户 effect)
计算属性 watcher 是 computed,用户 watcher 是 watch 方法

  • computed
  1. 计算属性仅当用户取值时才会执行对应的方法。
  2. computed 属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行。
  3. 计算属性中不支持异步逻辑。
// computed属性的实现原理主要基于 依赖收集 和 缓存机制
// computed实现
// a/b变化 -> effect -> dirty=true -> 等待访问 -> sum.value访问 -> runner执行 -> 返回新值
        function computed(getter) {
            // 存储 getter 函数的计算结果,避免重复计算
            let cache = null;
            // 标记缓存状态, true 表示需要重新计算, false 表示可直接使用缓存
            let dirty = true;
            // 依赖更新触发器
            const effect = () => {
                // 当依赖变化时,将缓存标记为失效
                dirty = true;
            };
            const runner = () => {
                // 检查缓存是否失效,惰性计算 :只有 dirty 为 true 时才执行 getter()
                if (dirty) {
                    // 激活当前effect(依赖收集阶段)
                    activeEffect = effect;
                    // 执行用户提供的getter函数计算结果,这个时候访问 state.a 和 state.b,这两个属性值的访问,会将effect收集,属性值变化时触发。
                    cache = getter();
                    // 重置激活状态
                    activeEffect = null;
                    // 将缓存标记为有效
                    dirty = false;
                }
                return cache;
            };
            return {
                get value() {
                    return runner(); // 访问.value时才执行计算
                }
            };
        }
        const sum = computed(() => {
            console.log('重新计算sum');
            return state.a + state.b;
        });

完整工作流程
1.初始状态 : dirty = true ,缓存为空
2.首次访问 :调用 value getter → 执行 runner() → 调用 getter() 计算并缓存结果 → dirty = false
3.再次访问 : dirty = false → 直接返回 cache 中的结果(不执行 getter() )
4.依赖变化 :响应式数据更新时调用 effect() → dirty = true → 下次访问触发重新计算

  • watch Vue的watch功能基于其响应式系统和依赖追踪机制实现,核心涉及 依赖收集 和 派发更新 两个阶段
        function effect(fn) {
            activeEffect = fn;
            fn(); // 立即执行一次收集依赖
            activeEffect = null;
        }
        // 5. watch实现
        function watch(source, callback) {
            // 获取数据的 getter 函数
            let getter;
            // 处理source是函数或响应式对象的情况
            if (typeof source === 'function') {
                getter = source;
            } else {
                getter = () => traverse(source);
            }
            let oldValue;
            // 执行effect收集依赖,触发了 activeEffect ,将 getter 中的变量和当前回调函数绑定
            effect(() => {
                const newValue = getter();
                if (oldValue !== undefined) {
                    callback(newValue, oldValue);
                }
                oldValue = newValue;
            });
        }
        // 辅助函数:递归遍历对象收集深层依赖
        function traverse(value) {
            if (typeof value !== 'object' || value === null) return value;
            for (const key in value) {
                traverse(value[key]);
            }
            return value;
        }
        // 使用示例
        const state = reactive({
            count: 0,
            user: {
                name: 'vue3'
            }
        });
        // 监听单个属性
        watch(
            () => state.count,
            (newVal, oldVal) => {
                console.log(`count变化: ${oldVal}${newVal}`);
            }
        );

ref 和 reactive

  • ref :用于包装基本数据类型(String/Number/Boolean等)或复杂对象,访问时需要通过 .value 属性
// Vue3的 ref 函数用于包装基本数据类型实现响应式,核心是通过对象的 getter/setter 拦截访问
function ref(initialValue) {
  // 创建包装对象
  const wrapper = {
    _value: initialValue
  }
  // 通过Object.defineProperty实现响应式
  Object.defineProperty(wrapper, 'value', {
    get() {
      // 收集依赖(实际Vue中会在这里实现track逻辑)
      console.log('获取ref值')
      return wrapper._value
    },
    set(newValue) {
      // 触发更新(实际Vue中会在这里实现trigger逻辑)
      if (newValue !== wrapper._value) {
        console.log(`设置ref值: ${newValue}`)
        wrapper._value = newValue
      }
    }
  })
  return wrapper
}

为什么 Vue3 中 ref 要使用 Object.defineProperty 来实现呢?

  1. JavaScript的基本类型无法直接被Proxy拦截,通过 Object.defineProperty 创建包装对象的 .value 属性,可以将基本类型值封装为对象形式,从而实现响应式能力。
  2. 与Proxy形成互补设计
    Proxy :适合处理复杂对象( reactive API),能拦截对象的各种操作(get/set/delete等)
    Object.defineProperty :适合简单值包装( ref API),实现更轻量的访问拦截
  • reactive :用于包装对象类型数据(Object/Array等),直接通过属性访问,无需 .value
// 简单版reactive实现
function reactive(obj) {
  // 仅处理对象类型
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  // 创建Proxy拦截器
  const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      // 收集依赖(实际Vue中会在这里实现track逻辑)
      console.log(`获取属性: ${key}`)
      // 递归处理嵌套对象
      return typeof result === 'object' ? reactive(result) : result
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      const result = Reflect.set(target, key, value, receiver)
      // 触发更新(实际Vue中会在这里实现trigger逻辑)
      if (oldValue !== value) {
        console.log(`更新属性: ${key}, 新值: ${value}`)
      }
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      // 触发删除更新
      console.log(`删除属性: ${key}`)
      return result
    }
  }
  // 返回代理对象
  return new Proxy(obj, handler)
}
// 使用示例
const state = reactive({
  name: 'Vue',
  info: {
    version: '3.x'
  }
})

watch 和 watchEffect

核心区别

需要获取Vue中watch和watchEffect的区别和用法。

toolName: web_search

status: success

query: Vue watch vs watchEffect

在Vue中,watchwatchEffect都是用于监听响应式数据变化的API,但它们在使用方式和行为上有显著区别:

核心区别

特性watchwatchEffect
数据源指定需要显式指定监听的响应式数据自动追踪回调中使用的响应式数据
执行时机默认惰性执行(仅数据变化时触发)立即执行一次,之后响应式数据变化时再次执行
参数获取提供旧值(oldVal)和新值(newVal)仅提供当前值,无旧值
配置选项支持deepimmediate等高级配置无配置选项

watchEffect 的基本用法

import { ref, watchEffect } from 'vue';
const count = ref(0);

// 自动追踪回调中使用的count
watchEffect(() => {
  console.log(`当前count值: ${count.value}`); // 初始化时立即执行,输出: 当前count值: 0
});
count.value = 1; // 触发重新执行,输出: 当前count值: 1
function watchEffect(effect) {
  // 保存当前副作用函数
  activeEffect = effect;
  // 立即执行副作用函数,触发依赖收集
  effect();
  // 执行完毕后重置
  activeEffect = null;
}

// 使用示例
const state = reactive({ count: 0 });
// 注册副作用
watchEffect(() => {
  console.log(`count变化了: ${state.count}`);
});
// 触发更新
state.count = 1; // 输出: count变化了: 1
state.count = 2; // 输出: count变化了: 2

template 转换为 render 函数

  1. 模板解析 (Parse):将模板字符串转换为AST抽象语法树
        // 简化的解析结果
        const ast = {
            type: 'Element',
            tag: 'button',
            props: [
                {
                    name: 'onClick',
                    value: 'isDark = !isDark'
                }
            ],
            children: [
                {
                    type: 'Element',
                    tag: 'span',
                    props: [{
                        name: 'v-if',
                        value: 'isDark'
                    }],
                    children: [{ type: 'Text', content: '🌙' }]
                },
                {
                    type: 'Element',
                    tag: 'span',
                    props: [{
                        name: 'v-else',
                        value: true
                    }],
                    children: [{ type: 'Text', content: '🌞' }]
                }
            ]
        }
  1. 优化 (Optimize) 标记静态节点,避免diff时重复比较
// 标记静态文本节点
ast.children[0].children[0].static = true;
ast.children[1].children[0].static = true;
  1. 将AST转换为render函数代码字符串:
// 生成的代码字符串
const code = `() => (
  _c('button', {
    on: { click: () => isDark = !isDark }
  }, [
    isDark ? _c('span', [_v('🌙')]) : _c('span', [_v('🌞')])
  ])
)`;
  1. 最终生成可执行的render函数:
import { defineComponent, ref } from 'vue'
export default defineComponent({
  setup() {
    const isDark = ref(false)
    // 最终的render函数
    return () => (
      <button onClick={() => isDark.value = !isDark.value}>
        {isDark.value ? <span>🌙</span> : <span>🌞</span>}
      </button>
    )
  }
})

new Vue() 过程

  1. 构造函数调用 : new Vue() 会触发 Vue 构造函数,在构造函数中首先检查调用方式是否正确(必须使用 new 关键字),然后调用 _init() 方法开始初始化过程
  2. 初始化代理(initProxy) :创建 vm 的 proxy 代理,主要用于检测错误的使用并调用 warnNonPresent 方法做出警示。在生产环境中则直接将 _renderProxy 指向 vm 实例
  3. 生命周期初始化(initLifecycle) :声明 parentparent 、 root 、 $refs 等属性,建立组件之间的层级关系
  4. 事件初始化(initEvents) :对父组件传入的事件添加监听,遵循事件创建者监听的原则
  5. 渲染初始化(initRender) :声明 slotsslots 和 createElement() 方法,为后续的模板渲染做准备
  6. 生命周期钩子调用 :触发 beforeCreate 生命周期钩子
  7. 状态初始化 :包括初始化 data、props、computed、watcher 等状态相关内容,完成数据响应式处理 整个过程可以概括为:合并配置 → 初始化核心功能模块(生命周期、事件、渲染等)→ 初始化状态 → 触发生命周期钩子,最终完成 Vue 实例的创建。

v-for 和 v-if

  • 优先级
    vue2: v-for > v-if,会导致先循环后再对每一项进行判断,浪费性能。
    image.png
    vue3: v-if > v-for
    image.png

生命周期

用户询问Vue2和Vue3的生命周期差异,我需要通过网络搜索获取准确的对比信息,确保包含官方文档或权威来源的内容。

toolName: web_search

status: success

query: Vue2 vs Vue3 生命周期对比

现有搜索结果存在信息冲突,特别是关于Vue3是否合并生命周期钩子的问题。需要进一步搜索Vue官方文档或权威来源以验证准确性,确保提供正确的生命周期对比信息。

toolName: web_search

status: success

query: Vue3官方文档 生命周期对比Vue2

Vue2 与 Vue3 生命周期对比

一、生命周期对应关系

Vue2 生命周期钩子Vue3 组合式 API 钩子说明
beforeCreatesetup()Vue3 中 setup() 替代了 beforeCreate 和 created,在实例初始化前执行
createdsetup()同上
beforeMountonBeforeMount挂载前执行
mountedonMounted挂载完成后执行
beforeUpdateonBeforeUpdate数据更新前执行
updatedonUpdated数据更新后执行
beforeDestroyonBeforeUnmount组件卸载前执行(名称变更)
destroyedonUnmounted组件卸载后执行(名称变更)
errorCapturedonErrorCaptured错误捕获钩子

Vue 父子组件生命周期执行顺序

  • 通用执行原则
    Vue 父子组件生命周期执行顺序遵循 "从外到内,再从内到外" 的原则:父组件先初始化创建,再递归初始化子组件,待子组件完成后父组件才最终完成。

  • 各阶段执行顺序对比

生命周期阶段执行顺序(Vue2/Vue3通用)
挂载阶段父beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted
更新阶段父beforeUpdate → 子beforeUpdate → 子updated → 父updated(需父子组件存在数据传递)
销毁阶段父beforeDestroy → 子beforeDestroy → 子destroyed → 父destroyed

diff原理

  1. 先比较是否是相同节点 key tag
  2. 相同节点比较属性并复用老节点(将老的虚拟 dom 复用给新的虚拟节点 DOM)
  3. 比较子节点,考虑老节点和新节点儿子的情况
    • 老的没儿子,现在有儿子,直接插入新的儿子
    • 老的有儿子,新的没儿子,直接删除页面节点
    • 老的儿子是文本,新的儿子是文本,直接更新文本节点即可
    • 老的儿子是一个列表,新的儿子也是一个列表 updateChildren
  4. 优化比较:头头、尾尾、头尾、尾头 image.png

key 的作用

Vue.use

安装 Vue.js 插件。
如果插件是一个对象,必须提供 install 方法。
如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入,这样插件中就不需要依赖 Vue 了。

let plugin1 = {
  install(_Vue, ...args) {
    console.log(_Vue, args) // 输出Vue构造函数和传入的参数数组
  }
}
let plugin2 = (_Vue, ...args) => {
  console.log(_Vue, args) // 输出Vue构造函数和传入的参数数组
}
Vue.use(plugin1, 1, 2, 3) // 安装插件1,传入参数1,2,3
Vue.use(plugin2, 1, 2, 3) // 安装插件2,传入参数1,2,3

插件的功能:

  • 添加全局指令、全局过滤器、全局组件。
  • 通过全局混入来添加一些组件选项。
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
    原理:
export function use(plugin, ...options) {
  // 防止重复安装同一个插件
  // 这种设计支持 链式调用,Vue.use(PluginA).use(PluginB).component('MyComponent', MyComponent)
  if (plugin.installed) return this; // 这里的 this 是 Vue 构造函数本身

  // 将 Vue 构造函数作为第一个参数,与用户传入的 options 合并为一个参数数组
  const args = [this].concat(options);

  // 如果插件是对象且有install方法,调用它
  if (typeof plugin.install === 'function') {
    plugin.install.apply(plugin, args);
  }
  // 如果插件本身是函数,直接调用
  else if (typeof plugin === 'function') {
    plugin.apply(null, args);
  }

  // 标记插件为已安装
  plugin.installed = true;
  return this;
}

Vue.extend

使用基础 Vue 构造器,创建一个子类。参数是一个包含组件选项的对象。
data 选项是特例,需要注意: 在 vue.extend () 中它必须是函数,避免多个实例共享同一数据对象

var Profile = Vue.extend({
    template: "<p>{{firstName}} {{lastName}} aka {{alias}}</p>",
    data: function () {
        return {
            firstName: "walter",
            lastName: "white",
            alias: "Heisenberg",
        };
    },
});

// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount("#mount-point");

new Vue().$mount();

在 Vue3 中

<script lang='tsx'>
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup () {
    const isDart = ref(true)
    const changeDart = () => {
      isDart.value = !isDart.value
    }
    return () => (
      <button onClick={ changeDart }>
        { isDart.value ? <span>🌙</span> : <span>🌞</span> }
      </button>
    )
  }
})
</script>

data 为什么是函数

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

Vue.extend = function (options) {
    function Sub() {
        // 会将data存起来
        this.data = this.constructor.options.data();
    }
    Sub.options = options;
    return Sub;
};

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

// Child 组件的 data 选项是一个函数,每次调用都会返回新的对象实例
let child1 = new Child();
let child2 = new Child();

console.log(child1.data.name);
child1.data.name = "jw";
// 修改 child1.data.name 只会影响 child1 的私有数据,不会影响 child2
console.log(child2.data.name);

在Vue2中,组件的 data 选项需要是一个函数并返回对象,主要是为了避免多个组件实例共享同一数据对象导致的状态污染问题。因为JavaScript对象是引用类型,若直接使用对象形式,所有组件实例会共用同一份数据,一个实例修改会影响其他实例。通过函数返回对象,每次创建组件实例时都会生成新的对象副本,确保数据独立性。

Vue3不需要这样设计,Vue3 内部也会自动处理为每个实例创建独立副本,因此可直接写成对象形式

// Vue3 Composition API
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0) // 每次实例化都会创建新的 ref 对象
    return { count }
  }
}

v-once

主要作用:

  1. 一次性渲染 :首次渲染后,即使数据发生变化,元素内容也不会更新
  2. 性能优化 :减少不必要的DOM更新,适用于静态内容
  3. 简化模板 :对于不需要响应式的数据,避免Vue进行双向绑定跟踪
<!-- 基本用法 -->
<span v-once>{{ message }}</span>

<!-- 用于组件 -->
<my-component v-once :prop="value"></my-component>
  1. 编译阶段标记静态节点 Vue编译器在解析模板时,遇到 v-once 会将该节点标记为 静态节点 (staticNode),并在AST(抽象语法树)中记录这一标识。
  2. 跳过响应式依赖收集 静态节点不会被纳入Vue的响应式系统,即不会为其创建Watcher或收集依赖。因此当数据变化时,不会触发该节点的重新渲染。
  3. 虚拟DOM优化 在生成虚拟DOM(VNode)时,静态节点会被特殊处理:
    • 首次渲染后缓存VNode实例
    • 后续更新时直接复用缓存的VNode,跳过diff算法对比
    • 不会生成新的DOM节点或更新现有节点

mixin

  1. 数据对象,合并规则 :递归合并,组件数据优先
const mixin = { data() { return { a: 1, b: 2 } } }
const vm = new Vue({ mixins: [mixin], data() { return { b: 3, c: 4 } } })
console.log(vm.$data) // { a: 1, b: 3, c: 4 } (b被组件数据覆盖)
  1. 生命周期函数,合并为数组,全部执行,mixin 钩子先执行
const mixin = { created() { console.log('mixin created') } }
new Vue({ mixins: [mixin], created() { console.log('component created') } })
// 输出顺序:mixin created → component created
  1. 方法(methods)、计算属性(computed)、侦听器(watch),组件选项优先,直接覆盖
const mixin = { methods: { foo() { return 'mixin' } } }
new Vue({ mixins: [mixin], methods: { foo() { return 'component' } } })
console.log(vm.foo()) // component (组件方法覆盖mixin方法)
  1. 组件选项(components、directives、filters),对象合并,组件选项优先

slot

  • 默认插槽
  • 具名插槽 image.png
  • 作用域插槽
    作用域插槽允许子组件向父组件传递数据,同时让父组件决定如何渲染这些数据。 image.png

双向绑定

  • 表单元素 v-model = v-bind:value + v-on:input
<!-- 基础用法 -->
<input v-model="message">
<!-- 等价于手动实现双向绑定 -->
<input :value="message" @input="message = $event.target.value">

<!-- 复选框示例 -->
<input type="checkbox" v-model="isChecked">
<!-- 等价于 -->
<input type="checkbox" :checked="isChecked" @change="isChecked = $event.target.checked">
  • 组件 v-model 实现父子组件之间的双向数据绑定
<!-- 父组件 -->
<CustomInput v-model="username" />

<!-- 等价于 -->
<CustomInput :value="username" @input="username = $event" />
export default {
  props: {
    value: {
      type: String, // 根据实际需求设置类型
      required: true
    }
  },
  methods: {
    updateValue(newValue) {
      this.$emit('input', newValue); // 触发 input 事件更新父组件数据
    }
  }
}

.sync

语法糖,用于简化父子组件之间的双向数据绑定
在父组件中使用<child-component :title.sync="parentTitle"></child-component>
等价于<child-component :title="parentTitle" @update:title="parentTitle = $event"></child-component>
子组件 接收/修改 值

import { defineProps } from 'vue'
export default {
  props: defineProps({
    title: {
      type: String,
      required: true
    }
  })
}

// 子组件中触发更新
this.$emit('update:title', '新标题值') // Vue 2
// 或
emit('update:title', '新标题值') // Vue 3 组合式 API

递归组件

必须指定组件名才能递归调用

<template>
  <div class="tree-node">
    <div class="node-content">{{ node.label }}</div>
    <div v-if="node.children && node.children.length" class="children">
      <RecursiveTree
        v-for="(child, index) in node.children"
        :key="index"
        :node="child"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'RecursiveTree', // 必须指定组件名才能递归调用
  props: {
    node: {
      type: Object,
      required: true
    }
  }
}
</script>
<RecursiveTree :node="treeData" />
  data() {
    return {
      treeData: {
        label: '根节点',
        children: [
          {
            label: '子节点1',
            children: [
              { label: '孙节点1-1' },
              { label: '孙节点1-2' }
            ]
          },
          {
            label: '子节点2',
            children: [
              {
                label: '孙节点2-1',
                children: [
                  { label: '曾孙节点2-1-1' }
                ]
              }
            ]
          }
        ]
      }
    }
  }

自定义指令

可以将操作DOM的逻辑复用
指令钩子函数:

  • bind :只调用一次,指令第一次绑定到元素时执行
  • inserted :被绑定元素插入父节点时调用
  • update :所在组件VNode更新时调用
  • componentUpdated :所在组件VNode及其子VNode全部更新后调用
  • unbind :只调用一次,指令与元素解绑时调用
Vue.directive('color', {
  bind: function (el, binding) {
    // binding.value 为指令的绑定值
    el.style.color = binding.value
  }
})

<p v-color="'red'">这段文字会变成红色</p>

nextTick

修改Vue实例的数据时,Vue不会立即更新DOM,而是将这些更新操作缓存在一个队列中,等到下一个事件循环周期才会统一执行并刷新DOM。这种异步更新机制可以提高性能,但有时你需要在DOM更新完成后立即执行某些操作(比如获取更新后的DOM尺寸、操作新渲染的元素等)。

// 修改数据
this.message = '新内容';

// 此时DOM尚未更新
console.log(this.$el.textContent); // 输出旧内容

// 使用nextTick确保在DOM更新后执行
this.$nextTick(() => {
  console.log(this.$el.textContent); // 输出新内容
});
import { nextTick, ref, onMounted } from 'vue';

const list = ref([]);
const loadData = async () => {
  list.value = await fetchData();
  await nextTick(); // 等待 DOM 更新
  // 操作更新后的 DOM
  console.log(document.getElementById('list').offsetHeight);
};

keep-alive

<keep-alive> 是一个内置组件,主要作用是缓存不活动的组件实例,避免组件频繁创建和销毁带来的性能开销。

  • 组件缓存 :当组件被包裹在 中时,切换路由或条件渲染导致组件失活时,组件实例不会被销毁,而是被缓存起来
  • 状态保留 :缓存的组件会保留其内部状态和DOM结构,再次激活时无需重新初始化
  • 常用场景:
    • 频繁切换的标签页或路由视图
    • 表单填写页面(避免切换后输入内容丢失)
  • 动态组件用 keep-alive 缓存
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
    <component :is="component"></component>
</keep-alive>
  • 路由中使用 keep-alive
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
  <router-view></router-view>
</keep-alive>
  • 通过 meta 属性指定缓存
<div id="app">
  <keep-alive>
    <!-- 需要缓存的视图组件 -->
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
  <!-- 不需要缓存的视图组件 -->
  <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

keep-alive 缓存策略使用了 LRU 算法

  • keep-alive 数据更新问题
  1. 利用激活/停用生命周期钩子,被缓存的组件在激活时会触发 activated 钩子,停用会触发 deactivated 钩子。可以在 activated 中执行数据更新逻辑:
export default {
  activated() {
    // 组件被激活时更新数据
    this.fetchData();
  },
  methods: {
    fetchData() {
      // 数据请求或更新逻辑
    }
  }
}
  1. 使用 $forceUpdate 强制更新
this.$forceUpdate();

避免在 deactivated 中进行重型数据操作,对于复杂状态,推荐使用状态管理方案而非本地数据。

Vue 中使用了哪些设计模式

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

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