实现一个简单的vue-响应式模拟(一)

974 阅读2分钟

前言

最近在研究Vue的源码,探索一下Vue的响应式原理。研究它是怎么实现的。Vue2.0是用 Object.defineProperty 去实现的, Vue3.0是用Proxy 去实现的。然后再结合观察者模式去实现数据跟视图的更新。本节先分享一下基础原理,下一节分享一下具体的实现。

数据响应式的核心原理

Vue2.x

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue2.0响应式原理</title>
</head>
<body>
<div id="app">hello</div>

<script>
  // 绑定单个
  let data = {
    msg: 'hello word',
    count: 10
  }
  let vm = {}
  Object.defineProperty(vm, 'msg', {
    configurable: true, // 是否能被改变,是否能被delete删除
    enumerable: true, // 是否可枚举,遍历
    get() {
      console.log('get:' + data.msg)
      return data.msg
    },
    set(newVal) {
      if(data.msg === newVal) return
      data.msg = newVal
      console.log('set:' + data.msg)
      document.getElementById('app').innerText = data.msg
    }
  })
  console.log(vm.msg)
  vm.msg = 'xxx'

  console.log('===================')
  // 绑定多个

  let data2 = {
    msg: 'hello word',
    count: 10,
    obj: {
      name: '你好啊'
    },
    arr: [1, 2, 3]
  }

  let vm2 = {
    data: data2
  }

  function defineProperty(obj, key, val) {
    // 如果value是对象,则继续对他下级成员进行响应式监听
    observer(val)
    Object.defineProperty(obj, key, {
      configurable: true, // 是否能被改变,是否能被delete删除
      enumerable: true, // 是否可枚举,遍历
      get() {
        console.log('get:' + val)
        return val
      },
      set(newVal) {
        if(val === newVal) return
        // 如果新设置的值是对象,则继续对他下级成员进行响应式监听
        observer(newVal)
        val = newVal
        console.log('set:' + key + ':' + val)
      }
    })
  }
  function observer(data) {
    // 如果不是对象,则返回
    if(!data || typeof data != 'object' ) return
    Object.keys(data).forEach(key => {
      defineProperty(data, key, data[key])
    })
  }
  observer(vm2.data)
  console.log(vm2)
</script>
</body>
</html>

Vue3.x

  • MDN-Proxy
  • 直接监听对象,而不是属性
  • IE不支持,性能由浏览器优化
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3.0响应式原理</title>
</head>
<body>
  <div id="app">hello</div>
  <script>
    let data = {
      msg: 'hello word',
      count: 10,
      obj: {
        name: '你好啊'
      },
      arr: [1, 2, 3]
    }

    let vm = new Proxy(data, {
      get(target, key) {
        console.log('get', key, target[key])
        return target[key]
      },
      set(target, key, newVal) {
        if(target[key] == newVal) return
        console.log('set', key, newVal)
        target[key] = newVal
      }
    })

    console.log(vm.msg)
  </script>
</body>
</html>

发布订阅模式

发布订阅模式由三者组成:发布者、订阅者、信号中心。我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)

  • 我们先看下Vue的自定义事件$on和$emit
let vm = new Vue()
vm.$on('dataChange', () => {
 console.log('dataChange')
})
vm.$on('dataChange', () => {
 console.log('dataChange1')
})
vm.$emit('dataChange')
  • 自己实现一个发布订阅模式的思路
    • 定义一个类,初始化的时候,定义一个对象subs,用来存储各种事件,以及订阅这个事件的数组对象
    • 创建一个订阅方法,把订阅的事件和对象存储到subs中,如果没有这个事件,则创建一个,有的话,则添加到订阅的数组对象中
    • 创建一个发布方法,把发布事件,所订阅的所有对象,都通知一遍
class EventEmitter {
  constructor() {
    // 存储所有事件对象
    this.subs = {}
  }

  $on(eventType, fn) { // 订阅
    this.subs[eventType] = this.subs[eventType] || []
    this.subs[eventType].push(fn)
  }

  $emit(eventType, ...params) { // 发布
    if(this.subs[eventType]) {
      this.subs[eventType].forEach(fn => {
        fn(...params)
      })
    }
  }
}

let eve = new EventEmitter()

eve.$on('click', function(data, two) {
  console.log('click1', data, two)
})

eve.$on('click', function(data) {
  console.log('click2', data)
})

eve.$on('change', function() {
  console.log('change')
})

eve.$emit('click', 1, 2)
eve.$emit('change')

观察者模式

  • 观察者 -- Watcher
    • update() 这里处理当事件发生时,所要做的事情
  • 目标 -- Dep
    • subs 存储所有观察者的数组
    • addSub() 添加观察者的方法,参数是观察者对象
    • notify() 循环subs,通知所有观察者,调用观察者的update方法
class Dep {
  constructor() {
    this.subs = []
  }

  addSub(sub) {
    if(sub && sub.update) {
      this.subs.push(sub)
    }
  }

  notify() {
    if(!this.subs.length) return
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

class Watcher {
  update() {
    console.log('update')
  }
}

let dep = new Dep()
let watch = new Watcher()
dep.addSub(watch)
dep.notify()

总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

实现一个简单的vue-响应式模拟(二)