源代码1
// 响应式原理 defineProperty
//数据
const data = {
obj: {
a: 4,
b: 6
},
arr: [1, 5, 9]
}
// 观察数据
function observe(data) {
Object.keys(data).forEach(function(key) {
let value = data[key]
if (value && typeof value === 'object') observe(value) // 递归
Object.defineProperty(data, key, {
get() {
console.log(`get ${key}`)
return value
},
set(newVal) {
console.log(`set ${key} = ${newVal}`)
if (newVal && typeof newVal === 'object') observe(newVal)
value = newVal
}
})
})
}
observe(data)
let obj = data.obj
// get obj
let arr = data.arr
// get arr
obj.a = 8
// set a = 8
obj.a
// get a
delete obj.b
// 无反应
obj.c = 9
// 无反应
obj.c
// 无反应
data.obj = {...obj, c: 7}
// set obj = [object Object]
obj = data.obj
// get obj
obj.c = 9
// set c = 9
obj.c
// get c
arr.push(9) // 包括pop,shift,unshift,splice,sort,reverse
// 无反应
data.arr = [...arr,9]
// set arr = 1,5,9,9,9
讲解
- vue只所以能实现双向绑定,是利用es5里面的Object.defineProperty(这就是为什么vue只支持es9及以上)
- 从以上代码可以看出,对象属性的删除(delete obj.b)和添加新属性(obj.c = 9),不会触发对应的set方法(vue对于初始化没有定义的属性,设置值不能触发视图层渲染)
- 在项目开发中肯定会遇到有些属性,初始化时没有定义,通过改变其父元素的值去实现(data.obj = {...obj, c: 7}),父元素的值改变后,会触发其set方法,会对其子元素重新进行双向绑定
- 对于数组的处理,push,pop,shift,unshift,splice,sort,reverse都不会触发set,但我们看到的vue,这些方法是会触发视图层的变化,这是因为vue针对这些方法做了特殊的处理,原理如
const arr = [5, 9, 8]
const push = Array.prototype.push
Array.prototype.push = function () {
const result = push.apply(this, arguments)
console.log('做自己喜欢的事情')
return result
}
arr.push(7)
console.log(arr)
vue代码片段
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
/* */
源代码2
// 响应式原理 defineProperty
//数据
const data = {
obj: {
a: 4,
b: 6
},
arr: [1, 5, 9]
}
function Dep() {}
Dep.target = null // 当前函数
function watcher(fn) { // 函数
Dep.target = fn
fn()
}
// 初始化
function init() {
const a = () => {
console.log(data.obj.a)
}
const mix = () => {
const c = data.obj.a + data.obj.b
console.log(c)
}
watcher(a)
watcher(mix)
}
// 观察数据
function observe(data) {
Object.keys(data).forEach(function(key) {
let value = data[key]
const dep = [] // 存放函数的容器
if (value && typeof value === 'object') observe(value) // 递归
Object.defineProperty(data, key, {
get() {
dep.push(Dep.target)
return value
},
set(newVal) {
if (newVal && typeof newVal === 'object') observe(newVal)
value = newVal
dep.forEach(fn => fn())
}
})
})
}
observe(data)
init()
setTimeout(() => {
data.obj.a = 10
}, 2000)
讲解
以上代码可以看出,当obj.a值变化的时候,会触发a函数和mix函数,具体执行步骤如下
- 先将属性a绑定了对应的set和get方法
- 初始化init()时,会调用watcher(a),此时 Dep.target等于a函数
- 执行a()函数时,会执行里面的console.log(data.obj.a)
- data.obj.a会调用a的get方法,此时dep.push(Dep.target)就相当于dep.push(a函数),mix函数同理
- 当属性a的值改变时(data.obj.a = 10),会触发其set方法,dep.forEach(fn => fn())将a函数和mix函数循环执行一遍,从而实现了数据驱动视图
注意点
- 在驱动视图之前都要先赋值,比如源代码2的(value = newVal)和(dep.forEach(fn => fn()))不能互换位置
- 以上代码不是vue的源代码,但原理是一致的,帮助开发者更好的理解自己写的代码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。