由于面试中多次被问到 Vue的响应式原理,双向绑定原理。这两个概念还是有一定区别的,但是面试过程中被面试官问到,还是会混为一谈来回答,故写一篇文章来总结一下这个问题,顺便学习一下vue的底层原理。
响应式原理
响应性是一种可以使我们声明式地处理变化的编程范式。举个例子:A=B+C,当C修改了,A也会随C的变化修改。
响应式原理是Vue的核心机制,当我们修改数据时,视图也会随数据变化更改。
Vue2
我们把一个对象传入Vue实例作为data选项,Vue会遍历所有对象的property(属性),并使用Object.defineProperty把这些property全部转化为getter/setter,在内部它们让Vue能够追踪依赖,在property被访问和修改时通知变更。
每个组件实例都对应一个watcher实例,它会在组件渲染过程中把“接触”过的数据property记录为依赖。之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。
Object.defineProperty的劫持机制
// 数据劫持实现示例
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend() // 依赖收集
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 触发更新
}
})
}
依赖收集系统
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
简要代码
// Dep module
class Dep {
static stack = []
static target = null
deps = null
constructor() {
this.deps = new Set()
}
depend() {
if (Dep.target) {
this.deps.add(Dep.target)
}
}
notify() {
this.deps.forEach(w => w.update())
}
static pushTarget(t) {
if (this.target) {
this.stack.push(this.target)
}
this.target = t
}
static popTarget() {
this.target = this.stack.pop()
}
}
// reactive
function reactive(o) {
if (o && typeof o === 'object') {
Object.keys(o).forEach(k => {
defineReactive(o, k, o[k])
})
}
return o
}
function defineReactive(obj, k, val) {
let dep = new Dep()
Object.defineProperty(obj, k, {
get() {
dep.depend()
return val
},
set(newVal) {
val = newVal
dep.notify()
}
})
if (val && typeof val === 'object') {
reactive(val)
}
}
// watcher
class Watcher {
constructor(effect) {
this.effect = effect
this.update()
}
update() {
Dep.pushTarget(this)
this.value = this.effect()
Dep.popTarget()
return this.value
}
}
// 测试代码
const data = reactive({
msg: 'aaa'
})
new Watcher(() => {
console.log('===> effect', data.msg);
})
setTimeout(() => {
data.msg = 'hello'
}, 1000)
网上刷到一种关于vue响应式原理完美答案的说法:
在生命周期的initState方法中将data,methods,prop,computed,watch中的数据劫持,通过Observe方法将相关对象转换为Observer对象,然后在initRender方法中解析模板,通过Watcher对象,Dep对象与观察者模式将模板中指令与对象的数据建立依赖关系,使用全局对象Dep.target实现依赖效果,当数据变化时,setter被调用,触发Object.defineproperty方法中dep.notify方法,遍历该数据依赖列表,执行器update方法通知Watcher进行视图更新。
虽然我们在回答过程中可以不全部都背下来,但是也可以在描述过程的时候加入源码。
Vue2的监听缺陷
对象
Vue2无法检测property的添加和删除
var vm = new Vue({
data:{
obj:{ a:1 }
}
})
obj.a = 2 //响应式
obj.b=2 // 非响应式
// 解决方式
// 方法 1:使用 Vue.set
Vue.set(vm.obj, 'b', 2);
// 方法 2:通过组件实例的 $set(推荐)
vm.$set(vm.obj, 'b', 2);
// 方法 3:Object.assign这种方法能触发多个属性修改,
// 这种方法其实是让vm.obj的引用地址改变了,所以可以监听到变化
vm.obj = Object.assign({}, vm.obj, { b: 2 });
数组
- Vue2无法检测数组索引
- 无法检测数组长度
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
// 解决方法
// 第一类问题
Vue.set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)
// 第二类问题
vm.items.splice(newLength)
Vue3
副作用(作用):如果一个函数引用了外部的数据,这个函数会受到外部数据改变的影响,我们就说这个函数存在副作用
let A2
function update() {
A2 = A0 + A1
}
这里的update是作用,A0和A1是作用的依赖,因为他们的值被用来执行这个作用。作用也称为依赖的订阅者
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)//收集依赖
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)//触发更新
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
// 这会在一个副作用就要运行之前被设置
// 我们会在后面处理它
let activeEffect
function track(target, key) {
if (activeEffect) {
//副作用订阅将被存储在一个全局的WeakMap<target, Map<key, Set<effect>>>数据结构中。
//如果在第一次追踪时没有找到对相应属性订阅的副作用集合,它将会在这里新建。
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
//执行所有该属性的副作用函数
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
whenDepsChange是魔法函数,能在依赖变化时调用副作用
function whenDepsChange(update) {
const effect = () => {
activeEffect = effect
update()
activeEffect = null
}
effect()
}
它将原本的 update 函数包装在了一个副作用函数中。在运行实际的更新之前,这个外部函数会将自己设为当前活跃的副作用。这使得在更新期间的 track() 调用都能定位到这个当前活跃的副作用。
简要代码
let activeEffect;
function reactive(target) {
return new Proxy(target, {
get(target, key) {
if (activeEffect) {
track(target, key);
}
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
trigger(target, key);
return result;
}
});
}
const targetMap = new WeakMap();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
function effect(fn) {
activeEffect = fn;
fn(); // 执行函数并触发依赖收集
activeEffect = null;
}
// 测试代码
const obj = reactive({ count: 1, double: 0 })
const updateDouble = () => {
obj.double = obj.count * 2
console.log(obj)
}
effect(updateDouble) // 关键步骤!
vue3中使用Reflect操作的原因:
- 确保this指向代理对象
- 保证原型链上的属性也能被响应式系统追踪
- 提供可靠的操作结果,明确的布尔返回值比异常更易处理
- Reflect 方法与 Proxy trap(Proxy陷阱是指在代理对象上可以被拦截的操作) 对应,简化代理实现。例如Reflect.get() 是专门为代理机制设计的,它能够正确触发代理的拦截器,并且在拦截器中正确处理属性访问。直接访问属性可能会绕过代理的拦截器,导致响应式系统失效。
- 支持元编程,为高级功能提供基础
双向绑定
vue2和vue3双向绑定在功能上的区别
- vue2对数组和对象重新赋值需要用特殊的方法,vue3可以直接赋值。
- vue2初始化时递归遍历所有属性,数据变化时全量检查,vue3只有属性被访问时才递归代理深层对象,数据改变时,仅更新实际依赖数据的部分。实现按需劫持和细粒度依赖追踪,缩短初始化时间,减少组件重新渲染范围。
- vue2一个组件只能绑定一个v-model,vue3可以绑定多个。
<!-- Vue2 只能绑定单个 v-model -->
<input v-model="message" />
<!-- Vue3 的多个 v-model -->
<UserForm v-model:name="name" v-model:age="age" />
回归问题,既然我们知道了双向绑定原理和响应式原理的区别,那么面试官问vue2和vue3的绑定原理的区别时我们究竟如何回答?其实,我觉得如果在面试中被提问到vue的双向绑定,可以从上述的双向绑定回答,但是面试官问到vue2和vue3的区别时,从面试的角度上来说,面试官更想考察的是我们对vue底层的了解,而响应式就是双向绑定的核心机制,所以在面试中回答响应式原理的区别应该更合适,当然,如果感觉不放心,还是可以从两方面入手:功能(数据绑定)和原理(响应式系统)
写这篇文章时其实也查阅了很多相关资料,由于许多文章在一些表述上也会有争议的地方,笔者还是按自己的理解做了取舍,如有不对之处,欢迎大家评论区指正。