『 深入 VUE 』响应

1,092 阅读2分钟
原文链接: alloween.top

简单的响应

给定 a 和 b 两个参数,要求无论 a 如何变化,b 永远等于 a 的 10 倍,如何实现?

即如下效果:

let a = 3
let b = a * 10
console.log(b) // 30
a = 4
b = a * 10
console.log(b) // 40

在每次改变 a 的值之后手动更新 b 的值,这是山顶洞人的做法,我们想要的是 a 的值改变之后,自动执行 b 的改变函数。

我们可以维护一个 state 对象:

state = {
  a: 10,
  b: 100
}

然后每次 a 发生变化时执行:

onStateChanged(() => {
    state.b = 10 * state.a
})

要实现这个很简单,我们可以每次改变 a 的值从原来的直接赋值变为调用一个函数:

setState({ a: 5})

setState 这个函数里做赋值操作,同时执行 onStateChanged 函数使 b 的值自动得到更新。

完整的实现如下:

let update
let state = {}
const onStateChanged = _update => {
    update = _update
}

const setState = newState => {
    state = newState
    update()
}

onStateChanged(() => {
    state.b = 10 * state.a
})

state = {
  a: 10,
  b: 100
}

setState({ a: 5})

console.log(state.b) // 50

setState({ a: 50})

console.log(state.b) // 500

严格来说应该把 stateupdateonStateChangedsetState 封装起来,当初始化值之后,只允许通过 setState 来改变变量的值。这其实已经有点像 react 了。

使用 Object.defineProperty

这里用到一个对象方法 Object.defineProperty,目的是将 data 对象里的所有属性通过这个方法来重新定义,重写属性的修改(setter)方法。

// 初始化 data 对象
const data = {
    a: 10,
    b: 100
}

// 将属性 a 的值临时存放
let value = data.a

// 重写 data.a 的方法
Object.defineProperty(data, 'a', {
  get() {
    return value
  },
  set(val) {
    value = val
    data.b = 10 * val     // 这样一来每次对 a 重新赋值时便会自动更新 b 的值
  }
})



console.log(data.b) // 100

data.a = 3

console.log(data.b) // 30

vue 的响应

使用 Object.defineProperty 方法已经和 vue 的实现有点接近了,vue 实例是将 data 对象里的所有属性通过这个方法来重新定义,实现属性被访问(getter)和修改(setter)时通知(Notify)变化(Watcher 调用函数)。

即实现如下功能:

const data = {
  a: 0
}

// observe 函数将 data 的属性重写
observe(state)

// triggerFunction 接受一个函数作为参数,当 data 中属性值改变时自动调用 triggerFunction 里的函数
triggerFunction(() => {
  console.log(state.a)
})

// 0

state.count++
// 1

具体的实现:

// 在全局定义一个 Watch 类
window.Watcher = class Watcher {
  constructor () {
    // 订阅所有 Watch 搜集到的属性
    this.subscribers = new Set()
  }

  depend () {
    if (activeUpdate) {
      // 将属性自动更新方法添加到订阅列表中
      this.subscribers.add(activeUpdate)
    }
  }

  notify () {
    // 当属性被重新赋值时触发 setter 函数通知 Watch 做出改变
    this.subscribers.forEach(subscriber => subscriber())
  }
}

let activeUpdate

function observe (obj) {
  if (!isObject(obj)) {
    throw new TypeError()
  }

  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]
    let watcher = new Watcher()
    Object.defineProperty(obj, key, {
      get () {
        watcher.depend()
        return internalValue
      },
      set (newValue) {
        const isChanged = internalValue !== newValue
        if (isChanged) {
          internalValue = newValue
          watcher.notify()
        }
      }
    })
  })
}

function triggerFunction (update) {
  function wrappedUpdate () {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }
  wrappedUpdate()
}

上面的 triggerFunction 只是简单地 console.loga 的值,其实可以进一步地实现 re-render 函数,使得页面的 html 元素也自动重新渲染,便是完整的 vue 响应了。