Vue 2的响应式原理主要是基于Object.defineProperty
来实现的。
-
数据劫持
- 当一个Vue实例被创建时,它会遍历
data
选项中的所有属性。对于每个属性,使用Object.defineProperty
来进行数据劫持。这个方法允许精确地定义一个对象的属性,包括属性的值、可枚举性、可配置性和可写性。 - 重点是定义了属性的
get
和set
函数。get
函数用于获取属性的值,在这个函数内部可以进行一些依赖收集的操作。例如,当模板中使用了这个属性,就会在get
被调用时,将这个模板中的使用点(watcher)收集起来。set
函数用于设置属性的值,当属性的值被修改时,set
函数会被触发。在set
函数内部,会通知之前收集到的所有依赖(watcher)进行更新,从而实现数据变化到视图更新的过程。
- 当一个Vue实例被创建时,它会遍历
-
依赖收集和通知更新(Watcher和Dep)
- Dep(Dependency):可以看作是一个依赖收集器,是一个发布者。它主要用于收集依赖(watcher),在数据劫持的
get
中,会将当前的watcher添加到Dep
实例中。每个响应式数据都会有一个对应的Dep
实例。 - Watcher:是一个订阅者,主要用于观察数据的变化。它可以是一个组件渲染的观察者,也可以是一个用户自定义的计算属性的观察者等。当数据发生变化时,
Dep
会通知所有订阅了它的Watcher
,Watcher
收到通知后,会执行相应的更新操作,如更新组件的DOM节点,重新计算计算属性的值等。
- Dep(Dependency):可以看作是一个依赖收集器,是一个发布者。它主要用于收集依赖(watcher),在数据劫持的
简单来说,当数据发生变化时,通过set
触发Dep
发布消息,Watcher
收到消息后更新视图,而视图中的数据绑定表达式被解析时,会触发get
进行依赖收集,这就实现了数据和视图之间的响应式绑定。
在Object.defineProperty()
方法中,用于定义对象属性时可以配置以下参数:
- value(可选)
- 这是属性的值。例如:
javascript">let obj = {};
Object.defineProperty(obj, 'name', {value: 'John'
});
console.log(obj.name); // 输出:John
- 如果没有提供
value
参数,并且在对象中该属性不存在,那么这个属性的值将是undefined
。
- writable(可选)
- 它是一个布尔值,用于确定属性的值是否可以被修改。默认值是
false
。例如:
- 它是一个布尔值,用于确定属性的值是否可以被修改。默认值是
javascript">let obj = {};
Object.defineProperty(obj, 'age', {value: 30,writable: true
});
obj.age = 31;
console.log(obj.age); // 输出:31
- 如果
writable
为false
,尝试修改属性的值将会在严格模式下抛出错误,在非严格模式下修改操作会被忽略。
- enumerable(可选)
- 这也是一个布尔值,用于决定属性是否可以被枚举,比如在
for...in
循环或者Object.keys()
方法中是否会出现。默认值是false
。例如:
- 这也是一个布尔值,用于决定属性是否可以被枚举,比如在
javascript">let obj = {};
Object.defineProperty(obj, 'city', {value: 'New York',enumerable: true
});
for (let key in obj) {console.log(key); // 输出:city
}
- 如果
enumerable
为false
,该属性在上述枚举操作中不会出现,但仍然可以通过直接访问属性名来获取其值。
- configurable(可选)
- 同样是布尔值,它决定了这个属性是否可以被删除,以及是否可以重新配置属性的其他特性(除了
value
和writable
被设置为false
后的情况)。默认值是false
。例如:
- 同样是布尔值,它决定了这个属性是否可以被删除,以及是否可以重新配置属性的其他特性(除了
javascript">let obj = {};
Object.defineProperty(obj, 'country', {value: 'USA',configurable: true
});
delete obj.country;
console.log(obj.country); // 输出:undefined
- 如果
configurable
为false
,尝试删除属性或者重新配置其特性(如改变enumerable
等)将会在严格模式下抛出错误,在非严格模式下操作会被忽略。
以下是一些Vue上层相关的底层面试题:
一、关于Vue实例
- Vue实例的生命周期钩子函数
- 问题:请简述Vue实例的生命周期钩子函数有哪些,以及它们分别在什么时候被调用?
- 答案:
beforeCreate
:在实例初始化之后,数据观测 (data observer) 和event/watcher
事件配置之前被调用。此时,data
、methods
、computed
等都还不可用。created
:在实例创建完成后被调用。此时实例已完成以下的配置:数据观测 (data observer)、属性和方法的运算、watch/event
事件回调。然而,挂载阶段还没开始,$el
不可用。beforeMount
:在挂载开始之前被调用。相关的render
函数首次被调用。mounted
:el
被新创建的vm.$el
替换,并挂载到实例上去之后调用。一般在此进行DOM操作的初始化,例如获取元素的高度、宽度等操作,因为此时DOM已经渲染完成。beforeUpdate
:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。此时DOM还没更新。updated
:由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时DOM已经更新。beforeDestroy
:实例销毁之前调用。在这一步,实例仍然完全可用。destroyed
:Vue实例销毁后调用。此时,Vue实例的所有指令都被解绑,所有的事件监听器被移除,子实例也被销毁。
- Vue实例的数据代理原理
- 问题:请解释Vue是如何实现数据代理的?
- 答案:
- Vue通过
Object.defineProperty()
方法来实现数据代理。在Vue实例创建时,它会遍历data
选项中的属性。 - 对于每个属性,它会在
Vue
实例上定义一个同名的属性(如this.foo
对应data
中的foo
属性)。 - 这个同名属性的
get
和set
访问器函数被定义为读取和修改data
中对应属性的值。当读取this.foo
时,实际上是调用data
中foo
属性的get
访问器;当修改this.foo
时,是调用data
中foo
属性的set
访问器。
- Vue通过
二、关于Vue组件
- 组件通信方式
- 问题:Vue组件有哪些通信方式?请举例说明。
- 答案:
- 父子组件通信:
- 父传子(props):父组件通过
props
向子组件传递数据。例如,父组件有一个message
属性,在子组件中通过props: ['message']
接收,然后在子组件的模板中可以使用{{message}}
显示。 - **子传父( e m i t ) ∗ ∗ :子组件通过 ‘ emit)**:子组件通过` emit)∗∗:子组件通过‘emit
触发事件向父组件传递数据。例如,子组件中有一个按钮,点击按钮时
this.$emit(‘child - event’, data),父组件通过
@child - event="parentMethod"监听这个事件,并在
parentMethod`方法中接收子组件传递的数据。
- 父传子(props):父组件通过
- 非父子组件通信(兄弟组件或跨多层级组件通信):
- 中央事件总线(Event Bus):创建一个Vue实例作为事件总线。例如,
var bus = new Vue()
。一个组件可以通过bus.$emit('event - name', data)
发送事件,其他组件通过bus.$on('event - name', callback)
接收事件和数据。 - Vuex:对于大型项目中复杂的状态管理和组件通信,使用Vuex。它有
state
(存储状态)、mutations
(修改状态的方法)、actions
(异步操作)和getters
(获取状态的派生数据)。组件可以通过this.$store.state
访问状态,通过this.$store.commit('mutation - name', data)
提交修改状态的操作等。
- 中央事件总线(Event Bus):创建一个Vue实例作为事件总线。例如,
- 父子组件通信:
- 组件的插槽(Slots)
- 问题:请解释Vue组件中的插槽有哪些类型,以及它们的作用?
- 答案:
- 默认插槽(单个插槽):
- 当在父组件中使用子组件时,如果在子组件标签内放置内容,这些内容会被渲染到子组件中的
<slot>
标签所在位置。例如,子组件ChildComponent
中有一个<slot></slot>
,父组件<ChildComponent>这是父组件传递的内容</ChildComponent>
,“这是父组件传递的内容”就会被渲染到子组件的<slot>
处。
- 当在父组件中使用子组件时,如果在子组件标签内放置内容,这些内容会被渲染到子组件中的
- 具名插槽:
- 当子组件有多个不同位置需要父组件填充内容时,可以使用具名插槽。在子组件中定义
<slot name="header"></slot>
和<slot name="footer"></slot>
等。父组件使用<template v - slot:header>
和<template v - slot:footer>
来分别向对应的具名插槽传递内容。
- 当子组件有多个不同位置需要父组件填充内容时,可以使用具名插槽。在子组件中定义
- 作用域插槽:
- 作用域插槽用于让父组件能够访问子组件中的数据。子组件在
<slot>
标签上绑定数据,如<slot :data="childData"></slot>
。父组件通过<template v - slot="slotProps">
(slotProps
是自定义的变量名)来接收子组件传递的数据,并在父组件中可以根据这些数据进行渲染,如{{slotProps.childData}}
。
- 作用域插槽用于让父组件能够访问子组件中的数据。子组件在
- 默认插槽(单个插槽):
三、关于Vue的渲染机制(除了Diff算法相关)
- 模板编译过程
- 问题:简述Vue模板的编译过程。
- 答案:
- 解析(parse):将模板字符串解析成抽象语法树(AST)。这个过程会把模板中的HTML标签、指令、插值表达式等解析成JavaScript对象的形式。例如,模板
<div>{{message}}</div>
会被解析成一个包含tag: 'div'
、text: null
、children: [ { type: 2, expression:'message', text: '{{message}}' } ]
等属性的AST对象。 - 优化(optimize):对解析后的AST进行优化,标记静态节点。静态节点是指在组件的生命周期内不会改变的节点,标记静态节点可以在后续的虚拟DOM更新过程中跳过这些节点的比较,提高性能。
- 生成(generate):将优化后的AST转换成渲染函数(
render
函数)。渲染函数返回虚拟DOM(VNode),例如with(this){return _c('div',[_v(_s(message))])}
,其中_c
、_v
、_s
等是Vue内部的渲染函数助手,用于创建VNode、创建文本节点、将数据转换为文本等操作。
- 解析(parse):将模板字符串解析成抽象语法树(AST)。这个过程会把模板中的HTML标签、指令、插值表达式等解析成JavaScript对象的形式。例如,模板
- 渲染函数(Render Function)与模板的区别和联系
- 问题:请说明Vue中渲染函数与模板的区别和联系,以及在什么情况下会选择使用渲染函数?
- 答案:
- 区别:
- 语法形式:模板是基于HTML的语法,通过指令(如
v - if
、v - for
等)来添加逻辑;渲染函数是纯JavaScript函数,通过调用Vue内部的渲染函数助手来创建虚拟DOM。 - 灵活性:渲染函数比模板更灵活。模板有一定的语法限制,而渲染函数可以实现更复杂的逻辑和渲染效果。例如,在模板中很难实现根据不同条件动态生成不同的HTML结构,但在渲染函数中可以通过JavaScript的条件判断来实现。
- 语法形式:模板是基于HTML的语法,通过指令(如
- 联系:
- 模板最终会被编译成渲染函数。无论是使用模板还是直接编写渲染函数,目的都是生成虚拟DOM来渲染页面。
- 选择使用渲染函数的情况:
- 当需要实现高度定制化的组件,模板的语法无法满足需求时,例如需要根据复杂的业务逻辑动态生成DOM结构。
- 当需要对组件的渲染性能进行极致优化时,因为渲染函数可以更精确地控制虚拟DOM的生成和更新,通过优化渲染函数可以减少不必要的DOM操作。
- 区别: