非火头瞎解牛系列~ =。=
在一样平常项目开辟的时刻,我们将js对象传给vue实例中的data选项,来作为其更新视图的基本,事实上是vue将会遍历它的属性,用Object.defineProperty 设置它们的 get/set,从而让 data 的属性能够相应数据变化:
Object.defineProperty(obj, name, {
// 获取值的时刻先置入vm的_data属性对象中
get() {
// 赋值的时刻显现的特征
},
set() {
// 值变化的时刻能够做点什么
}
})
接下来能够应用其完成一个最简朴的watcher.既然要绑定数据实行回调函数,data属性和callback属性是少不了的,我们定义一个vm对象(vue中vm对象作为根实例,是全局的):
/**
* @param {Object} _data 用于寄存data值
* @param {Object} $data data原始数据对象,当前值
* @param {Object} callback 回调函数
*/
var vm = { _data: {}, $data: {}, callback: {} }
在设置值的时刻,假如检测到当前值与存储在_data中的对应值发生变化,则将值更新,并实行回调函数,应用Object.definedProperty要领中的get() & set() 我们很快就能够完成这个功用~
vm.$watch = (obj, func) => {
// 回调函数
vm.callback[ obj ] = func
// 设置data
Object.defineProperty(vm.$data, obj, {
// 获取值的时刻先置入vm的_data属性对象中
get() {
return vm._data[ obj ]
},
set(val) {
// 比较原值,不相称则赋值,实行回调
if (val !== vm._data[ obj ]) {
vm._data[ obj ] = val
const cb = vm.callback[ obj ]
cb.call(vm)
}
}
})
}
vm.$watch('va', () => {console.log('已胜利被监听啦')})
vm.$data.va = 1
虽然开端完成了这个小功用,那末题目来了,obj对象假如只是一个简朴的值为值范例的变量,那以上代码完整能够满足;然则假如obj是一个具有一层以至多层树结构对象变量,我们就只能监听到最外层也就是obj本身的变化,内部属性变化没法被监听(没有设置给对应属性设置set和get),由于对象本身内部属性层数未知,理论上能够无穷层(平常不会这么做),所以此处照样用递归处理吧~
我们先将Object.defineProperty函数剥离,一是解耦,二是轻易我们递归~
var defineReactive = (obj, key) => {
Object.defineProperty(obj, key, {
get() {
return vm._data[key]
},
set(newVal) {
if (vm._data[key] === newVal) {
return
}
vm._data[key] = newVal
const cb = vm.callback[ obj ]
cb.call(vm)
}
})
}
咦,说好的递归呢,不着急,上面只是抽离了加get和set功用的函数,
如今我们到场递归~
var Observer = (obj) => {
// 遍历,让对象中的每一个属性能够加上get set
Object.keys(obj).forEach((key) =>{
defineReactive(obj, key)
})
}
这里仅仅只是遍历,要到达递归,则须要在defineReactive的时刻再加上推断,推断这个属性是不是为object范例,假如是,则实行Observer本身~我们改写下defineReactive函数
// 推断是不是为object范例,是就继承实行本身
var observe = (value) => {
// 推断是不是为object范例,是就继承实行Observer
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
// 将observe要领置入defineReactive中Object.defineProperty的set中,构成递归
var defineReactive = (obj, key) => {
// 推断val是不是为对象,假如对象有很多层属性,则这边的代码会不停挪用本身(由于observe又实行了Observer,而Observer实行defineReactive),一向到末了一层,从末了一层最先实行以下代码,层层返回(能够理解为洋葱模子),直到最前面一层,给一切属性加上get/set
var childObj = observe(vm._data[key])
Object.defineProperty(obj, key, {
get() {
return vm._data[key]
},
set(newVal) {
// 假如设置的值完整相称则什么也不做
if (vm._data[key] === newVal) {
return
}
// 不相称则赋值
vm._data[key] = newVal
// 实行回调
const cb = vm.callback[ key ]
cb.call(vm)
// 假如set进来的值为庞杂范例,再递归它,加上set/get
childObj = observe(val)
}
})
}
如今我们来整顿下,把我们刚最先完成的功用雏形举行进化
var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key) => {
// 一最先的时刻是不设值的,所以,要在表面做一套observe
// 推断val是不是为对象,假如对象有很多层属性,则这边的代码会不停挪用本身(由于observe又实行了Observer,而Observer实行defineReactive),一向到末了一层,从末了一层最先实行以下代码,层层返回(能够理解为洋葱模子),直到最前面一层,给一切属性加上get/set
var childObj = observe(vm._data[key])
Object.defineProperty(obj, key, {
get() {
return vm._data[key]
},
set(newVal) {
if (vm._data[key] === newVal) {
return
}
// 假如值有变化的话,做一些操纵
vm._data[key] = newVal
// 实行回调
const cb = vm.callback[ key ]
cb.call(vm)
// 假如set进来的值为庞杂范例,再递归它,加上set/get
childObj = observe(newVal)
}
})
}
var Observer = (obj) => {
Object.keys(obj).forEach((key) =>{
defineReactive(obj, key)
})
}
var observe = (value) => {
// 推断是不是为object范例,是就继承实行Observer
if (!value || typeof value !== 'object') {
return
}
Observer(value)
}
vm.$watch = (name, func) => {
// 回调函数
vm.callback[name] = func
// 设置data
defineReactive(vm.$data, name)
}
// 绑定a,a若变化则实行回调要领
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data[va] = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('已胜利被监听啦')})
vm.$data.va = 1
在谷歌浏览器的console中粘贴以上代码,然后回车发明,效果不出所料,va本身被监听了,能够,我们尝尝va的内部属性有无被监听,改下vm.$data.va = 1为vm.$data.va.a = 1,效果发明报错了
什么鬼?
我们又仔细检查了代码,WTF,本来我们在递归的时刻,Object.defineProperty中的回调函数cb的key参数一向在发生变化,我们愿望的是内里的属性变化的时刻实行的是我们事前定义好的回调函数~那末我们来改下要领,将一最先定义好的回调作为参数传进去,确保每一层递归set的回调都是我们事前设置好的~
var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key, cb) => {
// 一最先的时刻是不设值的,所以,要在表面做一套observe
var childObj = observe(vm._data[key], cb)
Object.defineProperty(obj, key, {
get() {
return vm._data[key]
},
set(newVal) {
if (vm._data[key] === newVal) {
return
}
// 假如值有变化的话,做一些操纵
vm._data[key] = newVal
// 实行回调
cb()
// 假如set进来的值为庞杂范例,再递归它,加上set/get
childObj = observe(newVal)
}
})
}
var Observer = (obj, cb) => {
Object.keys(obj).forEach((key) =>{
defineReactive(obj, key, cb)
})
}
var observe = (value, cb) => {
// 推断是不是为object范例,是就继承实行Observer
if (!value || typeof value !== 'object') {
return
}
Observer(value, cb)
}
vm.$watch = (name, func) => {
// 回调函数
vm.callback[name] = func
// 设置data
defineReactive(vm.$data, name, func)
}
// 绑定a,a若变化则实行回调要领
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data.va = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('又胜利被监听啦')})
vm.$data.va.a = 1
再实行一次以上代码,发明内部的a属性也被监听到了,而且属性值变化的时刻实行了我们事前定义好的回调函数~嘻嘻嘻~
虽然完成了$watch的基本功用,然则和vue的源码照样有肯定的间隔,特别是一些扁平化和模块化的头脑须要涉及到一些设想形式,实在我们在看源码的时刻,常常是逆着作者的头脑走的,功用从简朴到庞杂每每涉及到代码的模块化息争耦,使得代码异常地疏散,读起来艰涩难明,本身着手,从小功用代码块完成,然后连系源码,对照思绪,逐步雄厚,也不失为一种进修源码的体式格局~
ps: 假如列位读者看到本文的error或许由更好的优化发起,随时联络~