手写实现简易版vue的响应式原理(data, watch, computed)
给实习生们布置了这个作业,此处作个记录。
代码方面只实现基本的思想,并不是和vue源码一模一样的结构,为的是更好的理解响应式原理
实现范围:
- data:data内所有属性都绑好的get、set方法,有多层的话,递归完成多层
- watch:data任一个属性改变都会触发对应watch的监听函数
- computed:computed内的属性的data属性依赖改变后,computed的属性值会重新获取,否则读取缓存
目录结构:
- 实现的源代码
- 测试用例
实现的源代码
目录结构
├── index.html
└── vue2
└── index.js
./index.html(使用效果)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./vue2/index.js"></script>
</head>
<body>
打开控制台
<script>
window.vm = new Vue2({
el: '#app',
data: {
x: 1,
y: 2,
z: 3,
a: {
b: 4,
c: 5
}
},
watch: {
x (newVal, oldVal) {
console.log('我是watch的回调函数', newVal, oldVal)
}
},
computed: {
C_x () {
console.log('我是computed的回调函数')
return this.x
}
}
})
</script>
</body>
</html>
./vue2/index.js (实现细节,请看注释)
function Vue2(options) {
this.$options = options
initWatch.call(this, options.watch)
initComputed.call(this, options.computed)
initData.call(this, options.data)
}
function initWatch (data) {
this.watcherList = data
this.val = null
}
function initComputed (data) {
this._computedWatchers = {} // 保存computed对应key的缓存
this._computedAndDataMap = {} // 保存computed对应key 和其回调函数内的 this.xxdata 的映射关系 (如果this.xxdata变更了, 清空对应的this._computedWatchers的缓存)
this._currentComputedKey = null // 保存computed对应key. 为了上面this._computedAndDataMap能拿到依赖关系
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: function() {
this._currentComputedKey = key
if (!this._computedWatchers[key]) this._computedWatchers[key] = data[key].call(this)
this._currentComputedKey = null
return this._computedWatchers[key]
},
set: function (n) {
console.log('computed值不能修改')
}
})
})
}
function initData (data) {
for (let key in data){
let temp; // 利用了闭包, 保存了私有的变量 方便get和set的操作, 会常驻内存
// 如果 data[key] 是一个对象, 递归重写 setter getter
if(typeof data[key] === 'object'){
temp = new Object()
initData.call(temp, data[key]) // 递归 用Object.defineProperty监听data
} else {
temp = data[key]
}
// 将 data 中的数据直接绑定在 vue 的实例上, 好处是可以 this.x 调用数据了.
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: function() {
console.log(key, '的get函数被执行了')
if (this._currentComputedKey) { // 如果是computed内的回调函数像拿值, 做一个记录, 记录下当前key 对应哪个computed内的属性
this._computedAndDataMap[key] = this._currentComputedKey
}
return temp
},
set: function (n) {
console.log(key, '的----set-----函数被执行了')
const watcher = this.watcherList[key] // 如果被watch监听了, 执行watch内的回调函数
if (watcher) {
watcher(n, this.val) // newVal 和 oldVal
}
temp = n
this._computedWatchers[this._computedAndDataMap[key]] = null // 因为这个依赖改变了, computed对应的值也要刷新
this.val = n // 保存当前val, 后面会变成oldVal
}
})
}
}
测试用例
以下是测试用例 和 细节解释: 可以亲手试试一条一条在控制台输出
/* 以下是测试用例 和 细节解释: 可以亲手试试一条一条在控制台输出*/
console.log(vm.x) // 会触发this.x的get监听函数
console.log(vm.a.b) // 会触发this.a的get监听函数 和 this.a.b的get监听函数
/* computed的属性C_x是一个函数的返回值, 需缓存这个返回值, 并且收集依赖this.x,
当this.x改变时, 需要重新跑函数 获得返回值
跑函数的过程中, 需获取this.x, 会触发this.x的get监听函数 */
console.log(vm.C_x)
console.log(vm.C_x) // computed的属性C_x已经有缓存了, 直接返回缓存, 不会去取this.x的值了, 不会触发this.x的get监听函数
vm.x = 1111 // 会触发watch, 清掉C_x的缓存
vm.x = 22222222 // 会触发watch, 清掉C_x的缓存
console.log(vm.C_x) // computed的属性C_x需重新获取函数的返回值, 会重新触发this.x的 get监听函数
console.log(vm.C_x) // computed的属性C_x已经有缓存了, 直接返回缓存, 不会去取this.x的值了, 不会触发this.x的get监听函数
点赞一将,手留余香~