Vue3的响应式到底比Vue2优雅在哪
前言
- 常网IT戳我呀!
- 常网IT源码上线啦!
- 如果是海迷,RED红发剧场版有需要可关注我主页公众号回“海贼王red”领取。
- 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
- 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
- 请问Vue3的响应式到底比Vue2好在哪、以及双向绑定响应式原理是什么?
- 我相信这是面试Vue最经常被问到最多的一道题目。
星期六晚,相亲相爱一家人群拨通了第一次全员视频📹。 哥哥进来的时候说,开家庭会议啊。 父母总觉得我们在外面很忙,不想打扰麻烦我们,其实我们上班wx里当舔狗,下班刷短视频,熬夜通宵玩游戏样样精通,怎么可能会没时间呢。 开了第一次视频,有感而发。
正面试着急背的,我总结一下回答✍。
Vue2的响应式是基于Object.defineProperty
的。但它只对初始对象的属性有监听作用,而对新增的属性无效。
作者也知道有这个缺陷,提供了$set
来帮助我们达到响应式。、
那么Vue3的响应式机制改为用Proxy
。
我觉得proxy比较好的点有:
-
Object.defineProperty 和
Proxy
本质差别是,defineProperty 只能对属性进行劫持,所以出现了需要递归遍历,新增属性需要手动Observe
的问题。 -
Vue3.x 通过 Proxy 代理目标对象,且一开始只代理最外层对象,嵌套对象lazy by default(惰性监听) ,性能会更好( Proxy可以直接监听对象而非属性)
-
数据响应式系统全语言特性支持,添加数组索引修改监听,对象的属性增加和删除。(Proxy可以直接监听数组的变化)
-
Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
-
Proxy搭配Reflect主要是为了语义化,而且方法都一一对应。
其实响应式的原理根据 Data
变化更新 View
。
内部实现了四个步骤分别是:
-
实现一个监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者; -
实现一个订阅器
Dep
,用来收集订阅者,对监听器Observer
和 订阅者Watcher
进行统一管理; -
实现一个订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图; -
实现一个解析器
Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
有空的老爷,呱哥更希望咱能啃下本文靠理解和面试官吹水😁~ 细品,我相信一定能助君一臂之力💪 看不完的,可以先收藏一波。
一、问题剖析
Vue3的响应式到底比Vue2优雅在哪?
在Vue中,响应式这个词在入门的时候就深受喜欢。
我们在Vue项目实战中用得最爽的就是修改数据,模板更新,那面试中我们怎么去回答比较好,我个人觉得可以从以下五个方面去回答这个面试题。
- 先说一下Vue2的响应式原理,以及其缺陷。
- Vue3如何解决Vue2的痛点?
- 扩展:阐述MVVM响应式原理?
- 扩展:自己对响应式的理解?
- 聊到响应式,可以再聊一下,其实v-model也绑定一个响应式数据到视图的操作。
老子说过:道生一,一生二,二生三,三生万物。 个人觉得回答一个问题,如果可以引申出问题不同角度或者与其相关的知识点,那面试官可能会觉得你这个人有广度,思维不局限在当前问题下,可能对你刮目相看。
二、回:Vue2的响应式原理,以及其缺陷?
我们先回答第一个小点,Vue2的响应式原理,以及他的缺陷是什么?
大家都知道Vue2的响应式是基于Object.defineProperty
的。
我们举个Object.defineProperty
的一个例子,更深入了解其弊端。
// 响应式函数
function reactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
console.log(`输入${key}属性`)
return value
},
set(val) {
console.log(`${key}由->${value}->设置成->${val}`)
if (value !== val) value = val
}
})
}
const data = { name: '阿呱' }
Object.keys(data).forEach(key => reactive(data, key, data[key]))
console.log(data.name)
// 输入name属性
// 阿呱
data.name = 'Dignity' // name由->阿呱->设置成->Dignity
console.log(data.name)
// 输入name属性
// Dignity
可以看到,每次获取值都触发get方法,修改值则触发set方法。
那么,他有何缺陷呢?
// 接上
data.do = '打羽毛球'
console.log(data.do) // 打羽毛球
data新加do
属性,访问、设置值,都不会触发get和set方法。
所以缺陷是:Object.defineProperty
只对初始对象的属性有监听作用,而对新增的属性无效。
作者也知道有这个缺陷,为了让开发者能更好的响应式,对象新增的属性修改能有响应式,提供了$set
来帮助我们达到响应式。
2.1 Object.defineProperty不能监控到数组下标的变化?
这是一段小插曲。
🙋面试官说:既然都聊到Object.defineProperty
,那我问你,他本身能不能监控到数组下标的变化?
🙋🏻♂️Object.defineProperty是有监控数组下标变化的能力的,只是 Vue2.x 放弃了这个特性。
有人可能想:不是吧,我们不是无法监听到数组下标的变化,直接设置数组下标,不能响应式,所以Vue才重写了数组的七个方法(push、pop、shift、unshift、splice、sort、reverse)吗?
事实上,Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能 / 体验的性价比考虑,放弃了这个特性。
不信是吧,上例子🌰。
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key: ${key} value: ${value}`)
return value
},
set: function defineSet(newVal) {
console.log(`set key: ${key} value: ${newVal}`)
value = newVal
}
})
}
function observe(data) {
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key])
})
}
let arr = [1, 2, 3]
observe(arr)
通过下标获取某个元素和修改某个元素的值。
可以看到,通过下标获取某个元素会触发 getter
方法, 设置某个值会触发 setter
方法。
接下来,我们再试一下数组的一些操作方法,看看是否会触发。
数组的 push 方法
push 并未触发 setter 和 getter方法,数组的下标可以看做是对象中的 key ,这里 push 之后相当于增加了下索引为 3 的元素,但是并未对新的下标进行 observe ,所以不会触发。
我们的结论是:Object.defineProperty是有监控数组下标变化的能力的。 push是新增,我们的监控是原有数组下标的变化。所以push不会触发get、set。
数组的 unshift 方法
发生了什么?
🙋🏻♂️unshift 往数组前面添加操作会导致原来索引为 0、1、2、3 的值发生变化,这就需要将原来索引为 0、1、2、3 的值取出来,然后重新赋值,所以取值的过程触发了 getter ,赋值时触发了 setter。
整个解析过程是这样子的:
🍅结合上图好好看,可以理解的。
原数组:[1, 2, 3]
1的索引是0;
2的索引是1;
3的索引是2;
push添加了4,他的索引是3;
unshift往头添加0;
原本的索引0、1、2、3 的值发生变化,因为第一位的索引变了,影响第二个,类推
这就需要将原来索引为 0、1、2、3 的值取出来,然后重新赋值,期间触发get、set
因为unshift头部添加了0,所以0的索引是0; -> key: 0, value:0
原本1的索引从0变成了1 -> key: 1, value:1
原本2的索引从1变成了2 -> key: 2, value:2
我们可以看到符合上面的图set的输出。
至于上图get的输出,是将值取出来触发。
至于数组3的值怎么没有打印出来,因为Object.defineProperty
监控是原有数组下标的变化。后面下标的变化与我无关。
下面我们尝试通过索引获取一下对应的元素:
只有索引为 0、1、2 的属性才会触发 getter 。
这里我们可以对比对象来看,arr 数组初始值为 [1, 2, 3],即只对索引为 0,1,2 执行了 observe 方法,所以无论后来数组的长度发生怎样的变化,依然只有索引为 0、1、2 的元素发生变化才会触发。其他的新增索引,就相当于对象中新增的属性,需要再手动 observe
才可以。
数组的 pop 方法
当移除的元素为引用为 2 的元素时,会触发 getter 。
删除了索引为 2 的元素后,再去修改或获取它的值时,不会再触发 setter 和 getter 。
这和对象的处理是同样的,数组的索引被删除后,就相当于对象的属性被删除一样,不会再去触发 observe
。
所以面试官问Object.defineProperty
,他本身能不能监控到数组下标的变化?
答案是有监控数组下标变化的能力的,只是 Vue2.x 放弃了这个特性。
-
通过索引访问或设置对应元素的值时,可以触发 getter 和 setter 方法。
-
通过 push 或 unshift 会增加索引,对于新增加的属性,需要再手动初始化才能被 observe。
-
通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。
🍅Object.defineProperty
在数组中的表现和在对象中的表现是一致的,数组的索引就可以看做是对象中的 key。
可以哦,细狗。
三、回:Vue3如何解决Vue2的痛点?
小插曲细品,让我们回归正题。
接着,说一下Vue3的性能更好,怎么解决无法监听数组变化问题?
我们可以从以下几点回答:
-
Vue2.x 通过给每个对象添加
getter setter
属性去改变对象,实现对数据的观测; -
Object.defineProperty 和
Proxy
本质差别是,defineProperty 只能对属性进行劫持,所以出现了需要递归遍历,新增属性需要手动Observe
的问题。 -
Vue3.x 通过 Proxy 代理目标对象,且一开始只代理最外层对象,嵌套对象lazy by default(惰性监听) ,性能会更好( Proxy可以直接监听对象而非属性)
-
数据响应式系统全语言特性支持,添加数组索引修改监听,对象的属性增加和删除。(Proxy可以直接监听数组的变化)
-
Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
3.1 Proxy只会代理对象的第一层,那么Vue3怎么对深层级进行监听呢?
判断当前Reflect.get
的返回值是否为Object
,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
3.2 监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行触发。
3.3 Proxy属性讲解
const person = { name: '阿呱' }
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
console.log(target) // 原来的person
console.log(key) // 属性名
console.log(receiver) // 代理后的proxyPerson
},
set(target, key, value, receiver) {
console.log(target) // 原来的person
console.log(key) // 属性名
console.log(value) // 设置的值
console.log(receiver) // 代理后的proxyPerson
}
})
proxyPerson.name // 访问属性触发get方法
proxyPerson.name = 'Dignity' // 设置属性值触发set方法
其实Proxy是搭配Reflect的。
来个例子🌰,感受下Proxy的强大吧。
const data = { name: '阿呱' }
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log(`访问了${key}属性`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`${key}由->${target[key]}->设置成->${value}`)
Reflect.set(target, key, value, receiver)
}
}
return new Proxy(target, handler)
}
const proxyData = reactive(data)
console.log(proxyData.name)
// 访问了name属性
// 阿呱
proxyData.name = 'Dignity'
// name由->阿呱->设置成->Dignity
console.log(proxyData.name)
// 访问了name属性
// Dignity
关键的来了,那就是对象新增属性,来看看效果吧:
proxyData.do = '打羽毛球'
console.log(proxyData.do)
// 访问了do属性
// 打羽毛球
proxyData.do = '打篮球'
// do由->打羽毛球->设置成->打篮球
console.log(proxyData.do)
// 访问了do属性
// 打篮球
可以看到,Proxy对新增的do属性也进行监听管理,一视同仁。
而Object.defineProperty
新来的不管,管不了那么多。
3.4 Reflect属性讲解
在这列举Reflect的两个方法:
-
get(target, key, receiver):访问target的key属性,但是this是指向receiver,所以实际是访问的值是receiver的key的值,但是这可不是直接访问receiver[key]属性
-
set(target, key, value, receiver):设置target的key属性为value
上面提到,不能直接receiver[key]或者receiver[key] = value,而是要通过Reflect.get和Reflect.set,绕个弯去访问属性或者设置属性,这是为啥呢?下面咱们举个反例
const person = { name: '阿呱' }
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
return Reflect.get(receiver, key) // 相当于 receiver[key]
},
set(target, key, value, receiver) {
Reflect.set(receiver, key, value) // 相当于 receiver[key] = value
}
})
console.log(proxyPerson.name)
proxyPerson.name = 'Dignity'
// 会直接报错,栈内存溢出 Maximum call stack size exceeded
因为上面的get,返回Reflect.get(receiver, key)
相当于receiver[key]
,又触发到get方法,所以直接就死循环报错了。
🍅Proxy搭配Reflect主要是为了语义化,而且方法都一一对应。
- Proxy的get对应Reflect.get
- Proxy的set对应Reflect.set
🙋为啥尽量把this放在receiver上,而不放在target上?
🙋🏻♂️因为原对象target有可能本来也是是另一个代理的代理对象,所以如果this一直放target上的话,出bug的概率会大大提高。
其实回答到这里,已经回答得不错了。奖励兄弟们鸡腿🍗 但如果可以再补充一下响应式原理,那将是锦上添花🌷呀~ 对面试官别紧张,就像好朋友,想到什么就聊什么,扩展题。 相信面试官也是有朋自远方来,不亦乐乎的心态对待我们。
四、扩展:阐述MVVM响应式原理?
这其实也是另外的一道面试题。
只不过我们在回答到关于响应式的时候,顺便一提,我们要将自己掌握的东西全盘托出在面试中表现出来。(当然,要紧扣题,不要回答一些无关紧要的知识点)
让面试官看,这就是我的实力。
4.1 原理
Vue 内部通过 Object.defineProperty
方法属性拦截的方式,把data
对象里每个数据的读写转化成 getter/setter
,当数据变化时通知视图更新。
🙋那他是怎么进行依赖收集的呢?
🙋🏻♂️其内部定义了一个依赖收集器叫Dep
。
-
vue将
data
初始化为一个Observer
并对对象中的每个值,重写了其中的get、set,data中的每个key
,都有一个独立的依赖收集器。 -
在
get
中,向依赖收集器添加了监听。 -
在
mount
时,实例了一个Watcher
,将收集器的目标指向了当前Watcher
。 -
在
data
值发生变更时,触发set
,触发了依赖收集器中的所有监听的更新,来触发Watcher.update
。
4.2 MVVM
MVVM 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。
即:
-
输入框内容变化时,
Data
中的数据同步变化。即View => Data
的变化。(通过事件监听的方式实现) -
Data
中的数据变化时,文本节点的内容同步变化。即Data => View
的变化。
本文主要讨论如何根据 Data
变化更新 View
。
我们会通过实现以下 4 个步骤,来实现数据的双向绑定:
-
实现一个监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者; -
实现一个订阅器
Dep
,用来收集订阅者,对监听器Observer
和 订阅者Watcher
进行统一管理; -
实现一个订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图; -
实现一个解析器
Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
下面让我们详细讲解一下监听器Observer、订阅器Dep、订阅者Watcher、解析器Compile分别做了什么。
4.3 监听器Observer
监听器 Observer
的实现,主要是指让数据对象变得“可观测”,即每次数据读或写时,我们能感知到数据被读取了或数据被改写了。要使数据变得“可观测”,Vue 2.0 源码中用到 Object.defineProperty()
来劫持各个数据属性的 setter / getter
。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
/**
* 循环遍历数据对象的每个属性
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 将对象的属性用 Object.defineProperty() 进行设置
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了...`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了...`);
val = newVal;
}
})
}
4.4 订阅器 Dep
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。
我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是前一节所说的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
曾经面试官问过我,你知道哪些设计模式,Vue中有用到哪些设计模式? 我们可以回答: Vue在响应式内,使用了发布订阅模式也就是观察者模式,用到这个模式主要是想将所有依赖收集起来,作为订阅者,数据变化就通知所有依赖者,数据变化这个过程称为发布者。
现在,我们需要创建一个依赖收集容器,也就是消息订阅器 Dep
,用来容纳所有的“订阅者”。订阅器 Dep 主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。
创建消息订阅器 Dep
:
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null; // 留下伏笔,下面回答为什么要设为null
有了订阅器,我们再将 defineReactive
函数(上面【4.3 监听器Observer】的监听器一个方法)进行改造一下,向其植入订阅器:
defineReactive: function(data, key, val) {
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
从代码上看,我们设计了一个订阅器 Dep 类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 Dep.target
,这是一个全局唯一 的Watcher
,因为在同一时间只能有一个全局的 Watcher被计算,另外它的自身属性 subs
也是 Watcher 的数组。(请结合以上代码观看理解)
为什么在订阅器 Dep
类后面还要设置Dep.target = null
?
订阅者watchs
会先将自身的this
赋值给Dep.target
,这样就会走进get
往dep添加sub,即dep.addSub()
想要添加到订阅器中,后面添加完记得将Dep.target
设置为null
,不然每次都会走进get
往dep添加sub(因为Dep.target有值会进入)
懂了吧,就是怕重复push。
4.5 订阅者 Watcher
订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep
中,那该如何添加呢?
我们已经知道监听器Observer 是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作即可。
🙋那要如何触发 get 的函数?
🙋🏻♂️再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了 Object.defineProperty()
进行数据监听。
这里还有一个细节点需要处理,我们只要在订阅者 Watcher
初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在 Dep.target
上缓存下订阅者,添加成功后再将其去掉就可以了。
订阅者 Watcher
的实现如下:
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 全局变量 订阅者 赋值
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 全局变量 订阅者 释放
return value;
}
};
对以上订阅者 Watcher 代码分析:
订阅者 Watcher 是一个 类,在它的构造函数中,定义了一些属性:
vm
:一个 Vue 的实例对象;exp
:是 node 节点的 v-model 等指令的属性值 或者插值符号中的属性。如:v-model="name"
,exp 就是name;cb
:是 Watcher 绑定的更新函数;
当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,即构造函数中:this.value = this.get()
。
进入 get 函数,首先会执行:
Dep.target = this; // 将自己赋值为全局的订阅者
实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了:
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数,因为你获取data的值了
在这个过程中会对 vm
上的数据访问,其实就是为了触发数据对象的 getter
。
每个对象值的 getter 都持有一个 dep
,在触发 getter 的时候会调用 dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的 watcher订阅者 订阅到这个数据持有的 dep 的 watchers 中,这个目的是为后续数据变化时候能通知到哪些 watchers 做准备。
简而言之,就是想把订阅者添加到订阅器中,方便到时候订阅器统一通知订阅者更新。
这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即:
Dep.target = null; // 释放自己
而 update()
函数是用来当数据发生变化时调用 Watcher
订阅者 自身的更新函数进行更新的操作。
先通过Watcher订阅者的run
方法中: let value = this.vm.data[this.exp];
获取到最新的数据,然后将其与之前 get() 获得的旧数据进行比较,如果不一样,则调用更新函数 cb
进行更新。
4.6 解析器 Compile
通过监听器 Observer 订阅器 Dep 和订阅者 Watcher 的实现,其实就已经实现了一个双向数据绑定的例子。
但是整个过程都没有去解析 dom
节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile
来做解析和绑定工作。解析器 Compile 实现步骤:
- 解析模板指令,并替换模板数据,初始化视图;
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器;
我们下面对 '{{变量}}'
这种形式的指令处理的关键代码进行分析,感受解析器 Compile 的处理逻辑,关键代码如下:
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp]; // 获取属性值
this.updateText(node, initText); // dom 更新节点文本值
// 将这个指令初始化为一个订阅者,后续 exp 改变时,就会触发这个更新回调,从而更新视图
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);
});
}
多看几遍,以咱超群的智商,是可以懂的👍。
五、扩展:自己对响应式的理解?
你都把响应式原理和面试官吹了,不得说一下自己的理解?
我知道大家意犹未尽,那我们再说一下自己的理解吧。
我个人觉得可以从以下五个方面去回答。
-
响应式是什么?
-
为什么需要响应式?
-
它能给我们带来什么好处?
-
vue的响应式是怎么实现的?有哪些优缺点?
-
vue3中的响应式的新变化
回
第一:是什么
所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
第二:为什么需要响应式
MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
第三:有什么好处
以vue为例说明,通过数据响应式加上虚拟DOM和patch
算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。
对于patch,后期会出一篇文章。
第四:实现、优缺点
vue2中的数据响应式会根据数据类型来做不同处理。
如果是对象则采用Object.defineProperty() 的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是 数组则通过覆盖数组对象原型的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题。
但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete
这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。
第五:vue3变化
为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy
代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity
包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。
六、扩展:v-model其实响应式的一个实践
聊到响应式,可以再聊一下,其实v-model也是绑定一个响应式数据到视图的操作。
input输入框监听input
事件修改值,通过Object.defineProperty
劫持数据发生的改变,如果数据发生改变了(在set进行赋值的),触发update
方法进行更新节点的内容({{str}}),从而实现了数据双向绑定的原理。
详细问题回答在实战v-model如何绑定多循环表达式(内含原理)
面试官摸了摸胡子,顺便摸了摸我那八块腹肌,明天来报道。
后记
所以知道为什么同样的问题,有的人可以拿10k,有的可以拿20k,看你是敷衍的回答还是深层的回答,都是在面试过程中贴上标签了,中级还是高级。
最后,祝君能拿下满意的offer。
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
相关文献
为什么Vue3.0不再使用defineProperty实现数据监听?
原文链接
转载自:https://juejin.cn/post/7168274276787683341