对象操作和数据驱动

对象操作和数据驱动

对象访问和写入

访问:

属性访问 a.b
键访问 a['b']

 let obj = {a:1}
 obj.a // 1
 obj['a']  //1

两者在一般情况下是等价的,但有如下注意的点

  1. 属性访问要求属性名满足标识符的命名规范,键访问属性名可以是任意字符串
  2. 键访问允许使用变量
  3. 对象属性的键值一定是字符串,使用其它类型会自动转成字符串
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劫持属性的读写,目的是属性改变时可以通知该属性的观察者,并调用预设的更新回调。
核心概念 :observerdepwatcher
这三个概念分别对应了一个构造函数和它的实例

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.太灵活了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值