Vue响应式原理和实现方式,Observer、Dep、Watcher源码解析(下),深入浅出

829 阅读3分钟

通过上篇的初步分析和解读,大家对Vue应该都有了大概的了解,这一篇主要针对源码进行分析。

相信大家看完之后,自己动手写实现响应式原理 的核心代码也不成问题。

这是下篇,还有上篇

附示例源码

以下代码略有删减,但不印象总体流程分析,部分删减的依赖代码可详细查看vue源码

简介

在解读之前,我们先看一下这三个类之间的调用时序图,这样有助于大家理清这几个类之间的关系。如有错误请大家指正。

流程图.png

从上图可以看出初始化时会调用Watcher.get()方法,用于收集依赖关系,然后将依赖项存放在Dep类中。当用户重新赋值时,直接调用依赖项中的update方法。但最后的依然会调用Watcher.get(),重复上面的流程,如此往复,形成闭环。

Observer类源码解读

首先是Observer类,其核心思路是对传入的value,深度遍历每一项并使用defineProperty劫持getset方法

const NO_INIITIAL_VALUE = {} // 该对象setter方法为空
class Dep {}
class Observer {
  dep
  constructor(value, shallow) {
    // 每个Observer实例下都挂载了一个Dep
    this.dep = new Dep()
    Object.defineProperty(value, '__ob__', { value: this})
    if (Array.isArray(value)) {

      // shallow 默认是 undefined,进行深度观测。
      // 当观测vue属性$attr,$lisenter时shallow为 false
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      this.walk(value, shallow)
    }
  }
  // 用于Object类型部署defineProperty
  walk(obj, shallow) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      defineReactive(obj, key, NO_INIITIAL_VALUE, undefined, shallow)
    }
  }
  // 用于Array类型部署defineProperty
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

下面是Observe类中提供的observe方法,其主要功能是将value及其属性值实例化为Observer对象,与Observer类形成递归的关系,循环遍历所有子属性,直到属性值是一个不可扩展的类型,比如string,number

let shouldObserve = true
function observe(value, shallow){
  let ob
  if (value.__ob__ instanceof Observer) {
    // Observer对象只会实例化一次
    ob = value.__ob__
  } else if (shouldObserve && Object.isExtensible(value) ) { 
    // isExtensible判断value是一个可扩展的对象,如果是Number、String类型,则Observer结束
    // shouldObserve是一个开关,可以控制是否需要添加观察者
    // 比如处理props数据时,请查看updateChildComponent
    ob = new Observer(value, shallow)
  }
  return ob
}

使用Object.defineProperty劫持getset的核心方法defineReactive

function defineReactive(obj, key, val, customSetter, shallow) {

  // 利用闭包原理,每个key下面都创建对应的 dep.subs 空间
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  
  // 保存原始的getset方法
  const getter = property && property.get
  const setter = property && property.set

  // 根据不同的参数对val进行特殊处理
  // ps:obj为Object.create(null)时,getter为空
  if ((!getter || setter) && (val === NO_INIITIAL_VALUE || arguments.length === 2)) {
    val = obj[key]
  }
  
  // 深度遍历子属性,并转化成Observe实例
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    // 劫持get
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      // 取值时,从Dep.target开始,自顶向下收集dep依赖关系
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      // ref类型取存放在value.value中
      return  value
    },
    // 劫持set
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      // value和newVal一致,说明无更新,不需要`notify`
      if (value===newVal) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // ps:只读的属性,setter为空
        return
      }else {
        val = newVal
      }
      // 深度遍历子属性,并转化成Observe实例
      childOb = !shallow && observe(newVal)
      // 触发对应的依赖进行更新,可以理解为是发布事件
      dep.notify()
    }
  })
  return dep
}
let obj = { data:{ person: { name:'xiaomin', age:'30'}}}
observe(obj)
console.log(obj)

以上代码可以复制控制台运行,这样可以更直观的看出效果,obj转换成Observe实例后,结构大概如下:

image.png

以上就是vueObserve实现的的核心代码,一个对象通过observe改造后就具有了响应式的特性,同时附加Dep实例,用于关联Watcher类。但是还不完整,如果对obj的属性赋值会发现报错,因为Dep这个类还没有实现

Dep类源码解读

下面我们接着看Dep类的实现

let uid = 0
// Watcher类是DepTarget类的实现,但是在Dep中只会用到下面的几个属性和方法
// 为了支持ts语法,所以在此事先声明,便于大家理解
class DepTarget {
  id
  addDep(){}
  update(){}
}
// Dep 是用来管理和存放Watcher的类,而Dep又寄生在每一个Observe实例下面
class Dep {
  //target是一个静态属性,全局共享,值为空或Watcher实例   
  static target = null
  id
  subs
  constructor() {
    this.id = uid++
    // subs存放的是Watcher实例
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    const index = this.subs.indexOf(sub)
    if (index > -1) {
      return this.subs.splice(index, 1)
    }
  }
  depend(info) {
    // 向Watcher传递this
    // 在通过addSub、removeSub管理 this.subs
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify(info) {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // 实际调用的是 Watcher.update(),获取新的值以及更新依赖关系
      subs[i].update()
    }
  }
}

Dep类并不复杂,主要对外提供了几个方法来处理subs中保存的DepTarget实例,从Observe类可以看到depend是在get调用,进行依赖项收集,notify是在set里调用。这样发布订阅模型就具有了初步的雏形,已经有了订阅和发布的能力,但是还少了关键的一步,那就是发布订阅后的事件回调处理。

Watcher类源码解读

接下来看Watcher类是怎么来处理的,这才整个响应机制的核心所在,从中可以看到作者设计的巧妙,重点关注getaddDeprun这几个方法即可,其他几个方法主要是为computed属性和this.$watch设计的。

// Watcher类是对DepTarget类的实现
let uid = 0 n
class Watcher implements DepTarget {
  // 构造函数,进行初始化工作
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    
    // 每个vue组件实例,都会生成一个Watcher实例,并挂载在_watcher上
    if ((this.vm = vm)) {
      if (isRenderWatcher) {
        vm._watcher = this
      }
    }
    // 以下值都可以单独通过options可以配置,此处忽略该逻辑
    // user表示听过 this.$watch 添加的Watcher
    this.deep = this.user = this.lazy = this.sync = false
    this.cb = cb
    this.id = ++uid 
    this.active = true
    this.dirty = this.lazy 

    // 存放上一次取值时,收集到的依赖,当dep.notify触发时
    this.deps = []
    this.depIds = new Set()

    // 执行this.get(),取值操作时,所有新的依赖都会存放在这里
    this.newDeps = []
    this.newDepIds = new Set()
   
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 例如expOrFn值为'xxx.xxx'字符串,转换成 this.xxx.xxx 进行取值
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }

    // lazy = true 表示初始化时,不进行取值,用于 computed 属性
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
    // 当前对象this设置为依赖收集树的起点,自顶向下收集依赖
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } finally {
      // 进行深度依赖收集,这是一个递归函数
      // 递归对象value,是一个Observe实例对象
      if (this.deep) {
        traverse(value)
      }
      // 释放当前 Dep.target 
      popTarget()
      // 更新依赖关系,主要是保证dep.subs中,在下次data更新时,为对应需要更新的Watcher
      this.cleanupDeps()
    }
    return value
  }

  // Observe对象取值时,会触发对应空间下dep.depend(),进行依赖收集
  // 而depend()最终的调用是Dep.target.addDep()
  // 该方法的调用结果是在dep.subs中添加了this,即对应的Watcher实例
  addDep(dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // 将新的依赖关系缓存起来
      this.newDepIds.add(id)
      this.newDeps.push(dep)

      // 如果上一次的依赖关系中不存在这个dep,说明在dep.subs需要加上对应的Watcher
      // 同时cleanupDeps()会删除dep.subs中不存在newDeps的中的Watcher
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  // 更新时,最终调用的方法
  run() {
    if (this.active) {
      // 通过get方法,收集新的依赖关系,以用于下一次更新使用 
      const value = this.get()
      if (value !== this.value || isObject(value) ||this.deep) {
        // 新的值替换就得值
        const oldValue = this.value
        this.value = value
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
  // 新的依赖关系,覆盖原来的依赖关系,并清理dep.subs中的Watcher
  // ps:这里这么写是为了优化性能,避免重复创建空对象
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp: any = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  // 该方法只在计算属性中调用
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
  // 该方法也只在计算属性中调用,一般只有计算属性才会依赖多个deps
  // 例如:fullName = this.firstName + this.lastName 
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // 用于卸载依赖关系,一般用于this.$watch监听的卸载
  // 如果是data或者computed属性默认都是一直存在,一般无需卸载工作
  teardown() {
    if (this.active) {
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
      // 卸载完成之后的回调
      if (this.onStop) {
        this.onStop()
      }
    }
  }
}

从上述Watcher类的源码可以看出,Watcher类其实就是DepTarget类的实现,并对其进行完善,使其具有完整的事件回调处理能力。

我们接着看下面的代码

let mountComponent = ()=>{
   let updateComponent = () => {
      vm._update(vm._render(), hydrating)
   }
  new Watcher(vm, updateComponent, noop,watcherOptions,true)
}
Dep.target = null
const targetStack =  []
pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

先看第一段代码,这样有助于大家理解第二段代码,mountComponent就是$mount()调用的方法,执行过程中实例化了一个Watcher对象,这段代码的含义就是,在不使用计算属性的情况下,每一个Vue组件只有一个Watcher实例。这样Watcher就会和dom一样形成一个Watcher树的结构,因为他挂在在vm._watcher中,而vm本身就是树结构。

接着看第二段代码,这也就是作者设计巧妙的地方,他的作用就是在每次调用Watcher.get()收集依赖时,将当前的实例this设置为Dep.target,因为此时的依赖关系只会存在当前的Watcher节点下,而无需关心其他节点,处理完之后,再替换成其他节点的Watcher

示例

下面我们通过将observe Watcher Dep结合起来,模拟下在Vue中的效果,看函数调用的输出结果

class Vue {
  constructor(data) {
    this.data = data;
  }
  render() {
    console.log("this.age = " + this.data.age);
  }
  renderName() {
    console.log("this.name = " + this.data.name);
  }
}

let data = { name: "xiaomin", age: 20 };
observe(data);

let vm = new Vue(data);
let watcher = new Watcher(vm, function updateComponent() {
  this.render();
});
// 输出: this.age = 20

vm.data.age = 10;
// 输出: this.age = 10

vm.data.name = "xiaohua";
// 无输出
// 因为name在vm并没用被使用,所以不在$watcher的依赖项中

vm.renderName();
// 输出: this.name = xiaohua

vm.data.name = "xiaoxiao";
// 无输出
// 此时$watcher依然无data.name的依赖,因为需要执行$watcher.get()才能添加依赖

let $watcher = new Watcher(vm, "data.name", function cb(value, oldValue) {
  this.render();
  this.renderName();
});
data.name = "haha";
data.age = 50;
// 输出:  this.age = 10
//        this.name = haha

data.age = 40;
// 输出: this.age = 40 
// 注意:这是watcher中render()的输出结果

完整的输出结果就是:
this.age = 20
this.age = 10
this.name = xiaohua
this.age = 10
this.name = haha
this.age = 50
this.age = 40

思考:如果$watcher换成以下代码,data.name替换为data.age,输出结果还一样吗?

答案:不一样,具体原因大家可以自行思考

let $watcher = new Watcher(vm, "data.age", function cb(value, oldValue) {
  this.render();
  this.renderName();
});

替换后,完整的输出结果就是:
this.age = 20
this.age = 10
this.name = xiaohua
this.age = 50
this.age = 50
this.name = haha
this.age = 40
this.age = 40
this.name = haha

附完整代码

结尾

回到之前上篇中提到的疑问:

vue订阅发布模型为什么要使用观察者模式呢?可以用事件模型来实现吗?

假设vue以事件模型实现订阅和发布,那么每次注册事件都需要有定义eventName。这样就会带来一个问题,要怎么去定义eventName呢?有人可能会用说命名空间,比如:namespace.namespace.eventName,那同样的问题,怎么定义namespace呢?因为如果定义的不好的话就可能会存在同名事件,这样会引起错乱。再一个,对于不同的eventName之间要如何通信呢?这些都是棘手的问题。

所以作者设观察者模式,由于dom是一个树形的结构,每一层之间节点来进行隔离。这样就很好的解决了上述namespace的问题,一个节点可以看成是一个天然的namespace,同时Dep.subs可用于聚合不同节点的Watcher,这样就解决了不同的eventName之间通信的问题。

以上是个人拙见,错误的地方望大家指正。

完结

参考链接