深入浅出vue.js-----(1)object的响应式

vue会自动通过状态生成dom,然后将其输入到页面上显示出来,这个过程叫做渲染。vue的渲染过程是声明式的,我们通过模板来描述状态和dom之间的映射关系。

通常在运行时应用的内部状态是不断改变的,在改变的过程中要不断地进行重新渲染。那么要知道他们内部状态什么时候发生改变,就要进行变化侦测!变化侦测要对每个对象的每个属性进行侦测。其中的每个属性都会设置一个数组来收集订阅该属性的,收集到的叫做依赖,对于这个数组来说可能订阅这个有很多,一开始每个使用这个数据的dom节点都会被当成依赖收集进去,但是每个组件可能有多个地方使用这个数据,如果有大量的dom节点都使用了这个数据,那么在存储的数组的内存上消耗是很大的。而且如果要通知更新,要去遍历,内存消耗也是很大的。所以在vue2.0之后,依赖收集的是组件为单位的了,这样就明显可以减少依赖的数量。

关于追踪变化,大多数了解vue的都会知道Object.defineProperty和es6的Proxy。由于使用Object.defineProperty存在很多缺陷,所以尤雨溪说以后也要对其进行重写。但是接下来我们还要根据书上的学,使用object.defineProperty,因为即使使用Proxy重写,但是原理也是相似。

直奔主题,定义响应式

将一个对象、他的属性和属性所对应的值传进defineReactive(定义响应式的意思),当去获取这个对象的时候,就会触发getter,如果去设置这个属性的值得时候就会触发setter。那么就是说在获取的时候,在getter里面可以进行操作,来记录是谁来获取这个属性的值,并且把它记录起来保存在一个数组中,这样就会记录下来所有尝试获取这个属性值的‘东西’。另外每个获取这个属性的都会被记录下来,那么当这个属性值改变的时候,我们就去那个记录的数组中通知这些所有的‘东西’去把之前获取的这个值更新成新的值。

/**
 * 
 * @param {*} data 数据对象
 * @param {*} key 对象的key值
 * @param {*} value 对象key所对应的值
 * 这里只是以一个属性为例,因为要完成所有属性的响应式要进行遍历的
 *
 * 
 */
function defineReactive (data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,//是否可枚举,也就是是否可以遍历
    configurable: true,//是否可以删除或者修改
    get: function () {//获取的时候,可以获取
      return value
    },
    set: function (newValue) {//设置的时候如果新值不等于旧值重新赋值
      if (value !== newValue) {
        value = newValue
      }
    }
  })
}

 

(下面我们将一个属性的值改变称为状态的改变,因为书中介绍的就是这样写的) 刚刚说到尝试获取这个属性值的这个‘东西’,这个称为依赖,就是每一个去获取的都是一个依赖,都会被推入一个数组中,每一个具体的依赖是一个dom节点。当状态发生改变的时候,会通知每一个依赖,然后进行dom更新操作。但是,如果每个dom都是一个依赖的话,我们想一想,一个组件里面可以有好多节点去获取这个属性的值,那么这个存储依赖的数组就会变得很多,每个状态绑定的依赖越多,依赖追踪在内存上的开销就越大,vue2.0增加了虚拟dom,之后依赖并不是一个dom节点了,而是一个组件。这样状态改变后,会通知到组件,组件内部再使用虚拟dom进行对比。这样大大降低了依赖数,降低了追踪依赖所消耗的内存。

 

 如何进行收集依赖?

之所以观察数据,就是当数据改变的时候,要通知那些曾经使用了这个数据的地方更新状态

例如

 <template>

 <div>{{name}}</div>

 </template>

 当name发生该改变的时候,要通知使用他的地方(vue2.0中模板使用数据等同于组件使用数据,当数据发生便化的时候会通知组件,组件再根据虚拟dom重新进行渲染)

 另外每一key都对应一个数组来存储当前订阅key的依赖。假设依赖是一个函数,保存再window.target上,就可以将之前的函数改造一下

 


function defineReactive (data, key, value) {
  let dep = [];//新增收集依赖
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (window.target) {//新增收集依赖
        dep.push(window.target)
      }
      return value;
    },
    set: function (newValue) {
      if (newValue !== value) {
        dep.forEach(item => {  //新增,通知每个依赖函数更新值
          item(newValue, value)
        })
        value = newValue
      }
    }
  })
}

为了增强dep的独立性,给dep封装一个类,专门来管理依赖,包含添加依赖,通知更新,另外还有删除依赖的

其实关于这是发布订阅模式,订阅是依赖订阅这个属性状态,发布是这个存储的数组里所有的依赖进行更新

class Dep {
  constructor() {
    this.subs = []
  }
  addSub (sub) {//添加依赖的时候
    this.subs.push(sub)
  }
  depend () {
    if (window.target) {
      this.addSub(window.target)
    }
  }
  removeSub () {//删除某个依赖的时候
    remove(this.subs, sub)
  }
  //发布更新的时候,上面的时候我们说把依赖是挂在到全局的window.target上的,我们要通知window.target让他取调用自己updata方法去更window.target新
  notify () {
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i++) {
      subs[i].updata()
    }
  }
}
// 删除依赖的操作
function remove (arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      arr.splice(index, 1)
    }
  }
}

// 以上这个集订阅,发布和删除的于一身的Dep类封装完毕,接下来就是defineReactive的修改
function defineReactive (data, key, value) {
  const dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend()
      return value
    },
    set (newValue) {
      if (value !== newValue) {
        dep.notify()
        value = newValue
      }
    }
  })
}

上面所提到,依赖是windo.target,而且当时我们假设他是个函数,而且当我们发布的时候还调用了他的updata方法,那么依赖window.target到底是什么

我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既可以是模板,也有可能是用户写的一个watch , 这时需要抽象一个能够集中处理这些情况的类.然后我们再收集依赖的阶段只收集封装好的这个类的实例进来,通知也通知他一个,接着他在负责通知其他用到数据的地方,而这个就是watch

 * watch的用法:vm.$watch(path , function(value , oldValue){}) , vm代表的是一个组件 , a.b.c代表属性的字符串 , 后面的function是data.a.b属性发生变化的时候的回调

/**
 * 
 * @param {*} path 
 * 这里面path是一个字符串'a.b.c'
 * 要将他转化成取值的操作,这只是个解析简单的路径的
 */
let reg = /^\w.$/
function parsePath (path) {
  if (reg.test(path)) return
  const sefment = path.split('.')
  console.log(sefment);
  // sefment=['a' , 'b' , 'c']所以应该是先是a , 然后是a.b, a.b.c
  return function (obj) {
    for (let i = sefment; i < sefment.length; i++) {
      if (!obj) return
      obj = obj[obj[i]]
    }
    return obj
  }

}
class Watcher {
  constructor(vm, path, cb) {
    this.vm = vm;
    // this.getter返回的是一个函数,这个函数执行的时候会获取obj.a.b.c里面的数据
    this.getter = parsePath(path);
    this.cb = cb
    this.value = this.get()//初始化获取到的数据,如果没有更新状态,这里是新的,更新了状态之后这里存的是旧值,要用get到的新值覆盖
  }
  get () {
    window.target = this
    // 在下面的一步中this.vm执行继承了getter函数(其实this.vm指的就是这个组件),并且执行了该函数,将this.vm作为参数穿进去,另外这一步是获取值的过程,在获取的时候会触发我们定义的响应式里的收集依赖的操作,这个时候window.target是this,也就是watcher这个实例!!将window.target收集完了之后,将其置为空。第二个参数this.vm,把这个组件上所有的挂载的数据事件等传过去
    let value = this.getter.call(this.vm, this.vm)
    window.target = undefined
    return
  }
  // 上面说到了,状态dep只是通知到watcher中,而watcher应该来通知所有的来更新,另外存储在this.value中的数据是旧的,get到的才是新的
  updata () {
    const oldValue = this.value;
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

另外,我们先打印一下一个组件的this

我们可以看到,那个红色的是我定义methods里的方法和data里面的数据,都是挂载到这里的,this.vm就是这个,将一整个组件传入 ,这样let value = this.getter.call(this.vm, this.vm)就很好理解了。

parsePath返回的一个函数,函数里面有个obj对象,this.getter.call里面的第二个参数就是obj,所以obj能拿到数据就很正常了,一开始我也比较困惑。

 

最后我们要对每个属性做到响应式,意思就是遍历所有的属性值,如果遇到对象的话,再对其进行劫持(所谓的劫持就是new Observe)

//以上是对一个属性的封装 , 接下来要递归所有的key
class Observe {
  constructor(value) {
    this.value = value
    //因为关于定义响应式有两种情况,这是第一种对象的情况,数据的情况下次再讲
    if (!Array.isArray()) {
      this.walk(value)
    }
  }
  walk (value) {
    const keys = Object.keys(value)
    for (let i = 0; i < keys.length; i++) {
      this.defineReactive(value, keys[i], value[keys[i]]);
    }
  }
  defineReactive (data, key, value) {
    if (typeof value === 'object') {
      new Observe(valued)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get () {
        dep.depend();
        return value
      },
      set (newValue) {
        if (value !== newValue) {

          value = newValue
          dep.notify();
        }
      }
    })
  }
}

 

现在终于可以对这个图进行解读了,以前都还是搞不懂的。

首先,在beforeCreate阶段会对data进行数据劫持,数据劫持(observer)就是将data里面所有的对象属性转化为setter/getter,在这里是没有进行模板编译环节的,是不会产生watcher的。 

到了模板编译环节的时候,会根据模板上的有v-的值,到data中去获取,这样就触发了getter,getter触发之后会将这个watcher实例加入到订阅的对应的属性的数组中。当组件状态进行改变的时候,那么就会到这个属性保存的数组中去通知watcher , watcher再去集中更新数据,然后在页面上改变数据。大概就是这个样子。

以上来源于对深入浅出vue.js,有些内容是自己的理解,如果有错误,请指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值