Reactivity

September 08, 2017

最近有被问到 Vue 的响应式数据的原理,第一次被问到的时候有点语无伦次的感觉吧。对于一个知识点,看过,然后自己理解一下是一个层次,能够讲出来让别人明白就是另一个层次了。对于源码的阅读,我比较喜欢去明白一些机制的原理,比较理想的情况下是能够在明白源码的原理之后,在不查看源码的情况下能够实现类似的功能。今天我就尝试一下实现响应式数据,当然,在实现上比较粗略,以原理为主,在此之前,需要知道几个基本点。

首先是 Observer 类,在 Vue 中,通过 Observer 来给属性添加 settergetter,这是 Vue 实现响应式机制的关键。

第二点是 Watcher 类,Vue 的每一个组件都有一个 Watcher 实例,组件渲染的过程中会把相应属性记为依赖,之后当相应属性的 setter 被调用的时候,会通知 Watcher 实例进行计算,然后触发重新渲染。

第三点是 Dep,前面说到了 ObserverWatcher,还提到 Watcher 实例会在依赖属性的 setter 调用时候重新计算,那么 Watcher 实例是如何被通知的呢?这里就得说到 Dep 这个类了,正式通过 Dep 实例来通知相应的 Watcher 实例进行重新计算。

不多说,开始写代码。

首先实现 Observer

class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    const keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      let val = data[key]
      observe(val)
      Object.defineProperty(data, key, {
        get() {
          return val
        },
        set(newVal) {
          val = newVal
          console.log('receive new value', newVal)
        }
      })
    }
  }
}

function observe(data) {
  if (Object.prototype.toString.call(data) !== '[object Object]') {
    return
  }
  new Observer(data)
}

就这样,初步实现了一个 Observer 类,来测试一下我们能否知道属性被设置新值。

const data = {
  a: 'a',
  obj: {
    key1: 'key1',
    key2: 'key2',
  },
}

observe(data)

data.obj.key1 = 'key1_changed'
// console 输出:
// receive new value key1_changed

可以看到,通过设置对象属性的 settergetter,我们能够在设置属性或者获取属性值的时候做一些额外的事情。

接着我们定义一下 Watcher

class Watcher {
  constructor(data, exp, fn) {
    this.exp = exp
    this.fn = fn
    // ...more
  }
}

Watcher 的构造函数大概长这个样子,然后我们可以通过下面的方式来“观察”对象:

const data = {
  a: 'a',
  obj: {
    key1: 'key1',
    key2: 'key2',
  },
}

observe(data)

new Watcher(data, 'a', () => {
  console.log('data.a changed')
})

然后每次 data.a 被重新设置的时候,控制台会输出 data.a changed

那么具体来讲该怎么实现呢?直观来讲,我们会想到,在对应的 setter 属性里面加一些代码,当 setter 被调用的时候,调用对应 Watcher 实例的方法就可以了,那么,该怎么实现呢,这个时候就要请出连接 ObserverWatcher 的桥梁,Dep 了。

class Dep {
  constructor() {
    this.subs = []
  }

  addSub(watcher) {
    if (!this.subs.includes(watcher)) {
      this.subs.push(watcher)
    }
  }

  notify() {
    this.subs.forEach(sub => {
      sub.fn()
    })
  }
}

Dep 类就这样简单的实现了,思想就是将 Watcher 实例放到 subs 里面,当 Dep#notify 调用的时候,调用相应 Watcher 实例的 fn 函数。

不过,现在 Dep 还没能将 WatcherObserver 连接起来,接下来我们需要修改 WatcherObserver 函数。

首先给 Dep 加一个静态属性,Dep.target

Dep.target = null

接着修改 Wathcer

function pushTarget(watcher) {
  Dep.target = watcher
}

class Watcher {
  constructor(data, exp, fn) {
    this.exp = exp
    this.fn = fn
    pushTarget(this)
    data[exp]
  }
}

好的,这个时候有必要解释一下了,Dep.target 是一个 Watcher 实例,每当我们创建一个新的 Watcher 实例的时候,会通过 pushTarget 函数修改 Dep.target,将其设为这个新初始化的 Watcher 实例,接着,下面一行代码是 data[exp],咦,这行代码有什么用?这里我对问题先做一下简化,即 expdata 的一个 key(真正的实现不仅可以是 key,还可以是类似key1.key2 这样的形式,还可以是函数,但是从原理上来讲是一样的)。那么,data[exp] 其实就是调用响应属性的 getter!前面说过,通过 Observer 设置了对象属性的 settergetter,所以我们可以在 settergetter 里面做一些事情,而 data[exp] 所要做的就是收集依赖!

接着进一步修改 Observer

class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    const keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      let val = data[key]
      observe(val)
      const dep = new Dep()
      Object.defineProperty(data, key, {
        get() {
          // +++
          dep.addSub(Dep.target)
          // +++
          return val
        },
        set(newVal) {
          // +++
          if (newVal === val) {
            return
          }
          observe(newVal)
          dep.notify()
          val = newVal
          // +++
        }
      })
    }
  }
}

看,通过这种方式,对象的每一个属性都有一个 Dep 实例,其 Dep#subs 记录了依赖于这个属性的 watcher,每当这个属性被重新赋值的时候,会通过调用 Dep#notify 来通知 Watcher 该属性改变了。另外,当使用相同值重复设置属性的时候,不会去触发通知。

先贴一下所有代码:

class Dep {
  constructor() {
    this.subs = []
  }

  addSub(watcher) {
    if (!this.subs.includes(watcher)) {
      this.subs.push(watcher)
    }
  }

  notify() {
    this.subs.forEach(sub => {
      sub.fn()
    })
  }
}

Dep.target = null

function pushTarget(watcher) {
  Dep.target = watcher
}

class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    const keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      let val = data[key]
      observe(val)
      const dep = new Dep()
      Object.defineProperty(data, key, {
        get() {
          dep.addSub(Dep.target)
          return val
        },
        set(newVal) {
          if (newVal === val) {
            return
          }
          observe(newVal)
          dep.notify()
          val = newVal
        }
      })
    }
  }
}

function observe(data) {
  if (Object.prototype.toString.call(data) !== '[object Object]') {
    return
  }
  new Observer(data)
}

class Watcher {
  constructor(data, exp, fn) {
    this.exp = exp
    this.fn = fn
    pushTarget(this)
    data[exp]
  }
}

用刚才上面提到的代码测试一下:

const data = {
  a: 'a',
  obj: {
    key1: 'key1',
    key2: 'key2',
  },
}

observe(data)

new Watcher(data, 'a', () => {
  console.log('data.a changed')
})

data.a = 'a_change'
// 控制台输出:
// data.a changed

好了,以上就是在 Vue 中,响应式数据的一个大概原理了,当然,很多细节没有照顾到,而 Vue 的具体实现跟上面所说的也有所不同,Vue 的实现会复杂许多,比如上面的实现 Dep#notify 是直接调用 fn,但在 Vue 中,有一个 Wathcer#update 方法,Dep#notify 实际上调用的是这个方法,如在 Watcher 实例中设置的函数可以获取值的新值跟旧值,大家可以自行尝试实现,等等等等,大家可以参考 VueWatcher 具体的 API,我就不再一一赘述了,因为这篇文章着重讲的是一个实现原理。

# JavaScript
# 前端
知识共享许可协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
© 2015 - 2022 zhuscat
Hosted on Vercel