Object.defineProperty()方法
Object.defineProperty()的作用是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
1
| Object.defineProperty(obj, prop, desc)
|
obj:需要操作的当前对象
prop:当前需要操作的属性名
desc:属性描述符
属性描述符
通过Object.defineProperty()为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符
- 数据描述符(value, writabl)
value就是属性的值,
writable用来设置属性的值是否可以被改变(默认为false),
enumerable用来设置属性是否可以被枚举(默认为false)
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let obj = {}
Object.defineProperty(obj, 'a', { value: 123 })
Object.defineProperty(obj, 'b', { value: 456, writable: true })
console.log(obj); obj.a ++ obj.b ++ console.log(obj);
|
举第二个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let obj = {}
Object.defineProperty(obj, 'a', { value: 123, enumerable: false })
Object.defineProperty(obj, 'b', { value: 456, writable: true, enumerable: true })
Object.defineProperty(obj, 'c', { value: 789, enumerable: true })
console.log(obj);
for(let k in obj) { console.log(k); }
|
- 存取描述符(get,set)
get:该方法的返回值被用作属性值,默认值为undefined
set:该方法接收唯一参数,并将该唯一参数赋值为属性的新值,默认值为undefined
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let obj = {} let num = 123 Object.defineProperty(obj, 'a', { get() { console.log('你试图访问a属性') return num }, set(val) { console.log('你试图改变a属性') num = 456 } }) console.log(obj.a); obj.a = 456 console.log(obj.a);
|
数据描述符和存取描述均具有以下描述符
enumerable用来设置属性是否可以被枚举(默认为false)
configrable 描述属性是否配置,以及可否删除
defineReactive函数
在Object.defineProperty()的存取描述符中,你需要定义一个变量来周转属性值,如果没有定义这个变量,你通过set设置的新值在get中无法读取
1 2 3 4 5 6 7 8 9 10 11 12 13
| let obj = {} Object.defineProperty(obj, 'a', { get() { console.log('你试图访问a属性') return 123 }, set(val) { console.log('你试图改变a属性',val) } }) console.log(obj.a); obj.a = 456 console.log(obj.a);
|
这样写会导致代码不是那么优雅,所以我们用一个函数将Object.defineProperty()包裹起来,形成一个闭包,下面代码中第三个参数val就是这个闭包的核心,它很好的解决了前面的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| let obj = {}
function defineReactive(data, key, val) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { console.log('你试图访问a属性') return val }, set(newvalue) { console.log('你试图改变a属性',newvalue) val = newvalue } }) } defineReactive(obj, 'a', 10)
console.log(obj.a);
obj.a = 100 console.log(obj.a);
|
现在的defineReactive无法为每一层的数据都添加get和set方法
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| let obj = { a: { m: { n: 5 } } } function defineReactive(data, key, val) { if(arguments.length == 2) { val = data[key] } Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { console.log('你试图访问' + key +'属性') return val }, set(newvalue) { console.log('你试图改变' + key + '属性',newvalue) val = newvalue } }) }
defineReactive(obj, 'a') console.log(obj.a.m.n);
|
控制台打印的结果:打印的是试图访问a属性,而不是n属性,所以对n属性的改变无法监视到
递归侦测对象全部属性
基于上面defineReactive()的问题,我们在这里创建一个Observer(中文解释:观察者,观测者)类,它的作用是将一个正常的object对象转化成每一层属性都是响应式(可以被侦测到)的object对象
我们先定义一个observe函数(函数名没有 r),给他传入一个对象,observe首先会在它身上寻找有没有__ob__属性,如果没有,调用Observer类实例化一个ob对象,将这个ob对象添加到__ob__身上(这个__ob__属性在后面会用到)
observe函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import Observer from './Observer'
export const observe = function(value) { if(typeof value != 'object') return var ob if(typeof value.__ob__ != 'undefined') { ob = value.__ob__ } else { ob = new Observer(value) }
return ob }
|
Observer类
Observer类的作用是将一个对象转化成每一层都是响应式的对象
def是一个工具函数,用来给传入的对象加上一个__ob__属性,定义它主要是为了简化Observer类中的代码,使逻辑清晰
1 2 3 4 5 6 7 8 9 10 11 12
|
export const def = function(obj, key, value, enumerable) { Object.defineProperty(obj, key, { value, enumerable, writable: true, configurable: true }) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import {def} from './utils' import defineReactive from './defineReactive';
export default class Observer { constructor(value) { def(value, '__ob__', this, false) this.walk(value) } walk(value) { for(let k in value) { defineReactive(value, k) } } }
|
defineReactive()函数
在defineReactive()函数中会调用observe()函数,给它的子元素添加__ob__属性,并在observe中会调用Observer类,对它的子元素进行侦测,实现响应式,也是在此处形成三个函数的递归调用
注意:当个某个元素赋值时,对新的值也要调用observe,因为新值可能是一个对象,如果是一个对象,对它里面的属性也要侦测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { observe } from "./observe"
export default function defineReactive(data, key, val) { if(arguments.length == 2) { val = data[key] } let childOb = observe(val)
Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { console.log('你试图访问' + key +'属性') return val }, set(newvalue) { console.log('你试图改变' + key + '属性',newvalue) val = newvalue childOb = observe(newvalue) } }) }
|
流程图:
我们用一个例子来检测一下能否侦测对象的全部属性:
至此完成了对对象全部属性的侦测,但是,现在还不能对数组进行响应式处理
数组的响应式处理
在Vue的底层,为了实现对数组的响应式处理,将数组的七个方法 (push, pop, shift, unshift, splice, sort, reverse)进行了改写
这些方法都被定义在Array.prototype
中,Vue根据Array.prototype
为原型创建一个arrayMethods对象,在arrayMethods中对七个方法进行改写,并且利用ES6中的Object.setPrototypeOOf()
让Array实例的__proto__强制的指向这个arrayMethods对象
首先我们创建一个array.js对这7个方法进行改写。
在下面的代码中,我们先以Array的原型为原型创建一个对象。
然后遍历要改写的方法,对他们进行逐一改写。使用forEach进行遍历,先将这7个方法备份(以便在后面恢复它们的原始功能),然后调用def将arrayMethods对象上的这7个方法进行重写。
在这里我们先恢复7个方法原理的功能,然后让他打印一些东西来测试是否可用,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import {def} from './utils'
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsChange = [ 'push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse' ]
methodsChange.forEach(method => { const original = arrayPrototype[method] def(arrayMethods, method, function(){ const result = original.apply(this, arguments) console.log('哈哈哈');
return result }, false) })
|
接下来我们在Observer中也要做一些改变,使他能在要侦测的数据是数组时也可以实现响应式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import {def} from './utils' import defineReactive from './defineReactive'; import { arrayMethods } from './array'
export default class Observer { constructor(value) { def(value, '__ob__', this, false) if(Array.isArray(value)) { Object.setPrototypeOf(value, arrayMethods) this.observeArr(value) } else { this.walk(value) } } walk(value) { for(let k in value) { defineReactive(value, k) } }
observeArr(arr) { for(let i = 0, l = arr.length; i < l; i++) { observe(arr[i]) } } }
|
让我们来做一些阶段性的测试,看看上面的改动是否有效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import {observe} from './observe' let obj = { a: { m: { n: 100 } }, b: [1,2,3] }
observe(obj)
obj.b.push(4)
console.log(obj.b);
|
我们接下来还需要对push
,unshift
,splice
做一些特殊的处理,因为这个三个方法都可以插入新值,我们也需要监测新值(新值可能是对象或者包含着对象)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import {def} from './utils' const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsChange = [ 'push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse' ]
methodsChange.forEach(method => { const original = arrayPrototype[method] def(arrayMethods, method, function(){
const result = original.apply(this, arguments) const args = [...arguments] const ob = this.__ob__ let inserted = []
switch(method) { case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2); break; } if(inserted) { ob.observeArr(inserted) }
return result }, false) })
|
这样我们就实现了对数组的响应式,通过一些例子来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import {observe} from './observe' let obj = { a: { m: { n: 100 } }, b: [1,2,3] }
observe(obj)
obj.b.splice(2, 0, 1234)
console.log(obj.b);
obj.b.push({ name: 'cqy' })
console.log(obj.b);
|
依赖收集
需要用到数据的地方,称为依赖。
在Vue 2.x 中,用到数据的组件叫依赖
依赖要被收集起来,当数据发生变化时通过循环通知所有依赖
在用到数据的地方就会触发getter,当改变数据时会触发setter,所以就有一个结论:
在getter中收集依赖,在setter中触发依赖
把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例中都有一个Dep的实例。
在这里我们先创建一个Dep类,
1 2 3 4 5
| export default class Dep { constructor() {} notify() {} }
|
并在Observer实例中实例化Dep类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export default class Observer { constructor(value) { this.dep = new Dep()
} walk(value) { for(let k in value) { defineReactive(value, k) } }
observeArr(arr) { for(let i = 0, l = arr.length; i < l; i++) { observe(arr[i]) } } }
|
除此之外,我们需要在可能发生数据更新的地方都调用Dep的notify()方法来通知组件
在defineReactive中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export default function defineReactive(data, key, val) { const dep = new Dep()
if(arguments.length == 2) { val = data[key] } let childOb = observe(val)
Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { }, set(newvalue) { console.log('你试图改变' + key + '属性',newvalue) val = newvalue childOb = observe(newvalue)
dep.notify() } }) }
|
在数组的响应式中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| methodsChange.forEach(method => { const original = arrayPrototype[method] def(arrayMethods, method, function(){
const result = original.apply(this, arguments) const args = [...arguments]
const ob = this.__ob__ let inserted = []
switch(method) { case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2); break; } if(inserted) { ob.observeArr(inserted) } ob.dep.notify()
return result }, false) })
|
这样设置之后,当我们改变数据就会被Dep监测,然后发布订阅模式通知组件
让我们接着来看Dep类:Dep类中的depend方法用于添加依赖,当全局位置Dep.target(之后会说明)存在时,会调用addSubs方法将这个全局位置存入subs数组。当数据发生改变时,在前面我们已经知道会调用notify方法,而它又会调用Watcher类中的update方法(subs数组存放的是Watcher实例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export default class Dep { constructor() { this.subs = [] } addSubs(sub) { this.subs.push(sub) } depend() { if(Dep.target) { this.addSubs(Dep.target) } } notify() { const subs = this.subs.slice()
for(let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
|
Watcher类是一个观察者对象。依赖收集以后的Watcher实例被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例调用回调函数进行视图更新。
1 2 3 4 5 6 7
| export default class Watcher { constructor(target, expression, callback) { this.obj = target this.callback = callback } }
|
我们来模拟一下Watcher观察某个数据的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let obj = { a: { m: { n: 100 } }, b: [1,2,3] }
observe(obj)
new Watcher(obj, 'a.m.n', (val) => { console.log("@@@@", val); })
|
将字符串解析为点语法的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export default class Watcher { constructor(target, expression, callback) { this.obj = target this.callback = callback this.getter = parsePath(expression) } }
function parsePath(str) { let segments = str.split('.')
return (obj) => { for(let i=0; i<segments.length; i++) { obj = obj[segments[i]] }
return obj } }
|
当Watcher实例化之后就会去读取相应的数据,那么这个时候会触发数据的getter,我们需要在这个时候将全局位置存入Dep的subs数组中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export default function defineReactive(data, key, val) { const dep = new Dep() if(arguments.length == 2) { val = data[key] } let childOb = observe(val)
Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { console.log('你试图访问' + key +'属性') if(Dep.target) { dep.depend() } return val }, set(newvalue) { dep.notify() } }) }
|
我们在构造函数中定义变量value用来存储要观测的值,实例化时value会调用get()方法,在这个方法中,将Watcher实例本身设置为全局位置,并调用getter来获取要存储的值。
当这次的全局位置存入Dep中之后,记得将全局位置设置为null,为之后观测其他值做准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| export default class Watcher { constructor(target, expression, callback) { this.obj = target this.getter = parsePath(expression) this.callback = callback this.value = this.get() } get() { Dep.target = this
var value value = this.getter(this.obj)
Dep.target = null
return value } }
function parsePath(str) { let segments = str.split('.')
return (obj) => { for(let i=0; i<segments.length; i++) { obj = obj[segments[i]] }
return obj } }
|
如前所述,当监测的数据发生了更新,就会触发数据的setter,在setter中会调用dep的notify()方法,发布订阅模式,通知相应组件更新视图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default class Dep { constructor() { this.subs = [] } notify() { const subs = this.subs.slice()
for(let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
|
Dep的notify方法又会调用Watcher实例的update方法,update方法又会调用getAndInvoke方法,这个方法会再调用一次get方法来得到新的值,然后通过Watcher实例化时传入的回调函数进行视图更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| export default class Watcher { constructor(target, expression, callback) { this.obj = target this.getter = parsePath(expression) this.callback = callback this.value = this.get() } update() { this.getAndInvoke(this.callback) } get() { Dep.target = this console.log("全局位置",Dep.target);
var value value = this.getter(this.obj)
Dep.target = null
return value }
getAndInvoke(cb) { const value = this.get()
if(value !== this.value || typeof value == 'object') { const oldValue = this.value this.value = value cb.call(this.obj, value, oldValue) } } }
|
进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let obj = { a: { m: { n: 100 } }, b: [1,2,3] }
observe(obj)
new Watcher(obj, 'a.m.n', (val) => { console.log("@@@@", val); })
obj.a.m.n = 200
console.log(obj.a.m.n);
|
最后
响应式的实现完整代码
在defineReactive()函数中会调用observe()函数,给它的子元素添加__ob__属性,并在observe中会调用Observer类,对它的子元素进行侦测,实现响应式
defineReactive()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import Dep from "./Dep" import { observe } from "./observe"
export default function defineReactive(data, key, val) { const dep = new Dep() if(arguments.length == 2) { val = data[key] } let childOb = observe(val)
Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { console.log('你试图访问' + key +'属性') if(Dep.target) { dep.depend() } return val }, set(newvalue) { console.log('你试图改变' + key + '属性',newvalue) val = newvalue childOb = observe(newvalue) dep.notify() } }) }
|
observe()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import Observer from './Observer'
export const observe = function(value) { if(typeof value != 'object') return var ob if(typeof value.__ob__ != 'undefined') { ob = value.__ob__ } else { ob = new Observer(value) }
return ob }
|
Observer类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import {def} from './utils' import defineReactive from './defineReactive'; import { arrayMethods } from './array' import { observe } from './observe'; import Dep from './Dep';
export default class Observer { constructor(value) { this.dep = new Dep() def(value, '__ob__', this, false) if(Array.isArray(value)) { Object.setPrototypeOf(value, arrayMethods) this.observeArr(value) } else { this.walk(value) } } walk(value) { for(let k in value) { defineReactive(value, k) } }
observeArr(arr) { for(let i = 0, l = arr.length; i < l; i++) { observe(arr[i]) } } }
|
def是一个工具函数,用来给传入的对象加上一个__ob__属性,定义它主要是为了简化Observer类中的代码,使逻辑清晰
1 2 3 4 5 6 7 8 9 10 11 12
|
export const def = function(obj, key, value, enumerable) { Object.defineProperty(obj, key, { value, enumerable, writable: true, configurable: true }) }
|
arrayMethods对象用于重写数组的7个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import {def} from './utils' const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsChange = [ 'push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse' ]
methodsChange.forEach(method => { const original = arrayPrototype[method] def(arrayMethods, method, function(){
const result = original.apply(this, arguments) const args = [...arguments]
const ob = this.__ob__ let inserted = []
switch(method) { case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2); break; } if(inserted) { ob.observeArr(inserted) } ob.dep.notify()
return result }, false) })
|
依赖收集完整代码
Dep类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export default class Dep { constructor() { this.subs = [] } addSubs(sub) { this.subs.push(sub) } depend() { if(Dep.target) { this.addSubs(Dep.target) } } notify() { const subs = this.subs.slice()
for(let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
|
Watcher类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import Dep from "./Dep"; export default class Watcher { constructor(target, expression, callback) { this.obj = target this.getter = parsePath(expression) this.callback = callback this.value = this.get() } update() { this.getAndInvoke(this.callback) } get() { Dep.target = this console.log("全局位置",Dep.target);
var value value = this.getter(this.obj)
Dep.target = null
return value }
getAndInvoke(cb) { const value = this.get()
if(value !== this.value || typeof value == 'object') { const oldValue = this.value this.value = value cb.call(this.obj, value, oldValue) } } }
function parsePath(str) { let segments = str.split('.')
return (obj) => { for(let i=0; i<segments.length; i++) { obj = obj[segments[i]] }
return obj } }
|