实现数据代理和数据递归劫持
在vue2.x的源码中使用Object.defineProperty()这个api实现数据代理, 也可以实现数据劫持, 下面是数据代理和数据劫持的简单实现,下文使用MVue实现一个类vue的类
class MVue{
constructor(options){
this.$options = options
// TODO: data有可能是一个function
this._data = options.data
this.initData()
}
initData() {
let data = this._data
let keys = Object.keys(data)
// 实现数据代理
for(let i = 0; i< keys.length; i++){
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
console.log(`${keys[i]}取值`);
return data[keys[i]]
},
set: function proxySetter(newValue) {
if(value === newValue) {
return
}
data[keys[i]] = newValue
console.log(`${keys[i]}设置值`);
}
})
}
// 实现数据劫持
for(let i = 0; i< keys.length; i++){
let value = data[keys[i]]
Object.defineProperty(data, keys[i], {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`${keys[i]}reactiveGetter取值`);
return value
},
set: function reactiveSetter(newValue) {
if(value === newValue) {
return
}
value = newValue
console.log(`${keys[i]}reactiveSetter设置值`);
}
})
}
}
}
上面代码存在的问题: 无法深度劫持到对象中属性仍存在对象的情况
const vm = new MVue({
data:{
person:{
name: 'phil' // 无法监听到person中name属性的变化
}
}
})
递归实现深度劫持
function defineReactive(data, key, value) {
observe(data[key])
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`${key}reactiveGetter取值`);
return value
},
set: function reactiveSetter(newValue) {
if(value === newValue) {
return
}
value = newValue
console.log(`${key}reactiveSetter设置值`);
}
})
}
function observe(data) {
let type = Object.prototype.toString.call(data)
if( type !== '[object Object]' && type !== '[object Array]' ) {
return
}
new Observer(data)
}
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 实现数据劫持
let keys = Object.keys(data)
for(let i = 0; i< keys.length; i++){
let value = data[keys[i]]
defineReactive(data, keys[i], value)
}
}
}
实现一个watcher类,实现vue中watch侦听器的功能
class MVue{
...
$watch(exp, cb){
new Watcher(this, exp, cb)
}
// vue中使用watch监听属性变化
initWatch(){
let watch = this.$options.watch
if(watch) {
Reflect.ownKeys(watch).forEach(watcher => {
new Watcher(this, watcher, watch[watcher])
})
}
}
}
class Dep {
constructor() {
this.subs = []
}
//收集依赖
depend() {
if(Dep.target) {
this.subs.push(Dep.target)
}
}
//派发更新
notify() {
this.subs.forEach(watcher => {
watcher.run()
})
}
}
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.get()
}
get() {
Dep.target = this
this.vm[this.exp]
Dep.target = null
}
run() {
this.cb.call(this.vm)
}
}
这样可以实现一个简单的watch实现对数据的监听,但是此时的watch与vue中的watch还是有差距:1.vue中的watch是异步的,2.vue中会将多次赋值操作合并成一个,并不会多次调用就多次执行,是异步更新的。实现异步我们自然想到的就是使用promise实现。 将上面的run方法进行如下改造: 即可实现
run() {
if(watcherQueue.includes(this.id)) {
return
}
watcherQueue.push(this.id)
Promise.resolve().then(() =>{
this.cb.call(this.vm)
watcherQueue.pop()
})
}
实现一个$set
接下来我们再讨论vue中set的出现。因为vue2.x中的data中属性是一次性深度监听的,对于新增的属性,vue是无法监听到的。实现思路如下:
-
- 在创建observer实例的时候,创建一个container, 挂载到Observer的实例上,然后把Observer实例挂载到对象的
__ob__属性上。
- 在创建observer实例的时候,创建一个container, 挂载到Observer的实例上,然后把Observer实例挂载到对象的
- 2.触发getter的时候,收集一份watcher到container中
- 3.用户调用$set的时候,手动触发
__ob__.dep.notify() - 4.在notify之前调用defineReactive把新的属性定义成响应式的。
// 收集Observer实例上的依赖
function defineReactive(data, key, value) {
let childOb = observe(data[key])
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// console.log(`${key}reactiveGetter取值`);
dep.depend()
if( childOb ) {
childOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if(value === newValue) {
return
}
value = newValue
dep.notify()
// console.log(`${key}reactiveSetter设置值`);
}
})
}
// 在监听的对象上定义一个__ob__属性
class Observer {
constructor(data) {
this.dep = new Dep()
this.walk(data)
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true
})
}
}
熟悉上述原理之后,vue2.x中的一些困惑也就迎刃而解了。
-
为什么vue不能直接对新增的属性实现响应式? vue在页面初始化的时候就实现了data中数据的响应式,后面直接对象点一个属性是无法不能被Object.defineproperty这个监听到的,vue中新增和删除属性都无法被vue监听到,所以delete就应运而生了。
-
vue项目中常见性能优化? data中数据的层级尽量不要嵌套太深,data中数据尽量少,这都能减轻页面在初始化的时候的压力。 3.vue2.x中实现数据响应式的缺点? -> vue3.0使用proxy可以解决这些问题
1) 深度监听,一次递归到底,一次性计算量巨大,
2) 无法监听新增和删除的属性,所以vue2.0 中提供了Vue.set 和 Vue.delete,
3) 无法实现对数组的监听。
下期我们一起来分析, vue中是如何实现对数组的处理的?computed属性是如何实现的?vue是如何实现模板编 译的?如何实现一个vdom?