一、Vue 2 和 Vue 3 响应式原理的区别
实现方式
- Vue 2:采用
Object.defineProperty()
方法来实现响应式。当一个 Vue 实例创建时,Vue 会遍历data
选项中的所有属性,使用Object.defineProperty()
将这些属性转换为getter/setter
。这样,当这些属性的值发生变化时,Vue 能够自动检测到并更新与之绑定的 DOM。
// 模拟 Vue 2 响应式原理
const obj = {};
let value = 0;
Object.defineProperty(obj, 'count', {
get() {
console.log('Getting value');
return value;
},
set(newValue) {
console.log('Setting value');
value = newValue;
}
});
obj.count = 1; // 触发 set 方法
console.log(obj.count); // 触发 get 方法
- Vue 3:使用 ES6 的
Proxy
对象来实现响应式。Proxy
可以拦截对象的各种操作,包括属性的读取、设置、删除等。Vue 3 通过创建一个Proxy
代理对象来包装原始对象,当对代理对象进行操作时,Vue 可以捕获这些操作并做出相应的响应。
// 模拟 Vue 3 响应式原理
const target = { count: 0 };
const handler = {
get(target, key) {
console.log('Getting value');
return target[key];
},
set(target, key, newValue) {
console.log('Setting value');
target[key] = newValue;
return true;
}
};
const proxy = new Proxy(target, handler);
proxy.count = 1; // 触发 set 拦截器
console.log(proxy.count); // 触发 get 拦截器
性能
- Vue 2:
Object.defineProperty()
是一个相对较旧的 API,在处理大规模数据时性能较差。因为它是在对象属性被定义时进行劫持,对于嵌套对象,需要递归地进行Object.defineProperty()
转换,这会导致初始化时的性能开销较大。此外,对于新增或删除属性,Vue 2 无法自动检测到,需要使用Vue.set()
和Vue.delete()
方法来手动处理。 - Vue 3:
Proxy
是 ES6 引入的新特性,它可以直接拦截对象的操作,不需要递归处理嵌套对象,因此在初始化时的性能更好。同时,Proxy
可以拦截对象的属性新增和删除操作,使得 Vue 3 能够自动响应这些变化,无需手动处理。
灵活性
- Vue 2:由于
Object.defineProperty()
的限制,Vue 2 对对象属性的劫持是静态的,即一旦对象创建完成,就无法动态地添加或删除响应式属性。这在某些场景下会带来不便,例如在运行时动态添加属性时,需要手动调用Vue.set()
方法。 - Vue 3:
Proxy
提供了更强大的灵活性。它可以拦截对象的各种操作,包括属性的新增、删除、访问等,使得 Vue 3 能够更自然地处理动态属性。开发者可以直接对对象进行属性的添加和删除操作,Vue 3 会自动更新与之绑定的 DOM。
兼容性
- Vue 2:
Object.defineProperty()
是 ES5 的特性,兼容性较好,支持所有现代浏览器以及部分旧版本浏览器。 - Vue 3:
Proxy
是 ES6 的特性,不支持 Internet Explorer 等旧版本浏览器。如果需要支持这些浏览器,需要使用 Babel 等工具进行编译转换。
二、ref 和 reactive 的作用以及区别
ref
和 reactive
都可用于创建响应式数据,不过它们的使用场景和特点存在差异。
1. ref
- 定义:
ref
用于创建一个响应式的引用对象。它接收一个初始值作为参数,返回一个包含该值的响应式对象,这个对象有一个.value
属性来访问和修改其值。 - 使用场景:主要用于基本数据类型(如
number
、string
、boolean
等)的响应式处理,也能用于引用复杂数据类型。
2. reactive
- 定义:
reactive
用于创建一个响应式对象。它接收一个普通对象作为参数,返回一个响应式代理对象。对代理对象的任何修改都会自动更新与之绑定的 DOM。 - 使用场景:主要用于处理复杂数据类型(如对象、数组等)的响应式处理。
三、Computed 和 Watch 的区别
特性 | Computed(计算属性) | Watch(侦听器) |
---|---|---|
用途 | 基于依赖数据动态计算一个新值(适合派生数据) | 监听某个数据的变化并执行副作用操作 |
缓存机制 | 有缓存,依赖不变时直接返回缓存值 | 无缓存,每次变化都会触发回调 |
返回值 | 必须返回一个值(用于模板或逻辑) | 无返回值,用于执行异步或复杂逻辑 |
异步支持 | 不能直接处理异步操作 | 可以处理异步操作(如 API 请求) |
语法 | 声明为函数(自动追踪依赖) | 声明为对象,需指定监听目标和回调函数 |
适用场景 | 模板中的复杂计算、数据过滤、依赖计算 | 数据变化时执行副作用(如日志、请求、DOM 操作) |
四、Vue 编译原理
模板解析(Template Parsing)
-
作用:把 Vue 模板字符串解析成抽象语法树(AST)。AST 是一种以树状结构表示代码语法结构的抽象形式,树中的每个节点代表模板中的一个语法元素。
-
实现步骤:
- 词法分析:借助词法分析器把模板字符串拆分成一个个词法单元(Token)。例如,对于模板
<div id="app">{{ message }}</div>
,词法分析后会得到<div
、id="app"
、>
、{{ message }}
、</div>
等词法单元。 - 语法分析:语法分析器依据词法单元和 Vue 模板的语法规则构建 AST。它会分析词法单元之间的关系,判断是否符合语法规则,进而生成对应的 AST 节点和树结构。
- 词法分析:借助词法分析器把模板字符串拆分成一个个词法单元(Token)。例如,对于模板
AST 优化(AST Optimization)
-
作用:对解析得到的 AST 进行优化,标记出其中的静态节点。静态节点是指那些在渲染过程中不会发生变化的节点,例如纯文本节点或不包含动态绑定的元素节点。通过标记静态节点,可以在后续的渲染过程中跳过这些节点的重新渲染,从而提高性能。
-
实现步骤:
- 深度优先遍历:从根节点开始,对 AST 进行深度优先遍历,检查每个节点是否为静态节点。
- 标记静态节点:如果节点是静态节点,则标记该节点及其所有子节点为静态节点。在后续的渲染过程中,这些静态节点将不会被重新渲染。
代码生成(Code Generation)
-
作用:将优化后的 AST 转换为渲染函数代码。渲染函数是一个返回虚拟 DOM(VNode)的 JavaScript 函数,虚拟 DOM 是真实 DOM 的抽象表示。
-
实现步骤:
- 递归生成代码:从根节点开始,递归地将 AST 节点转换为对应的 JavaScript 代码。对于不同类型的节点,生成不同的代码逻辑。例如,对于元素节点,生成创建元素的代码;对于文本节点,生成创建文本的代码。
- 生成渲染函数:将生成的代码组合成一个完整的渲染函数。在 Vue 实例初始化时,会调用这个渲染函数来生成虚拟 DOM。
总结
Vue 的编译过程可以概括为:模板字符串 -> 抽象语法树(AST) -> 优化后的 AST -> 渲染函数 -> 虚拟 DOM -> 真实 DOM。通过这种方式,Vue 实现了模板的编译和渲染,同时通过 AST 优化提高了渲染性能。
五、Vue 中 patch 函数的作用
-
作用:
- 比较新旧 VNode,将差异应用到真实 DOM
- 实现于
@vue/runtime-core
-
流程:
-
首次调用:创建真实 DOM
-
后续调用:比较差异并更新
-
示例简化逻辑:
function patch(oldVNode, newVNode, container) { if (!oldVNode) { // 首次渲染:创建真实 DOM mountElement(newVNode, container) } else { // 更新:比较差异 if (oldVNode.type !== newVNode.type) { // 节点类型变化:替换节点 replaceVNode(oldVNode, newVNode) } else { // 节点类型相同:更新属性和子节点 patchProps(oldVNode.props, newVNode.props) patchChildren(oldVNode.children, newVNode.children) } } }
-