对象操作和数据驱动
对象访问和写入
访问:
属性访问 a.b
键访问 a['b']
let obj = {a:1}
obj.a // 1
obj['a'] //1
两者在一般情况下是等价的,但有如下注意的点
- 属性访问要求属性名满足标识符的命名规范,键访问属性名可以是任意字符串
- 键访问允许使用变量
- 对象属性的键值一定是字符串,使用其它类型会自动转成字符串
var obj1 = {a:1}
var obj2 = {b:2}
var objText = {}
objText[obj1] = 3
console.log(objText[obj2]) // 3
此时objText的结构是 { [object object]: 3 },所以我们平时用引用类型做属性的key时要注意序列化
写入:
对象的写入通常和它的定义有关 对象的定义有两种
构造式 : var obj = new Object()
构造时是接受参数的,但一般不传
在传基本类型的参数会对该值进行相应类型的对像包装
var a = new Object(1)
等价于
var a = new Number(1)
最奇葩的是参数是对象,构造函数会返回对象本事,也就是
var obj = {a:1}
var obj2 = new Object(obj)
obj === obj2 // true
构造式不能在定义时写入属性
可计算属性名
在es6语境下,我们可以在声明式定义时直接使用可计算属性名
let obj = {a:1}
let objEg = {
[obj.a]:1,
[obj.a + 1]: 2
}
以上是比较常规的对象读写方式
ES6引入的对象概念和读写(存取)方式
对象描述符 :描述对象属性的特征属性
//我们可以显式的访问
Object.getOwnPropertyDescriptor(obj,'a')
//{
// value:1,
// writable: true, 可写
// enumerable, true, 可枚举
// comfigurable, true 可配置
//}
// 也可以显式的改写
Object.defineProperty(obj,'a',{
value:1,
writable: false,
enumerable, false,
comfigurable, false
})
writable:false
obj.a = 2
obj.a === 1 //true
enumerable :false
a in obj // ture
let keys = []
for(let key in obj){
keys.push(key)
}
keys //[]
comfigurable, false
无法再次使用defineProperty对该属性进行操作
Object.defineProperty(obj,'a',{
value:2,
writable: true,
enumerable, true,
comfigurable, true
})
Object.getOwnPropertyDescriptor(obj,'a')
//{
//value:1,
// writable: false,
// enumerable, false,
// comfigurable, false
//}
Getter/Setter
我们执行属性读取是,实际上会隐式调用用对象默认的[[get]]和[[set]]
[[get]]:顺着原型链去寻找特定属性名的值
[[set]]:检查是否有显式定义的setter,有,调用setter。 无,检查writable属性。ture,赋值;false,失败或报错
从上面我们可以看出,我们可以通过显式的声明部分影响默认操作
ES5 //
let obj = {
_a: 1,
get a(){
return this._a
}
set a(value){
this._a = value
}
}
局限:必须在对象定义时对属性的getter/setter进行手动声明,不方便用逻辑(函数)控制
ES6
let obj = {
a:1
}
Object.defineProperty(obj,'b',{
value:2,
writable: true,
enumerable, true,
comfigurable, true,
get: function(){
retrun this.a *2
}
set: funtion(value){
this.a = value
}
})
obj.b = 1
obj.b // 2
defineProperty不仅可以用以创造新的属性,还可以用以改写已有属性
对getter和setter的声明会使得value/writeable失效
数据驱动的实现
注:这里的数据驱动是广义的,指以数据的更新触发预设逻辑
1.轮询
场景:可形变的DOM元素的形变,触发回调
实现:轮训比较目标对象的目标状态,不一致(或满足条件)时触发回调
//代码仅供参考
window.onresizeOfDom = function(el,callback){
let offsetHeight = el.offsetHeight
let offsetWidth = el.offsetWidth
let newH,newW
let params
setinterval(function(){
newH = el.offsetHeight
newW = el.offsetWidth
if(newH !== offsetHeight || newW !==offsetWidth){
params = { el,newH,newW,
oldH:offsetHeight,
oldW:offsetWidth
}
callback.call(this,params)
offsetHeight = newH
offsetWidth = newW
}
},100)
}
代表:web应用的布局系统;goldenLayout
局限:比较耗费资源,异步代码容易延迟响应,适合监听少量容易被外部影响的对象。不适合数据驱动框架的底层支持
2.函数式/更新声明
顾名思义是在数据变更后主动声明数据变更
数据更新 -> 声明更新 -> 回调逻辑
但在工程中通常会优化成:
声明更新 ->( 更新数据 -> 回调逻辑 )
我们最熟悉的两种实例
react: setState -> (更新state -> render/自定义回调)
redux: dispatch -> (更新reducer -> 执行listener队列 )
这种优化的好处在于可控制,可预测。因为每一次数据的改变都有对应的声明,这使得数据的改变是有迹可循的,可追溯的。同时数据的实际更新在框架内部完成,也保障了数据的安全性
优点:可控,安全,保证了可编程性
局限:使用门槛相对较高,使用时通常引入附加概念
这是我觉最稳定的数据驱动的实现思路,它的具体实现涉及到函数式编程,数据的传递,复制和比较,在之前的分享中做过简单涉及,这里不做展开
3.观察者模式(observer + watcher)
本质:使用getter/setter劫持属性的读写,目的是属性改变时可以通知该属性的观察者,并调用预设的更新回调。
核心概念 :observer,dep,watcher
这三个概念分别对应了一个构造函数和它的实例
Observer/observer :负责改造对象,劫持对象属性和递归子属性的读写操作。
使属性在读写时可与属性的观察者watcher发生关联
ob = new Observer(value)
Observer 会对传进来的参数进行类型判断,如果参数是一般对象,执行核心逻辑walk
//walk:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 对对象的所有可枚举属性进行劫持
defineReactive(obj, keys[i])
}
}
//defineReactive:
export function defineReactive (
obj: Object,
val :Any
)
{
// 忽略所有和dep相关的代码,defineReactive就做了一件事
//,改写属性的getter/setter,使它们与特定的watcher发生关联
const dep = new Dep()
// 递归操作所有后代属性
let childOb = new Observer(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
if (Dep.target) {
// 如果有watcher就与watcher建立联系
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
Dep.target = null
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
// 重新劫持新的属性值
let childOb = new Observe(newVal)
// 通知相关的watcher,调用它们的回调
dep.notify()
}
})
}
你可能会问 这样对对象属性进行劫持,如果碰到循环引用的对象怎么办,比如
let p = {}
let s = {
p
}
p.s = s
实际上,有一层阻断机制
if(hasOwn(obj, '__ob__') && value.__ob__ instanceof Observer){
ob = new Observer(obj)
obj._ob_ = ob
}
可能又有人问了,这样不会触发obj的setter吗,从而使_ob_ 也被纳入监视
答案是不会,不管obj在那一层次都不会
let a = {
b:{
c:{
d :1
}
}
}
a.b.c.e = 2 // 不会处罚任何setter,因为被监视属性a,b,c,d的值都没有变
我们把dep放一放,先说wathcer ,留下几个未明确的概念
Dep.target
dep.depend()
dep.notify()
我们模拟一个简单的wacther
import Dep from '.....dep'
class Watcher {
constructor(
vm:VM, // 最外层对象的挂载对象
key:String, //目标属性的路径 eg:a.b.c
cb:Function
){
// 触发目标属性的getter
this.value = this.get()
this.cb = cb.bind(vm)
}
get(){
Dep.target = this
let keyArr = key.split('.')
let value = vm
keyArr.forEach(k,i){
// 访问被wtach属性,触发该属性的getter
value = value[k]
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.depIds.has(id)) {
// 现在dep又多了个概念 addSub
dep.addSub(this)
this.depIds.add(id)
}
}
}
}
Dep/dep 属性和watcher之间的桥梁
Dep 即是构造函数又是全局变量。Dep.target保存当前阶段代绑定的watcehr。
class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].cb()
}
}
}
梳理一下watcher 与属性建立联系的过程
// 建立监视联系:
Watcher.constructor()
|
watcher.get()
| —— Dep.target = watcher
value[[get]]
|
dep.depend()
|
Dep.target.addDep(dep) —— watcher.addDep(dep)
|
dep.addSub(wather)
|
dep.subs.push(wather)
//属性改变,watcher响应
value[[set]]
|
dep.notify()
|
dep.subs.forEach()
|
watcher.cb()
为什么修改数组某一项的值不会触发响应
defineProperty 只能对一般对象进行操作,vue的Observer对对象进行处理时会直接,跳过这一层,对子项建立ob对象
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
let ob = new Observe(items[i])
}
}
Observe操作的是对象,但劫持的对象的属性
代表:vue,mobx
优点:灵活;性能相对较高,省略了很多中间的计算过程(复制,比较)
局限:
1.只能监听一般对象的属性。
2.最外层对象的引用不能变。
3.es版本限制,
4为属性添加新值不会触发setter
5.太灵活了!