引用 深入浅出Object.defineProperty()
勾三股四的 Vue.js 源码学习笔记
vue.js关于Object.defineProperty的利用原理
一 属性定义 Object.defineProperty()语法说明
通过Object.defineProperty()为对象定义属性,有两种形式,且不能混合使用,
分别为数据描述符,存取描述符
数据描述符(value,writable)
let Person = {}
Object.defineProperty(Person, 'name', {
value: 'jack',
writable: true // 是否可以改变
})
存取描述符 --是由一对 getter、setter 函数功能来描述的属性(get,set)
let Person = {}
let temp = null
Object.defineProperty(Person, 'name', {
get: function () {
return temp
},
set: function (val) {
temp = val
}
})
数据描述符和存取描述均具有以下描述符
configrable 描述属性是否配置,以及可否删除
enumerable 描述属性是否会出现在for in 或者 Object.keys()的遍历中
configurable: false 时,不能删除当前属性,且不能重新配置当前属性的描述符(有一个小小的意外:可以把writable的状态由true改为false,但是无法由false改为true),但是在writable: true的情况下,可以改变value的值
configurable: true时,可以删除当前属性,可以配置当前属性所有描述符。
注意 以下二种区别
Person.name=‘jack’
let Person={}
Person.name='jack'
//等价于
Object.defineProperty(Person, 'name', {
value: 'jack',
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(Person, ‘age’, { value: ‘28’})
Object.defineProperty(Person, 'age', { value: '28'})
//等价于
Object.defineProperty(Person, 'age', {
value: '28',
writable: false,
enumerable: false,
configurable: false
});
Object.keys(Person)(enumerable false)
Person.age=‘30’ 不可修改( writable:false)
Object.defineProperty(Person, ‘age’, { value: ‘32’}) 不可重新定义 (configurable:false)
对象属性的 不变性
对象常量 writable: false 和 configurable: false
结合writable: false 和 configurable: false 就可以创建一个真正的常量属性(不可修改,不可重新定义或者删除)
禁止扩展 Object.preventExtensions(…)
如果你想禁止一个对象添加新属性并且保留已有属性,就可以使用Object.preventExtensions(…)
在非严格模式下,创建属性gender会静默失败,在严格模式下,将会抛出异常
密封 Object.seal()会创建一个密封的对象,
这个方法实际上会在一个现有对象上调用object.preventExtensions(…)并把所有现有属性标记为configurable:false
所以, 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以改属性的值)
冻结 Object.freeze()
Object.freeze()会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(),并把所有现有属性标记为writable: false,这样就无法修改它们的值。
这个方法是你可以应用在对象上级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(但是这个对象引用的其他对象是不受影响的)
你可以深度冻结一个对象,具体方法为,首先这个对象上调用Object.freeze()然后遍历它引用的所有对象,并在这些对象上调用Object.freeze()。但是一定要小心,因为这么做有可能会无意中冻结其他共享对象。
总结 属性定义
1 如果Obj没有名为Prop的自身属性的话:如果Obj是可扩展的话,则创建Prop这个自身属性,否则拒绝
2 如果Obj已经有了名为Prop的自身属性:则按照下面的步骤重新配置这个属性
3 如果这个已有的属性是不可配置的,则进行下面的操作会被拒绝
1: 将一个数据属性转换成访问器属性,反之变然
2: 改变`[[Configurable]]`或`[[Enumerable]]`
3: 改变[[Writable]]由false变为true
4: 在`[[Writable]]`为`false`时改变`[[Value]]`
5: 改变[[Get]]或[[Set]]
4 否则这个已有的属性可以被重新配置
二 属性赋值 通过obj.prop = ''prop"形式
1 如果在原型链上存在一个名为P的只读属性(只读的数据属性或者没有setter的访问器属性),则拒绝
2 如果在原型链上存在一个名为P的且拥有setter的访问器属性,则调用这个setter
3 如果没有名为P的自身属性,则如果这个对象是可扩展的,就创建一个新属性,否则,如果这个对象是不可扩展的,则拒绝
4 如果已经存在一个可写的名为P的自身属性,则调用Object.defineProperty(),该操作只会更改P属性的值,其他的特性(比如可枚举性)都不会改变
三 数据双向绑定
view
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div>
<p>你好,<span id='nickName'>123</span></p>
<div id="introduce">...</div>
</div>
</body>
</html>
数据绑定
//视图控制器
var userInfo = {};
Object.defineProperty(userInfo, "nickName", {
get: function(){
return document.getElementById('nickName').innerHTML;
},
set: function(nick){
document.getElementById('nickName').innerHTML = nick;
}
});
Object.defineProperty(userInfo, "introduce", {
get: function(){
return document.getElementById('introduce').innerHTML;
},
set: function(introduce){
document.getElementById('introduce').innerHTML = introduce;
}
})
userinfo
双向绑定演示
原始页面
更改数据
userInfo.nickName='cch'
userInfo.introduce=' hello'
更新后的页面
总结
而vue.js主要利用了accessor descriptors的set和get来更新视图,上面这里看到的这个例子挺好,是一个简单的绑定。
这个例子只是数据和dom节点的绑定,而vue.js更为复杂一点,它在网页dom和accessor之间会有两层,一层是Wacher,一层是Directive,比如以下代码。
var a = { b: 1 }
var vm = new Vue({
data: a
})
把一个普通对象(a={b:1})传给 Vue 实例作为它的 data 选项,
Vue.js 将遍历它的属性,用Object.defineProperty 将它们转为 getter/setter,如图绿色的部分所示。
每次用户更改data里的数据的时候,比如a.b =1,setter就会重新通知Watcher进行变动,Watcher再通知Directive对dom节点进行更改。
四 数据劫持,数据监听机制
view
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>
数据劫持
一般对数据的劫持都是通过Object.defineProperty方法进行的,
Vue中对应的函数为 defineReactive
,
其普通对象的劫持的精简版代码如下:
var foo = {
name: 'vue',
version: '2.0'
}
function observe(data) {
if (!data || typeof data !== 'object') {
return
}
// 使用递归劫持对象属性
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
})
}
function defineReactive(obj, key, value) {
// 监听子属性 比如这里data对象里的 'name' 或者 'version'
observe(value)
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newVal) {
if (value === newVal) {
return
} else {
value = newVal
console.log(`监听成功:${value} --> ${newVal}`)
}
}
})
}
observe(foo)
foo.name = 'angular' // “监听成功:vue --> angular”
foo.version='1.0' //监听成功:1.0 --> 1.0
get ,set 的方法名可以不写 reactiveGetter /reactiveSetter
function defineReactive(obj, key, value) {
// 监听子属性 比如这里data对象里的 'name' 或者 'version'
observe(value)
Object.defineProperty(obj, key, {
get: function () {
return value
},
set: function (newVal) {
if (value === newVal) {
return
} else {
value = newVal
console.log(`监听成功:${value} --> ${newVal}`)
}
}
})
}
监听结果
上面完成了对数据对象的监听,接下来还需要在监听到变化后去通知订阅者,这需要实现一个消息订阅器 Dep
,Watcher通过 Dep
添加订阅者,当数据改变便触发 Dep.notify()
,Watcher调用自己的 update()
方法完成视图更新。
vue.js 数据监听机制 源码
如何监听某一个对象属性的变化呢?我们很容易想到 Object.defineProperty 这个 API,为此vue属性设计一个特殊的 getter/setter,然后在 setter 里触发一个函数,就可以达到监听的效果。
vue源码
总结 vue对数据对象的监听
上面完成了对数据对象的监听,接下来还需要在监听到变化后去通知订阅者
- Observer 数据监听器
负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。
- Compiler 指令解析器(vue文件中的的内容)
扫描模板,并对指令进行解析,然后绑定指定事件。
- Watcher 订阅者
关联Observer和Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。Update()是它自身的一个方法,用于执行Compile中绑定的回调,更新视图。
这需要实现一个消息订阅器 Dep
,
Watcher通过 Dep
添加订阅者,
当数据改变便触发 Dep.notify()
,
Watcher调用自己的 update()
方法完成视图更新。