1. 变化侦测是啥

当组件中的状态发生了变化,Vue会天然的知道状态变化了,并根据状态生成DOM进行渲染.
通常,在应用运行时,内部的状态在不停的变化,那该如何知道是哪些状态发生了变化呢?
变化侦测(也有叫数据绑定的?)就是来解决这个问题的.
不同于React和Angular的'拉'的方式,Vue属于'推'.当状态发生变化时,Vue立马就知道了,而且知道是哪些状态发生了变化.
假如说有一个状态name绑定着好多个依赖,每个依赖就是一个DOM节点,当name发生变化时,Vue会向依赖name的所有依赖发送通知,让DOM节点进行更新操作.
但是这样做会有一定的代价,如果每一个状态都绑定着很多的依赖,每次更新操作时的依赖追踪的开销会很大.因此,Vue从2.0开始引入了虚拟DOM,每个状态所绑定的依赖不再是某个DOM节点,而是一个组件.当状态发生变化时,通知到组件即可,组件内部再使用虚拟DOM进行比对.

2. 咋就知道数据变了呢

如何侦测到数据变化了呢?
好吧,其实你们已经都知道了,那就是使用强大的Object.defineProperty和ES6的Proxy.只是目前ES6在浏览器的兼容性并不理想,到目前为止Vue仍使用Object.defineProperty来实现(尤大大说以后会用Proxy进行重写的,不过无妨,原理都是一样的)
那么,尝试封装一个函数吧:

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
    }
  })
}

defineReactive翻译一下叫做定义响应式,已经很直白了,好伐?每当从obj中读取key属性时,都会触发get函数;每当给obj的key属性设置数据时,都会出发set函数

3. 依赖是啥

如果只是定义了defineReactive,并没有什么卵用(反手就给我两巴掌).
让我们用劫持的思想考虑一下,是不是可以在get和set函数里面搞一些事儿呢?
答案是:必~须~的~
举个糖炒例子:

<template>
  <span>{{name}}</span>
</template>

模板中用到了name数据,那必然是先在初始化时获取name,那必然就会触发get函数;当给name赋值时,必然会触发set函数.
捋顺了,在get函数中收集依赖,在set函数中通知依赖,那就尝试定义一个Dep类吧:

  let id = 0
  function Dep () {
    this.id = id ++
    this.subs = []
  }

  Dep.prototype.addSub = function addSub(sub) {
    this.subs.push(sub)
  }

  Dep.prototype.depend = function depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
  }

  Dep.prototype.notify = function notify () {
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i ++ ) {
      subs[i].update()
    }
  }

  Dep.prototype.removeSub = function removeSub (sub) {
    //从数组中删除
    remove(this.subs, sub);

  };

  Dep.target = null
}

然后再修改一下defineReactive:

function defineReactive(data, key, val) {
  const dep = new Dep() //新增
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      //新增
      if (Dep.target) {
         dep.depend()
      }
      return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify() //新增
    }
  })
}

Dep.target是个啥?Dep.prototype.depend和Dep.prototype.notify都是干嘛用的?
简单来说,Dep.target是个全局唯一,完全可以用window.target来表示.Dep.prototype.depend嘛,就是收集依赖,依赖就保存在Dep.target里,再调用依赖的addDep来建立关系.Dep.prototype.notify就是当数据发生变化了,通知依赖.
这时候你就会想,是不是感觉缺点什么?这个依赖貌似很有用的样子,是不是要封装一下?

function Watcher(vm, expOrFn, cb) {
  this.vm = vm
  this.cb = cb
  this.value = this.get()
  this.depIds = {}
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    this.getter = parseGetter(expOrFn.trim())
  }
}

Watcher.prototype.addDep = function (dep) {
  if(!this.depIds.hasOwnProperty(dep.id)){
    dep.addSub(this)
    this.depIds[dep.id] = dep
  }
}

Watcher.prototype.get = function () {
  Dep.target = this
  const value = this.getter.call(this.vm, this.vm)
  Dep.target = null
  return value
}

Watcher.prototype.update = function () {
  const oldValue = this.value
  this.value = this.get()
  this.cb.call(this.vm, this.value, oldValue)
}

function parseGetter (exp) {
  const bailRE = /[^\w.$]/
  if (bailRE.test(exp)) return
  const exps = exp.split('.')
  return function (obj) {
    for (let i = 0; i < exps.length; i++) {
      if (!obj) return
      obj = obj[exps[i]]
    }
    return obj
  }
}

在get方法中先把Dep.target设置为this,紧接着读取表达式的值,肯定会触发表达式的get方法
触发了get,就会进入if(Dep.target),将watcher和dep建立关系,dep里用subs保存所有依赖于这个表达式的watcher,watcher里用depId保存着所观察的dep,并且不会重复添加
这样Vue就知道当前表达式变化的时候,要通知哪一个watcher
接着修改表达式的值,触发set方法,dep.notify()会通知dep中的每一个watcher去执行update方法
update方法会把旧值和新值传递给callback
是不是有小伙伴对parseGetter有点好奇,其实并不复杂,内部就是一个循环读取,举个例子:
当前表达式为'a.b.c',经过循环之后,最后的obj为c的值

4.递归侦测

经过以上的折腾,我们终于可以侦测data中某一个数据的某一个属性了,对,只是某一个不是全部.
我们希望定一个类,在对data数据进行劫持的时候,对每一个数据都进行侦测:

function Observer(value) {
  this.value = value
  if(!Array.isArray(value)){
    this.walk(value)
  }
}

Observer.prototype.walk = function (obj) {
  const keys = Object.keys(obj)
  for (let i = 0; i< keys.length; i ++) {
    defineReactive(obj, keys[i], obj[keys[i]])
  } 
}

修改一下defineReactive:

function defineReactive(data, key, val) {
  if(typeof val === 'object'){
    new Observer(val)
  }
  ...省略代码

Observe目前只考虑Object的情况,Array下一篇章再讲.
要想侦测一个object中的每一个属性,只需要使用递归就好了,在劫持之前,先判断当前的val是否是对象,如果是,就去递归.如果不是,那就放心的劫持吧~
经过这样的折腾,只要将任何一个object传到Observe中,那么这个object就是一个响应式的了.欧耶!

5. 有啥问题

由于是使用Object.defineProperty的方式来变化侦测,那么就会有些问题:
比如,在data中定义了一个obj,它天然就是响应式的

obj: {
    name: 'ntyang'
}

但是,当我们这样操作时,Vue无法侦测到这种变化

methods: {
  handler(){
    this.obj.address = 'bj'
    delete this.obj.name
  }
}

Object.defineProperty只能设置getter/setter来追踪修改和获取,并不能追踪到新增或删除(除非来一个deleter/adder?).不过也别怕,Vue提供了vm.$setvm.$delete两个API来帮助我们实现,以后会讲的哦


参考:

  1. https://github.com/DMQ/mvvm

2.<深入浅出Vue.js> 刘博文著 人民邮电出版社


ntyang
165 声望29 粉丝

非典型性代码搬运工,搞点儿事情