持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
在学习过一段时间的vue2之后,也是浅浅的了解了一些偏原理性的东西,那我们今天就来聊聊如何在vue2中实现数据响应式原理。
响应式原理
响应式从字面上理解就是数据发生改变视图随着改变而改变
,在VUE2.0响应式主要是利用了Object.defineProperty()数据劫持,以及观察者。
Object.defineProperty()
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其中有两个关键的可选键get和set
-
get -
属性的 getter 函数,如果没有 getter,则为
undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。 -
set -
属性的 setter 函数,如果没有 setter,则为
undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。 默认为undefined。
看个例子:
// 在对象中添加一个属性与存取描述符的示例
var obj = {};
var bValue;
Object.defineProperty(obj, "name", {
get : function(){
console.log('数据劫持正在监听name属性')
return bValue
},
set : function(newValue){
console.log('数据劫持正在修改name的属性值')
bValue = newValue;
},
enumerable : true,
configurable : true
});
obj.name = 18;
console.log(obj.name)
运行结果如下:
而在vue2.0中实现响应式的核心原理就是利用了javascript原生的Object.defineProperty()这个API,它只能劫持一个对象;可以重新定义属性,给属性安插 getter setter 方法
updateView
模拟diff算法去比较两个虚拟dom树的改变,来进行视图更新,并不是实现响应式原理的核心,所以就用一个简单的函数模拟一下;(在 Vue 中表现为 template 模板中引用了该变量值的 DOM 元素的变化)
// 检验视图是否更新
function updateView() {
console.log('更新视图');
}
defineReactive
对Object.defineProperty()进行二次封装接受三个参数,监听的目标对象(target)、属性名(key),以及属性值(value),一个target(对象)通过调用 defineReactive 就能够实现对 key(对应属性名)进行监听,类比到 Vue 中:
<script>
export default {
data(){ // data ---> target
name: '前端' // name ---> key
} // '前端'---> value
}
</script>
具体实现:
// 响应式
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
value = newVal
updateView() // 数据更改,视图更新
}
}
})
}
observe
既然只能监听对象,那么我们就需要一个观察者函数,来用于对对象中的每个属性进行监听
function observer(target) {
if (typeof(target) !== 'object' || target == null) {
return target
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
用typeof判断类型时null时特例,不太了解类型判断的可以看看这篇文章JS中令人疑惑的数据类型及其判断方法
如图最基本的响应式原理就实现了,我们修改了数据源的值,视图也相应的更新了
function updateView() {
console.log('更新视图');
}
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
value = newVal
updateView() // 数据更改,视图更新
}
}
})
}
function observer(target) {
if (typeof(target) !== 'object' || target == null) {
return target
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
let data = {
name: 'HTML'
}
observer(data)
console.log(data.name = 'CSS');
深度对象监听
我们再想象一个场景,我们在写vue的项目的时候,数据源里再套对象很常见的对吧,但是嵌套的对象,defineReactive就无法监听到,那我们应该用什么方法来解决呢? 其实只需要使用递归就可以了,在defineReactive内部再调用一次观察者函数就可以,这样就可以循环访问到嵌套的最底层,这样就可以解决嵌套对象的情况了。
// 1.
function updateView() {
console.log('更新视图');
}
function defineReactive(target, key, value) {
observer(value)
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
value = newVal
updateView() // 数据更改,视图更新
}
}
})
}
function observer(target) {
if (typeof(target) !== 'object' || target == null) {
return target
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
let data = {
name: 'HTML',
information: {
tel: '18888888888'
}
}
observer(data)
// console.log(data.name = 'CSS');
console.log(data.information = {tel: '18888888889' });
但是有一个在vue2中我们以上这样封装无法实现给数据源添加没有的属性,因为在监听时,for in 显然遍历不到这个属性,那么就到不了 Object.defineProperty的里面去给它添加属性。
实现数组监听
前面我们已经实现了对象、嵌套对象的所有属性的的监听,但如果某个属性是数组,对数组进行push、pop等操作会触发更新吗?很显然是不会的,因为 Object.defineProperty 并不具备监听数组内部变化的能力,那么我们该如何解决呢————重写数组原型上的方法。
1.定义监听数组的原型 在 JS 中,通常对象都有原型(Object.create(null)创建的对象没有原型),而我们的目的是通过重写数组原型上方法(push、pop等)实现监听,而作为库或是框架,我们都不应该去改变全局原型上的任何原生方法或者属性,污染全局环境,所以,这里分3步:
首先:创建一个对象,将数组对象的原型赋给创建的对象
let oldArrayPrototype = Array.prototype然后:创建新对象,原型指向该对象 (继承)
let proto = Object.create(oldArrayPrototype)最后: 重写该对象上的方法
proto.push = function(){}...
proto.pop = function(){}...
代码实现:
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype) // 继承
Array.from(['push','shift','pop','unshift']).forEach(method => {
proto[method] = function() { // pushxxx 函数劫持, 把函数内部重写
oldArrayPrototype[method].call(this, ...arguments)
updateView()
}
})
2、将需要监听的数组的原型指向自定义的特殊原型
对原来的 observe 进行修改,加入数组判断,如果是数组则修改该数组的原型
function observer(target) {
if (typeof(target) !== 'object' || target == null) {
return target
}
if(Array.isArray(target)) {
// target.__proto__ = proto
Object.setPrototypeOf(target,proto)
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
完整代码:
// 1.
function updateView() {
console.log('更新视图');
}
function defineReactive(target, key, value) {
observer(value)
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
value = newVal
updateView() // 数据更改,视图更新
}
}
})
}
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype) // 继承
Array.from(['push','shift','pop','unshift']).forEach(method => {
proto[method] = function() { // pushxxx 函数劫持, 把函数内部重写
oldArrayPrototype[method].call(this, ...arguments)
updateView()
}
})
function observer(target) {
if (typeof(target) !== 'object' || target == null) {
return target
}
if(Array.isArray(target)) {
// target.__proto__ = proto
Object.setPrototypeOf(target,proto)
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
let data = {
name: 'HTML',
information: {
tel: '18888888888'
},
num: [1,2,3]
}
observer(data)
// console.log(data.name = 'CSS');
// console.log(data.information = {tel: '18888888889' });
data.num.push(4)
缺点:
在性能开销方面,因为可能存在深层对象嵌套,所以需要对对象进行深度遍历,递归到底,需要开销不小的小的性能,如果有大量的层级非常高的对象进行响应式监听的绑定,会极大耗费初始化时的性能,导致拖慢 First Paint Time。