Skip to content
🔬The reactive render implementation including dependency collection and rendered by vdom. 响应式更新结合 vdom 渲染
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
dist
docs
mindmap
samples
src
.gitignore
CHANGELOG.md
README.md
package.json
tsconfig.json
webpack.config.js
yarn.lock

README.md

剖析 Vue.js 响应式原理以及依赖收集

👉在线示例

本仓库用于展示现代 MVVM 框架 Vue.js 的 响应式原理(依赖收集)的实现。

下文解析一并结合了 publish/subscribe pattern 的相关设计模式知识(查看更对我对于设计模式的理解)。

案例已实现的核心功能

  • 响应式渲染 ✔️

  • 由 vdom 执行 DOM 渲染和更新 ✔️

简要概括 MVVM 的响应式原理(数据双向绑定)?

  • 以数据监听(即下文经由 publish/subscribe pattern 所实现的逻辑)为基础,通过数据驱动的方式来渲染 DOM。

  • 以 DOM 监听为基础(可通过 MutationObserver 来实现),通过对 DOM 变化的监听来响应式执行数据层的更新。

mind map

Watcher - 订阅者

Watcher 实例的 作用 在于它的 update 方法调用时,将调用 render 函数,以开启 DOM 更新队列。

⚠️ 重要:通过控制调用 update 方法,即可控制是否开启 DOM 更新队列。

在 Vue.js 的 mount 方法中,将实例化 Watcher。

class Watcher {
  constructor (vm, fn) {
    this.vm = vm
    this.render = fn

    Dep.target = this
  }

  // 用于传递当前 Watcher 实例至 subs 中
  addDep (dep) {
    dep.addSub(this)
  }

  update () {
    // 在实例化 Watcher 时传入 _render 函数,并成为 this.render 的引用,用于 DOM 更新
    this.render.call(this.vm)

    callHook('updated')
  }
}

在一个 Vue 实例中,只存在 data 选项时, 全局将只对应一个 Watcher 实例,后续向 data 对象的属性的 subs 容器中添加的 Watcher 实例,都是添加的对该 Watcher 实例的引用。

那么,宏观上来看,各个 subs 容器中存储了调用开启更新 DOM 队列的 trigger(即该 Watcher 实例)。

注:此处并未验证在存在 computed,watch 等选项情况下,全局 Watcher 实例情况,该处只验证了只存在 data 选项时,全局将只存在 唯一一个 Watcher 实例。

依赖收集解析 - 发布者

⚠️ 重要:区分需要订阅 Watcher 实例的对象,即收集依赖的目的是在值更新时,决定谁可以调用更新 DOM 的方法。

在 Vue.js 中,数据的依赖的收集和更新主要是依赖 Dep 的原型方法 depend 和 notify。

dep.depend()

在 data 对象的某个属性被调用时(即 getter 函数被调用),该方法将被调用。用于向当前值的 subs 容器传入 Watcher 实例的引用。

特别地,在 mount 函数中实例化 Watcher 时,将指定全局的 Dep.target 为当前 Watcher 实例。

dep.notify()

在复写 data 对象某属性 A 时(即 setter 函数被调用),该方法将被调用,以通知当前 A 的专有 subs 容器中所有的 Watcher 实例调用 update 方法。

dep.subs - 主题/事件通道

每一个被重写 getter 和 setter 的 data 对象 都将被赋予一个 topic/event channel,该通道的主要作用是为了防止在触发当前 data 对象 的相关订阅者时同样触发其他无关 data 对象 的订阅者。实现的方式即是通过一个 dep 实例的 subs 数组来存储所有该 data 对象 的相关订阅者。那么在该 data 对象 发布更新消息时,将通过 subs 数组(主题/事件通道)来实现只向有关的订阅者发布,这样就排除了向没有订阅该 data 对象 的订阅者。

实现

const defineReactive = (obj, key) => {
  // dep 通过闭包的形式与每个 data 对象相关联,因 getter 和 setter 中均保持了对 dep
  // 的引用
  const dep = new Dep()
  // ...
}

class Dep {
  constructor () {
    // 即主题/事件通道(topic/event channel)
    // data 对象的某属性的专有的 subs 容器,其中只包含对当前 data 对象感兴趣的订阅者,
    // 即 Watcher 实例
    this.subs = []
  }

  // 构建主题/事件通道(topic/event channel)
  addSub (watcher) {
    this.subs.push(watcher)
  }

  // 通过主题/事件通道收集依赖
  depend () {
    // Dep.target 指向目标订阅者,将订阅者加入主题/事件通道
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 发布更新消息,因存在主题/事件通道,故只向相关订阅者发布
  notify () {
    callHook('beforeUpdate')

    // 因为在触发 mvvm 渲染函数将触发 data 对象的 getter ,那么将触发 dep.append(),
    // 那么将会拓展 this.subs 数组,所以固定 subs 数组,防止无限循环
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i++) {
      subs[i].update()
    }
  }
}
// 添加 Dep 的静态属性
Dep.target = null

在初始化 Vue 时,data 对象中的每个属性都存在自己专属的 subs 容器,它将用于存储 Watcher 实例的引用(可理解为 trigger),在更新该值时,Watcher 实例将被调用 update 方法以触发 DOM 更新(调用 render 函数)。

选择性订阅消息(触发 Watcher)

讨论结合发布订阅模式,区分未参与渲染的数据对象,并阻止任何订阅者订阅该对象的更新消息。

在 Vue.js 中,为了防止未参与渲染 DOM 的 data 对象的属性 B 在值改变时触发 Watcher ,即触发无意义的 DOM 更新(因为它没有参与渲染),那么将 Dep.target 重置为 null,此时,未参与渲染的 B 将 不应该存在 相关订阅者(即不会添加当前 Vue 实例 data 的 Watcher 引用至 B 的 subs 容器中)。那么 B 在值改变时,将没有订阅者接受更新消息(即没有 Watcher 可被触发),也就不会触发 DOM 更新。

⚠️ 实现逻辑:为了控制是否调用 Watcher 实例的 update 方法,需要在 dep.depend 被调用之前实现 区分 哪些 data 对象的属性参与了 DOM 渲染。后续将 只有参与渲染 的 data 对象才能订阅更新消息(即该对象的 subs 容器中添加 Watcher 实例引用)。那么,对于没有参与渲染的 data 对象的属性 B,当 B 在值被更新时,B 的 subs 中为空,它将不会触发 Watcher 实例,也就阻止了无意义的 DOM 更新。

案例中实现以上内容的逻辑是,在执行到 dep.depend 之前(此函数一定会被调用,因为它在 B 的 getter 函数中)将 Dep.target 重置为 null。

响应式原理解析

响应式原理关键在于复写 data 对象各个属性的 getter 和 setter ,并在其中加入触发 添加依赖 和 更新依赖 的方法。其中依赖将触发存储在 subs 容器中的 Watcher 实例,之后将触发 DOM 更新。

在 Vue 构造函数中,初始化传入的 data 对象的每个属性:

const defineReactive = (obj, key) => {
  // 该处实例化 Dep 用于调用 dep.depend 和 dep.notify() 方法,并且实例化后每个
  // data 对象的属性将存在 subs 用于存入 Watcher 实例的容器。
  const dep = new Dep()

  let value = obj[key]

  Reflect.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get () {
      dep.depend()
      return value
    },
    set (newValue) {
      if (newValue === value) return
      value = newValue

      // 发布消息,通知订阅者,继而触发 vdom 更新
      dep.notify()
    }
  })
}

DOM 高效更新

DOM 的高效渲染借助 vdom 来实现,高效 体现在:

  • vdom 实现了对 DOM 树映射(其中的 elm 属性 形成了对节点的引用),避免了节点查找(more detail)。

  • vdom 经由 diff 算法得到需要更新的节点,使得需要更新重绘的 区域最小。该对比过程是纯 JS 运算,事实上该过程的效率要远远高于直接通过 DOM 查找并更新的效率。

渲染函数是由订阅者(Watcher实例)调用,并非人工干预调用 DOM 相关操作,即是响应式的体现。借助 publish/subscribe pattern(我的博客) 实现了数据层与表现层的 同步更新。

MVVM 框架的数据层与表现层的 同步更新 这一特点也是它与传统前端框架的不同,这也是我为什么使用现代 MVVM 框架的原因。

You can’t perform that action at this time.