饭团追书免费版
67.97MB · 2025-11-15
核心原理就是通过 Object.defineProperty 对对象属性进行劫持,重新定义对象的 getter 和 setter,在 getter 取值时收集依赖,在 setter 修改值时触发依赖更新,更新页面。
Vue2 对数组和对象做了两种不同方式的处理。
监听对象变化:
针对对象来说,Vue 会循环遍历对象的每一个属性,用 defineReactive 重新定义 getter 和 setter。
function defineReactive(target,key,value){
observer(value);
Object.defineProperty(target,key,{ ¸v
get(){
// ... 收集依赖逻辑
return value;
},
set(newValue){
if (value !== newValue) {
value = newValue;
observer(newValue) // 把新设置的值包装成响应式
}
// ...触发依赖更新逻辑
}
})
}
function observer(data) {
if(typeof data !== 'object'){
return data
}
for(let key in data){
defineReactive(data,key,data[key]);
}
}
监听数组变化:
我们都知道,数组其实也是对象,同样可以用 Object.defineProperty 劫持数组的每一项,但如果数组有100万项,那就要调用 Object.defineProperty 一百万次,这样的话性能太低了。鉴于平时我们操作数组大都是采用数组提供的原生方法,于是 Vue 对数组重写原型链,在调用7个能改变自身的原生方法(push,pop,shift,unshift,splice,sort,reverse)时,通知页面进行刷新,具体实现过程如下:
// 先拿到数组的原型
const oldArrayProtoMethods = Array.prototype
// 用Object.create创建一个以oldArrayProtoMethods为原型的对象
const arrayMethods = Object.create(oldArrayProtoMethods)
const methods = [
'push',
'pop',
'shift',
'unshift',
'sort',
'reverse',
'splice'
]
methods.forEach(method => {
// 给arrayMethods定义7个方法
arrayMethods[method] = function (...args){
// 先找到数组对应的原生方法进行调用
const result = oldArrayProtoMethods[method].apply(this, args)
// 声明inserted,用来保存数组新增的数据
let inserted
// __ob__是Observer类实例的一个属性,data中的每个对象都是一个Observer类的实例
const ob = this.__ob__
switch(method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
default:
break
}
// 比如有新增的数据,新增数据也要被定义为响应式
if(inserted) ob.observeArray(inserted)
// 通知页面更新
ob.dep.notify()
return result
}
})
Object.defineProperty的缺点:
$set、$delete 实现。Vue3 采用 Proxy + Reflect 配合实现响应式。能解决上述 Object.defineProperty 的所有缺陷,唯一缺点就是兼容性没有 Object.defineProperty 好。
let handler = {
get(target, key) {
if (typeof target[key] === "object") {
return new Proxy(target[key], handler);
}
return Reflect.get(target, key);
},
set(target, key, value) {
let oldValue = target[key];
if (oldValue !== value) {
return Reflect.set(target, key, value);
}
return true;
},
};
let proxy = new Proxy(obj, handler);
Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
比较过程:
Vue3 在这个比较过程的基础上增加了最长递增子序列实现diff算法。
Vue 中的模板编译就是把我们写的 template 转换为渲染函数(render function) 的过程,它主要经历3个步骤:
<div id="app">
<p>{{ message }}</p>
</div>
v-for、v-if)和事件(@click)、插值表达式{{}}等 vue 语法。v-once 的节点,以及没有用到响应式数据的节点。vue2 解析结果:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v(_s(message))])])
}
}
_c: 是 createElement 的别名,用于创建 VNode。_v: 创建文本 VNode。_s: 是 toString 的别名,用于将值转换为字符串。vue3 解析结果:
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", { id: "app" }, [
_createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
_openBlock: 开启一个"block"区域,用于收集动态子节点。_createElementBlock: 创建一个块级虚拟 DOM 节点。_createElementVNode: 创建一个普通虚拟 DOM 节点。_toDisplayString: 将响应式数据 _ctx.message 转换为显示字符串,或者处理 null/undefined 等值,确保它们能正确渲染为空白字符串。vue2在线编译:template-explorer.vuejs.org/。
vue3在线编译:v2.template-explorer.vuejs.org/。
运行时+编译(runtime-compiler) vs 仅运行时(runtime-only):
vue-loader 进行编译。平时开发项目推荐使用仅运行时(runtime-only)版本。
编译后的特点:
简单来说,v-if 内部是通过一个三元表达式来实现的,而 v-show 则是通过控制 DOM 元素的 display 属性来实现的。
v-if 源码:
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()
if (condition.exp) { // 如果有表达式
return `(${condition.exp})?${ // 将表达式作为条件拼接成元素
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
v-show 源码:
{
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display // 获取原始显示值
el.style.display = value ? originalDisplay : 'none' // 根据属性控制显示或者隐藏
}
}
v-for 的优先级比 v-if 高,它们作用于一个节点上会导致先循环后对每一项进行判断,浪费性能。v-if 的优先级比 v-for 高,这就会导致 v-if 中的条件无法访问 v-for 作用域名中定义的变量别名。<li v-for="item in arr" v-if="item.visible">
{{ item}}
</li>
以上代码在 vue3 的编译结果如下:
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createCommentVNode as _createCommentVNode } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_ctx.item.visible)
? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(_ctx.arr, (item) => {
return (_openBlock(), _createElementBlock("li", null, _toDisplayString(item), 1 /* TEXT */))
}), 256 /* UNKEYED_FRAGMENT */))
: _createCommentVNode("v-if", true)
}
可以看出 vue3 在编译时会先判断 v-if,然后再走 v-for 的循环,所以在 v-if 中自然就无法访问 v-for 作用域名中定义的变量别名。
这样的写法在 vue3 中会抛出一个警告️,[Vue warn]: Property "item" was accessed during render but is not defined on instance,导致渲染失败。
以上代码在 vue2 还不能直接编译,因为 vue2 的组件需要一个根节点,所以我们在外层加一个 div:
<div>
<li v-for="item in arr" v-if="item.visible">
{{ item}}
</li>
</div>
其编译结果如下:
function render() {
with(this) {
return _c('div', _l((arr), function (item) {
return (item.visible) ? _c('li', [_v("n " + _s(item) + "n ")]) :
_e()
}), 0)
}
}
很明显是先循环 arr,然后每一项再用 item.visible 去判断的,也印证了在 vue2 中, v-for 的优先级高于 v-if。
这里体现了优雅降级的思想。
Vue2的实现:在 Vue 2 中,Vue.set 的实现主要位于 src/core/observer/index.js 中:
export function set (target: Array | Object, key: any, val: any): any {
// 1.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 2.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// 3.如果是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 4.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 5.通知视图更新
ob.dep.notify()
return val
}
Vue3 中 set 方法已经被移除,因为 proxy 天然弥补 vue2 响应式的缺陷。
Vue.use 是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。
Vue.use 源码:
Vue.use = function (plugin: Function | Object) {
// 插件不能重复的加载
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this) // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
if (typeof plugin.install === 'function') { // 调用插件的install方法
plugin.install.apply(plugin, args) Vue.install = function(Vue,args){}
} else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
plugin.apply(null, args)
}
installedPlugins.push(plugin) // 缓存插件
return this
}
mixin 是 Vue 2 中一种复用组件逻辑的方式,允许将可复用的配置(data、methods、computed、lifecycle hooks 等)抽离成一个对象,然后通过 mixins: [] 合并到组件中。支持全局注入和局部注入。
mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。mixin 的优点:
mixin 中有很多缺陷:
vue3 不再推荐使用它的理由如下:
| 问题 | 说明 |
|---|---|
| 1. 隐式依赖 & 数据来源不明确 | 组件行为来自多个 mixin,难以追踪 data、methods 是从哪里来的。 |
| 2. 命名冲突 | 多个 mixin 可能定义同名 data、methods,合并规则复杂(同名 methods 后者覆盖前者,data 合并为对象,同名 Key 后者覆盖前者)。 |
| 3. 调试和维护困难 | 父组件无法知道子组件内部有哪些 mixin 注入的属性,排查 bug 和调试困难。 |
| 4. 不利于 Tree-shaking | 打包时难以移除未使用的 mixin 代码。 |
| 5. 与 Composition API 理念冲突 | Mixin 是“横切关注点”,而 Composition API 强调显式、可组合的逻辑。 |
Vue 3 推荐替代方案:Composition API + 可复用函数(Composables)。
| 特性 | Mixin | Composables |
|---|---|---|
| 数据来源明确 | 隐式 | 显式(import) |
| 是否有命名冲突问题 | 有 | 无 |
| 逻辑封装 | 全局污染 | 按需引入 |
| Tree-shaking 支持 | 差 | 好 |
| TypeScript 支持 | 差 | 好 |
对于全局混入(Global Mixin),Vue3 虽然提供了 app.mixin(),但不推荐,推荐使用:
app.config.globalProperties。app.provide 在顶层提供数据,组件通过 inject 方法消费数据。函数式组件是一种轻量级、无状态的组件形式。它们很像纯函数:接收 props,返回 虚拟 DOM(vnode)。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this),也不会触发常规的组件生命周期钩子。没有响应式系统、生命周期和实例的开销,函数式组件自然在渲染上更加高效和快速。
总而言之,函数式组件有无状态、无this、无生命周期、性能更高等特点。
使用场景:适合简单、静态的 UI 元素,如列表项或包装组件。
在 Vue 2 中,通过 functional: true 声明;在 Vue 3 中,函数式组件更简单,直接返回渲染函数。
Vue2 示例:
<template functional>
<div>{{ props.msg }}</div>
</template>
或者 js 形式:
export default {
functional: true,
props: ['msg'],
render(h, { props }) {
return h('div', props.msg);
}
};
Vue3 示例:
<script setup>
import { h } from 'vue';
const FunctionalComp = (props) => h('div', props.msg);
</script>
<template>
<FunctionalComp msg="Hello Functional" />
</template>
异步组件是一种懒加载(Lazy Loading)机制,用于按需加载组件代码,优化初始加载时间和性能。Vue 会动态导入组件,只有在使用时才下载和渲染,常用于路由或大型组件。
特点:
import() 动态加载,返回 Promise。defineAsyncComponent 更规范,支持与 <Suspense> 结合(Vue 3 独有,用于统一处理异步)。Vue2 示例:
<script>
export default {
components: {
AsyncComp: () => import('./AsyncComp.vue')
}
};
</script>
<template>
<AsyncComp />
</template>
Vue3 示例:
<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'));
</script>
<template>
<Suspense>
<template #default>
<AsyncComp />
</template>
<template #fallback>加载中...</template>
</Suspense>
</template>
递归组件是指组件内部调用自身,用于处理树形或嵌套数据结构,如菜单、树视图或评论回复。Vue 支持组件自引用,但需注意避免无限循环(通过条件终止递归)。
特点:
name 选项),才能自引用。v-for 和 props 传递数据。Vue 2 示例:
<template>
<ul>
<li v-for="item in tree" :key="item.id">
{{ item.name }}
<Tree v-if="item.children" :tree="item.children" />
</li>
</ul>
</template>
<script>
export default {
name: 'Tree', // 必须有 name
props: ['tree']
};
</script>
Vue 3 示例:
<script setup>
import { defineAsyncComponent } from 'vue'; // 可选:异步加载避免循环
const Tree = defineAsyncComponent(() => import('./Tree.vue')); // 自引用
defineProps(['tree']);
</script>
<template>
<ul>
<li v-for="item in tree" :key="item.id">
{{ item.name }}
<Tree v-if="item.children" :tree="item.children" />
</li>
</ul>
</template>
Vue-loader 是一个专为 Vue.js 设计的 Webpack loader(加载器),其主要作用是将 Vue 的单文件组件(Single-File Components,简称 SFC,即 .vue 文件)转换为可执行的 JavaScript 模块。 它允许开发者以一种结构化的方式编写组件,将模板(template)、脚本(script)和样式(style)封装在同一个文件中,便于管理和维护。
核心功能:
.vue 文件中的三个部分:
<docs> 或其他自定义标签,用于文档生成或其他工具集成。Vue.extend方法可以作为基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
Vue2 示例:
var dialog = Vue.extend({
template: "<div>{{hello}} {{world}}</div>",
data: function () {
return {
hello: "hello",
world: "world",
};
},
});
// 创建 dialog 实例,并手动挂载到一个元素上。
new dialog().$mount("#app");
Vue3 示例:
Vue3 中不在使用 Vue.extend 方法,而是采用render方法进行手动渲染。
<!-- Modal.vue -->
<template>
<div class="modal">这是一个弹窗</div>
</template>
<script>
export default {
name: 'Modal',
}
</script>
<template>
<div id="box"></div>
</template>
<script setup>
import { h, render, createApp, onMounted } from 'vue'
import Modal from './Modal.vue'
onMounted(() => {
render(h(Modal), document.getElementById('box'));
})
</script>
<keep-alive> 是 Vue.js 的内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例,避免重复渲染和状态丢失,提高性能。它是一个抽象组件(abstract: true),不会渲染到 DOM,也不会出现在组件树中,而是通过插槽(slots)和自定义 render 函数实现缓存逻辑。
核心实现机制:
<keep-alive> 通过 render 函数处理包裹的内容(通常是动态组件,如 <component> 或 v-if 切换的组件)。描述:Vue 3 的响应式系统直接使用 ES6 Proxy 作为代理层,拦截对象操作,实现透明的响应式。 应用:reactive() 返回 Proxy 对象,代理目标对象的访问和修改。 优势:比 Vue 2 的 defineProperty 更强大,支持数组和 Map/Set 等类型。
createComponentInstance 就可以创建实例。<script setup>
import { onMounted, onUnmounted } from 'vue';
let timer = null;
let controller = null;
let raf = null;
onMounted(() => {
// 定时器
timer = setInterval(() => {}, 1000);
// 动画
raf = requestAnimationFrame(() => {});
// 事件监听
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
clearInterval(timer);
cancelAnimationFrame(this.raf);
window.removeEventListener('resize', handleResize);
});
</script>
<script setup>
import { onMounted, onUnmounted } from 'vue';
let chart = null;
onMounted(() => {
chart = echarts.init(this.$refs.chart);
});
onUnmounted(() => {
chart?.dispose();
});
</script>
vue2 用可以用 new Vue 全局创建一个事件总线实例,或者在组件中直接使用 this.$on、this.$emit、this.$off。
vue3 则需要借助第三库,比如 mitt 来实现事件总线。
<script setup>
import { onMounted, onUnmounted } from 'vue';
import mitt from 'mitt';
// 创建事件总线实例
const emitter = mitt();
onMounted(() => {
emitter.on('update', this.handler);
});
onUnmounted(() => {
emitter.off('update', this.handler);
});
</script>
顺便提一下, vue3 为啥去掉 $on、$emit、$off 这些 API,主要有以下原因:
Vue 3 更加注重组件间通信的明确性和可维护性。$on 这类事件 API 本质上是一种 "发布 - 订阅" 模式,容易导致组件间关系模糊(多个组件可能监听同一个事件,难以追踪事件来源和流向)。Vue 3 推荐使用更明确的通信方式,如:
Vue 3 主推的 Composition API 强调逻辑的封装和复用,而 $on 基于选项式 API 的实例方法,与 Composition API 的函数式思维不太契合。移除后,开发者可以更自然地使用响应式变量或第三方事件库来实现类似功能。
Vue 本身不会泄漏内存,泄漏几乎都来自开发者未清理的副作用。养成“创建即清理”的习惯,使用 beforeDestroy 或者 onUnmounted 集中清理,在使用 keep-alive 的组件中,视情况在 deactivated 钩子中清理资源。
以上是整理的 Vue 高级的高频面试题,如有错误或者可以优化的地方欢迎评论区指正,后续还会更新 Vuex 和 Vue-router 相关面试题。