前言
最近在研究Vue的源码,探索一下Vue的响应式原理。研究它是怎么实现的。Vue2.0是用 Object.defineProperty 去实现的, Vue3.0是用Proxy 去实现的。然后再结合观察者模式去实现数据跟视图的更新。本节先分享一下基础原理,下一节分享一下具体的实现。
数据响应式的核心原理
Vue2.x
- Vue2.x深入响应式原理
- MDN-Object.defineProperty
- 兼容IE8以上
<!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 = newValconsole.log('set:' + key + ':' + val) } }) } function observer(data) {// 如果不是对象,则返回if(!data || typeof data != 'object' ) returnObject.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) returnconsole.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) returnthis.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-响应式模拟(二)