写在前面
阅读本文需要先了解Object.defineProperty知识,不了解的移步咱之前的文章:Vue源码探索之知识小储备 ——01.Object.defineProperty VS proxy
🌰 先看一段小例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return val
},
set(newVal) {
if (val !== newVal) {
val = newVal
update()
}
}
})
}
const obj = {}
defineReactive(obj, 'foo', new Date().toLocaleString())
setInterval(() => {
obj.foo = new Date().toLocaleString()
}, 1000)
function update() {
document.getElementById('app').innerHTML = obj.foo
}
</script>
</html>
这样我们就实现了一个页面数据跟随数据变化而刷新的案例。这也是vue实现响应式的核心原理。但是往往我们开发中的对象没有这么纯粹,大多多级嵌套对象的情况,也有数组的情况。所以下面我们来做更深层级的解析。
🍀 基本实现
核心思想:递归遍历传入obj,定义每个属性的拦截
// val是形参 但是却可在外部访问 --闭包
function defineReactive (obj, key, val) {
// 递归遍历
observe(obj[key])
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
if (val !== newVal) {
// 如果newVal是对象,也要对其进行社会主义教育
observe(newVal)
update()
val = newVal
}
}
})
}
function observe (obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
Object.keys(obj).forEach(key => {
// 对obj每个key执行拦截
defineReactive(obj, key, obj[key])
})
}
function update() {
console.log('这里数据发生了改变,进行视图更新')
}
const obj = {
a: '120',
b: '555',
c: {
d: '1231'
}
}
observe(obj)
// 普通更新
obj.a = 5
console.log(obj.a)
// 嵌套属性更新
obj.c.d = 'kjk1'
console.log(obj.c.d)
// 赋值是对象时
obj.c.d = {k: '卡卡'}
obj.c.d.k = 'test'
console.log(obj.c.d.k)
🐯 数组的情况
如果是数组,是需要写hack方法的,重新定义数组的七个方法:
const oldArrayProperty = Array.prototype
const arrayProto = Object.create(oldArrayProperty)
//从数组原型中获取这7个方法,并覆盖为可以发送更新通知的函数实现
const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse']
methodsToPatch.forEach(methodName => {
console.log(oldArrayProperty[methodName])
Object.defineProperty(arrayProto, methodName, {
value () {
console.log('you change the array')
update()
oldArrayProperty[methodName].apply(this, arguments)
}
})
})
对observe中的数组的情况则替换数组的原型
function observe (obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
return
}
Object.keys(obj).forEach(key => {
// 对obj每个key执行拦截
defineReactive(obj, key, obj[key])
})
}
🌷特殊情况
你一定遇到过,如果在vue的data中没有定义某一个属性直接给其通过this.data.XX=XXX进行赋值时,当这个属性发生改变时,页面并不会更新。这是由于 Vue 会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性遍历添加 getter/setter 方法,所以属性必须在 data 对象上存在时才能进行上述过程 ,这样才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行过上述过程,不是响应式的,所以会出想数据变化,页面不变的情况。此时Vue官方给提供了$set这个API。
我们这里也给实现下,其实非常简单:
function $set (obj, key, val) {
defineReactive(obj, key, val)
}
set(obj, 'ff', '的活动')
obj.ff = 'ddpm'
console.log(obj.ff)
🍂 简版vue之数据响应式
结合以上的实践,对比源码的函数拆分及命名整理出一份简版源码如下:
// 给一个obj定义一个响应式的属性
function defineReactive (obj, key, val) {
// 递归
// val如果是个对象,就需要递归处理
observe(val)
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
if (newVal !== val) {
console.log('set', key, val)
val = newVal
// 新值如果是对象,仍然需要递归遍历处理
observe(newVal)
}
}
})
}
class MVue {
constructor (options) {
// 保存options
this.$options = options
this.$data = options.data
// 将data进行响应式处理
observe(this.$data)
// 代理
proxy(this)
}
}
// 遍历响应式处理
function observe (obj) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
new Observer(obj)
}
const oldArrayProperty = Array.prototype
const arrayProto = Object.create(oldArrayProperty)
const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse']
methodsToPatch.forEach(methodName => {
Object.defineProperty(arrayProto, methodName, {
value () {
console.log('you change the array')
oldArrayProperty[methodName].apply(this, arguments)
}
})
})
class Observer {
constructor (obj) {
// 判断传入obj类型,做相应处理
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
} else {
this.walk(obj)
}
}
walk (obj) {
Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
}
}
// 能够将传入对象中的所有key代理到指定对象上
function proxy (vm) {
Object.keys(vm.$data).forEach((key) => {
Object.defineProperty(vm, key, {
get () {
return vm.$data[key]
},
set (v) {
vm.$data[key] = v
}
})
})
}
测试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">{{ counter }}</div>
</body>
<script src="./index.js"></script>
<script>
const app = new MVue({
el: '#app',
data: {
counter: 1
}
})
setInterval(() => {
app.counter++
}, 1000)
</script>
</html>
打开控制台,就可以看到counter的变化都被监听到了~