美文网首页饥人谷技术博客
Vue响应式的基本原理 - 实现一个Mini Observer

Vue响应式的基本原理 - 实现一个Mini Observer

作者: 饥人谷_刘氏熏肉大饼 | 来源:发表于2018-10-23 22:09 被阅读101次

今天看了一个尤大神介绍Vue中实现响应式基本原理的视频, 把视频内容总结一下。

首先是问题的提出: 什么响应式(Reactivity) ?


举一个简单的例子

let a = 2
let b = 3
let c = a * b
console.log(c) // 6
a = 3
console.log(c) // 6 or 9 ?

从上面的代码可以看出,c的值显而易见仍然还是6, 因为JS是顺序执行的。当a变成3之后,c = a * b 并没有再次执行, 当然c的值也就不会发生变化。而所谓的响应式,就是希望c的值能够根据a, b的变化而自动变化,这就很像我们在Excel表格中做计算一样, 不管哪一个变量发生变化, 结果都会自动更新。

用公式总结一下
正常情况下, 结果就是依赖项的函数运算值, 这个函数只运行一次就结束了:
结果 = fx(依赖项1, 依赖项2, 依赖项3,...)

而所谓响应式的结果就是时刻观察着这些依赖项,无论哪一个发生变化, 都重新运行一次函数,从而的得到更新后的值:
响应式的结果 = watch(fx(依赖项1, 依赖项2, 依赖项3,...))

Vue中实现响应式的利器: Object.defineProperty API


有关这个API的具体用法, 这里就不做详细介绍了, 大家可以参看MDN。这里只给出一个简单的例子, 说明一下Object.defineProperty API 的 get / set方法, 相信大家应该都看的懂。

const convert = (obj) => {
  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]
    Object.defineProperty(obj, key, {
      get () {
        console.log(`Get value ${internalValue}`)
        return internalValue
      },
      set (newValue) {
         internalValue = newValue
         console.log(`Set value ${internalValue}`)
      }
    })
  })
}

let test = {
  'a': 2
}

convert(test)
test.a = 3 //  'Set value 3'
test.a // 'Get value 3'

// get () 方法在读取对象属性时被调用
// set () 方法在设置对象属性时被调用

实现响应式基本思路: 如何在依赖项发生变化时, 重新触发计算

无论在什么样的计算中,我们总是会有多个依赖项,那么我们首先把依赖项抽象成一个类,每个依赖项都是这个类的实例。
每个实例需要有两个方法,分别是depend() 和 notify()。depend()的作用是将该依赖项所在的所有函数记录下来, notify()的作用是在该依赖项变化时, 将之前存储的函数全部运行一次。
说到这里可能有点抽象, 还是用代码解释一下:

let dep = new Dependency()

watch(() => {
  dep.depend()
  console.log('运行一次计算')
}) // 打印:'运行一次计算'

dep.notify() // 打印:'运行一次计算'

我们首先创建了一个依赖项实例dep,而后我们监听一个函数运算, 这是一个箭头函数。dep是它的依赖项,或者从数学角度理解,dep就是这个函数的一个变量。当这个函数运行时,它做了两件事,一个运行dep的depend()方法, 此时我们希望将这个箭头函数存储在某个地方,以便日后使用,而后打印出'运行一次计算‘。

() => {
  dep.depend()
  console.log('运行一次计算')
}

接下来,运行dep的notify()方法,我们希望刚才存储起来的箭头函数运行一次, 进而又打印出'运行一次计算‘。
这些其实就是实现响应式的大致思路,接下来我们开始用代码具体实现一下:

  let global = null // 用来临时存储函数的全局变量

  // 实现 watch 方法
  const watch = (func) => {
    global = func // 将这个函数临时存储在全局变量中
    func() // 运行watch的时候, 首先运行一次函数
    global = null // 清空全局变量
  }
  // 注意: 这个global并不是最终存储函数的地方, 因为一个依赖项可能用在多个函数中
  // 我们真正要实现的是将这些函数和这个依赖项关联起来, 那么很容易想到, 将这些函数存在这个依赖项实例中
  // 因此我们可以这样定义Dependency类
 class Dependency {
    constructor () {
      this.subscribers = new Set() // 用于存储所有相关的func
    }
    depend () {
      if (global) {
        this.subscribers.add(global) // 添加func
      }
    }
    notify () {
      this.subscribers.forEach(func => func()) // 运行一遍所有func
    }
  }

此时我们已经大体实现了响应式,因为只要运行dep.notify,就会打印出’运行一次计算‘
接下来的问题是,在上面的代码中,我们是通过手动触发depend()和notify()实现的响应式。这显然离我们的要求还差一点, 我们希望可以自动触发depend()和notify(), 一个典型的使用场景如下:


// 一个名为state的依赖项
let state = {
  'a': 1,
  'b': 2
}

let sum

// 监听求和运算
watch(() => {
  sum = state.a + state.b // 此处应该触发了depend()
})

console.log(sum) // 3
state.a = 4 // 此处应该触发了notify()
console.log(sum) // 6
state.b = 4 // 此处应该触发了notify()
console.log(sum) //8

该怎么实现自动触发depend() 和 notify()呢? 这里我们就想到了前面提到过的Object.defineProperty() API。思路就是: 当读取state.a或者state.b时,在get() 方法中触发depend(); 当给state.a 或 state.b 赋值时,在set()中触发notify()。 由此, 我们将前文提到的convert()函数修改一下:

// 创建一个dep实例与obj绑定
const convert = (obj) => {
  let dep = new Dependency()
  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]
    Object.defineProperty(obj, key, {
      get () {
        dep.depend(global)
        return internalValue
      },
      set (newValue) {
         internalValue = newValue
         dep.notify()
      }
    })
  })
}

完整的示例如下:

let global = null // 用来临时存储函数的全局变量

const convert = (obj) => {
  let dep = new Dependency()
  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]
    Object.defineProperty(obj, key, {
      get () {
        dep.depend(global)
        return internalValue
      },
      set (newValue) {
         internalValue = newValue
         dep.notify()
      }
    })
  })
}

const watch = (func) => {
  global = func // 将这个函数临时存储在全局变量中
  func() // 运行watch的时候, 首先运行一次函数
  global = null // 清空全局变量
}

class Dependency {
  constructor () {
    this.subscribers = new Set()
  }
  depend () {
    if (global) {
      this.subscribers.add(global)
    }
  }
  notify () {
    this.subscribers.forEach(func => func())
  }
}

// 响应式测试
let state = {
  'a': 1,
  'b': 2
}

convert(state) // 重新定义state的get / set 方法

let sum
watch(() => {
  sum = state.a + state.b
})

console.log(sum) // 3
state.a = 4
console.log(sum) // 6
state.b = 4
console.log(sum) //8

我又对完整示例中的方法和类做了一下简单的处理, 全部都封装在一个名为Vue的类中, 最终效果如下:

class Vue {
  constructor () {
    this.global = null
    this.Dep = class Dependency {
      constructor () {
        this.subscribers = new Set()
      }
      depend (global) {
        if (global) {
          this.subscribers.add(global)
        }
      }
      notify () {
        this.subscribers.forEach(sub => sub())
      }
    }
  }

  convert (obj) {
    let dep = new this.Dep()
    Object.keys(obj).forEach(key => {
      let _this = this
      let internalValue = obj[key]
      Object.defineProperty(obj, key, {
        get () {
          dep.depend(_this.global)
          return internalValue
        },
        set (value) {
          internalValue = value
          dep.notify()
        }
      })
    })
  }
  
  watch (update) {
    this.global = update
    update()
    this.global = null
  }
}

// 响应式测试
let A = {
  'a': 1,
}

let B = {
  'b': 1,
} 

let vue = new Vue()
vue.convert(A)
vue.convert(B)

let sum
let minus

vue.watch(() => {
  sum = A.a + B.b
})

vue.watch(() => {
  minus = A.a - B.b
})

console.log(sum, minus) // 2, 0
A.a = 10
console.log(sum, minus) // 11, 9
B.b = 20
console.log(sum, minus) // 30, -10

到此为止有关Vue响应式的基本原理就介绍完了。当然这些与Vue中的真正实现方法还有很大不同,但是希望能帮助大家对Vue响应式的基本实现有个宏观的认识。

相关文章

  • Vue源码03-响应式原理

    这节将专门讲解vue MVVM响应式处理 Vue采用的是 实现数据的响应式 数据劫持 Observer类 Obse...

  • Vue响应式的基本原理 - 实现一个Mini Observer

    今天看了一个尤大神介绍Vue中实现响应式基本原理的视频, 把视频内容总结一下。 首先是问题的提出: 什么响应式(R...

  • Vue原理学习(二)

    响应式系统的基本原理 Vue基于Object.defineProperty来实现响应式,对于Object.defi...

  • vue2数据响应式原理

    vue2响应式原理由Observer类,Dep类和Watcher类互相调用实现,Observer类是把一个普通的o...

  • Vue响应式原理Observer、Dep、Watcher理解

    Vue响应式原理Observer、Dep、Watcher理解 Observer Observer是用来给数据添加D...

  • Vue的34道题

    1、如何理解MVVM原理? MVVM的实现原理 2、响应式数据的原理是什么? 响应式数据与数据依赖基本原理vue双...

  • Vue的响应式浅析

    1 Vue如何实现响应式? Vue的响应式是建立在监听data中的数据. 2 在Vue2中响应式的实现 Vue通过...

  • Vue响应式基本原理

    Vue响应式系统基本原理 Object.defineProperty Object.defineProperty(...

  • 【Vue3.0】- 响应式

    响应式原理 响应式是 Vue.js 组件化更新渲染的一个核心机制 Vue2.x响应式实现 Object.defin...

  • vue2响应式原理总结

    响应式原理 Observer Dep Watcher 遍历整个组件树 vue组件实例化时,会对data属性深度遍历...

网友评论

    本文标题:Vue响应式的基本原理 - 实现一个Mini Observer

    本文链接:https://www.haomeiwen.com/subject/kmtzzftx.html