Object.defineProperty()
Vue 内部使用了 Object.defineProperty() 来实现数据响应式,通过这个函数可以监听到 set 和 get 的事件。
先定义一个对象 以及属性
var data = {name: 'john'};
observe(data); // 监听 这个对象
console.log(data.name);
data.name = 'lily';
console.log(data.name);
定义observe
function observe(obj){
// 先进行类型判断
if(!obj || typeof obj !== 'object') return
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
定义defineReactive
function defineReactive(obj, key, val){
observe(obj); // 进行递归(如果值是对象的话)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val
},
set: function reactiveSetter(newval) {
val = newval
}
})
}
以上就只是 进行监听数据的set 与 get事件。但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,才能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集。
<div>{{name}}</div>
在解析如上模板代码时,遇到 {{name}} 就会进行依赖收集。
接下来我们先来实现一个 Dep 类,用于解耦属性的依赖收集和派发更新操作。
class Dep{
constructoe(){
this.subs = [];
},
// 添加依赖
addSub(sub) {
this.subs.push(sub)
},
// 更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
以上的代码实现很简单,当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。
接下来我们先来简单的了解下 Vue 组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
class Watcher{
constructor(obj, key, cb){
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.traget = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}
以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher 然后执行 update 函数。
接下来,需要对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码。
function defineReactive(obj, key, val) {
observe(val);
let dp = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter(){
// 将 Watcher 添加到订阅
if(Dep.target){
dp.addsub(Dep.target)
}
return val
}
set: function reactiveSetter(newVal){
val = newVal
dp.notify()
}
})
}
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的 getter 来实现依赖收集。
进行验证
var data = { name: 'yck' }
observe(data)
function update(value) {
document.querySelector('div').innerText = value
}
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'
Object.defineProperty 的缺陷
基于以上,如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
如何解决呢:
vue 官方文档中有贴:
手动触发视图,这$ forceUpdate()。