Vue
双向绑定
双向数据绑定是指数据的变化会自动更新到视图,视图的变化也会自动更新到数据。在Vue2
中,双向数据绑定主要基于 Object.defineProperty()
实现,以 v-model
指令为例,其原理如下:
- 数据劫持:使用
Object.defineProperty()
对数据对象的属性进行劫持,当属性值发生变化时,会触发setter
方法。 - 发布 - 订阅模式:每个数据属性都有一个对应的
Dep
(依赖收集器)对象,当有依赖(如视图中的 DOM 节点)访问该属性时,会将依赖添加到Dep
的订阅者列表中。当属性值改变时,Dep
会通知所有订阅者进行更新。 - DOM 监听:对于表单元素,通过监听其
input
等事件,当用户输入时,更新对应的数据属性。
defineProperty()
的缺点
- 无法检测对象属性的添加和删除:
Object.defineProperty()
只能劫持对象已有的属性,对于新增或删除的属性无法自动劫持,需要使用Vue.set
或Vue.delete
等方法来手动处理。 - 无法检测数组的部分方法:虽然可以劫持数组的
push
、pop
等变异方法,但对于通过索引直接修改数组元素或修改数组长度的操作无法检测,需要使用 Vue 提供的特定方法来更新数组。 - 深层嵌套对象需要递归劫持:对于深层嵌套的对象,需要递归地对每个属性进行劫持,性能开销较大。
与Vue3
的区别
- 响应式原理:Vue 3 使用
Proxy
代替Object.defineProperty()
实现响应式。Proxy
可以劫持整个对象,能够检测对象属性的添加和删除,并且不需要递归劫持,性能更好。 - 兼容性:
Object.defineProperty()
是 ES5 的特性,兼容性较好,但功能有限;Proxy
是 ES6 的特性,在一些旧浏览器中不支持。 - 代码复杂度:Vue 3 的响应式系统代码更简洁,因为
Proxy
可以直接对对象进行拦截,而不需要像Object.defineProperty()
那样对每个属性进行单独处理。
Vue 是如何收集依赖的
1. 创建响应式对象
使用 reactive
函数创建响应式对象,它会返回一个 Proxy
对象。
reactive
函数内部会创建一个 Proxy
对象,拦截对象的 get
和 set
操作。
2. 创建副作用函数
使用 effect
函数创建副作用函数,在副作用函数中访问响应式对象的属性。
当调用 effect
函数时,会将传入的函数封装成一个 Effect
实例,并立即执行该函数。
3. 依赖收集
在副作用函数执行过程中,当访问响应式对象的属性时,会触发 Proxy
的 get
拦截器。
在 track
函数中,会维护一个全局的 targetMap
,它是一个 WeakMap
,用于存储每个响应式对象对应的依赖信息。targetMap
的键是响应式对象,值是一个 Map
,该 Map
的键是对象的属性名,值是一个 Set
,存储依赖于该属性的 Effect
。
4. 触发更新
当响应式对象的属性值发生变化时,会触发 Proxy
的 set
拦截器。
在 trigger
函数中,会从 targetMap
中找到对应的依赖集合,然后遍历该集合,重新执行每个 Effect
。
slot 是什么?有什么作用?原理是什么?
- 定义:
slot
即插槽,是 Vue 提供的一种内容分发机制,用于在组件中预留位置,让父组件可以向子组件传递内容。分为:默认插槽、具名插槽、作用域插槽。
- 默认插槽:又名匿名插槽,当
slot
没有指定name
属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。 - 具名插槽:带有具体名字的插槽,也就是带有
name
属性的slot
,一个组件可以出现多个具名插槽。 - 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
-
作用:
- 提高组件的复用性:通过插槽,组件可以根据不同的使用场景插入不同的内容。
- 实现组件的定制化:父组件可以根据需要自定义子组件的部分内容。
-
原理:
- 编译阶段:Vue 编译器会将组件模板中的
slot
标签编译成特殊的占位符。 - 渲染阶段:在渲染子组件时,会将父组件传递的内容替换到
slot
占位符的位置。
- 编译阶段:Vue 编译器会将组件模板中的
对 keep-alive
的理解,它是如何实现的,具体缓存的是什么?
-
理解:
keep-alive
是 Vue 提供的一个内置组件,用于缓存动态组件或路由组件,避免重复渲染,提高性能。 -
实现原理:
- 缓存机制:
keep-alive
内部维护一个cache
对象,用于存储缓存的组件实例。当组件被keep-alive
包裹时,会将组件实例缓存到cache
中。 - 生命周期钩子:被缓存的组件在激活和失活时会触发
activated
和deactivated
生命周期钩子,而不是mounted
和destroyed
。
- 缓存机制:
-
缓存内容:
keep-alive
缓存的是组件的实例,包括组件的状态和 DOM 结构。
Vue 模版编译原理
- 解析阶段:将模板字符串解析成抽象语法树(AST),Vue 使用正则表达式和状态机来实现解析。
- 优化阶段:对 AST 进行优化,标记出静态节点(在渲染过程中不会发生变化的节点),减少重新渲染的开销。
- 生成阶段:将优化后的 AST 转换为渲染函数,渲染函数返回虚拟 DOM。
虚拟 DOM
对虚拟 DOM 的理解
虚拟 DOM 是一种轻量级的 JavaScript 对象,它是真实 DOM 的抽象表示。虚拟 DOM 通过 JavaScript 对象的形式来描述真实 DOM 的结构和属性,通过对比新旧虚拟 DOM 的差异,只更新需要更新的真实 DOM 部分,从而提高渲染性能。
虚拟 DOM 不一定比真实 DOM 快
虚拟 DOM 的优势在于减少不必要的 DOM 操作,在数据变化频繁的情况下,通过批量更新可以提高性能。但在以下情况下,虚拟 DOM 可能并不比真实 DOM 快:
- 简单场景:对于简单的页面,直接操作真实 DOM 可能更高效,因为虚拟 DOM 需要额外的计算和对比过程。
- 首次渲染:首次渲染时,虚拟 DOM 需要先创建虚拟节点,再将其转换为真实 DOM,可能会比直接操作真实 DOM 慢。
虚拟 DOM 解析过程
- 创建虚拟 DOM:根据组件的模板和数据,通过渲染函数创建虚拟 DOM 对象。
- 对比差异:当数据发生变化时,创建新的虚拟 DOM 对象,并与旧的虚拟 DOM 对象进行对比,找出差异。
- 更新真实 DOM:根据差异,只更新需要更新的真实 DOM 部分。
DIFF 算法原理
-
同层比较:Vue 的 DIFF 算法采用同层比较的策略,只比较同一层级的节点,不会跨层级比较。
-
key 的作用:为节点添加唯一的
key
可以帮助 DIFF 算法更高效地识别节点的变化,避免不必要的节点重新创建。 -
比较策略:
- 节点类型不同:直接替换节点。
- 节点类型相同:比较节点的属性和子节点,只更新有差异的部分。