模拟Vue响应式原理
重点回顾:
数据驱动:数据响应式、双向绑定、数据驱动
数据响应式:数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作( Vue 内部封装了复杂的 DOM 操作),提高开发效率;
双向绑定:包含数据响应式,数据改变,视图改变,反之亦然;Vue 中我们可以使用 v-module 在表单元素上创建数据双向绑定;
数据驱动是 Vue 最独特的特性之一:开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图;
数据响应式的核心原理:
Vue 2.x:当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
// DOM
<div id="app">
hello
</div>
// defineProperty 如何实现vue的数据双向绑定
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello'
}
// 模拟 Vue 的实例
let vm = {}
// 数据劫持(劫持 vm 对象的单个成员):当访问或者设置 vm 中的成员的时候,做一些干预操作
// defineProperty 有三个参数:param1:对象;param2:为vm对象添加的属性;param3:属性描述符
Object.defineProperty(vm, 'msg', {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
configurable: true,
// 访问器:当获取值的时候执行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 设置器:当设置值的时候执行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
data.msg = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data.msg
}
})
// 测试:当 vm.msg 改变时,视图中的数据随之改变
vm.msg = 'Hello World'
// defineProperty 劫持 data 内部多个成员
// 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
// 把 data 中的属性,转换成 vm 的 setter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('get: ', key, data[key])
return data[key]
},
set (newValue) {
console.log('set: ', key, newValue)
if (newValue === data[key]) {
return
}
data[key] = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data[key]
}
})
})
Vue 3.x:直接监听对象,而非属性。 ES6 中新增,IE 不支持,性能由浏览器优化
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 0
}
// 模拟 Vue 实例
// Proxy 代理整个对象,它是构造函数,接收两个参数
// param1:代理的对象data
// param2:对象,成员为执行代理行为的函数
let vm = new Proxy(data, {
// 执行代理行为的函数
// 当访问 vm 的成员会执行
// target 代理目标对象 ;key 代理属性
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 当设置 vm 的成员会执行
// newValue 新的值
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
总结:vue2.x 中的响应式原理核心是object.definproperty(),数据劫持实例对象的属性,遍历实例对象内部属性,利用get与set方法,当数据发生变化,更新 data 内部数据与 DOM;vue3.x 中更新了数据响应式原理的核心方法,使用了Proxy代理对象,监听整个对象,无需遍历,简化代码,内部同样使用get与set方法,但是不支持 IE 浏览器。
参考链接:https://cn.vuejs.org/v2/guide/reactivity.html
发布订阅模式和观察者模式:
发布/订阅模式:
订阅者
发布者
信号中心
vue的自定义事件:$on $emit
// 自定义事件
const vm = new Vue()
// 注册事件(可定义多个) => 订阅消息
// $on 接收两个参数:事件名称 事件处理函数
vm.$on('eventHandle', function () {
console.log('事件处理函数1');
})
vm.$on('eventHandle', function () {
console.log('事件处理函数2');
})
// 触发事件 => 发布消息
vm.$emit('eventHandle')
兄弟组件的通信过程:发布订阅模式
// 事件中心
const eventHub = new Vue()
// ComponentA: 发布者
addTodo:function(){
// 发布消息(调用事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB: 订阅者
created: function(){
// 订阅消息(注册事件)
eventHub.$on('add-todo',this.addTodo)
}
模拟 Vue 自定义事件的实现:此处简单实现,忽略传参的情况
// 事件触发器
class EventHandle {
constructor(){
// 定义一个空对象,以键值对的形式存储注册的事件名称与事件处理函数,Object.create(null) 创建对象不会创建对象的原型,提升性能
this.subs = Object.create(null)
}
// 注册事件
$on(eventType, handler){
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 触发事件
$emit(eventType){
if(this.subs[eventType]) this.subs[eventType].forEach(handler => {handler()})
}
}
// 测试
const eh = new EventHandle()
eh.$on('click',() => {
console.log('自定义事件处理函数1');
})
eh.$on('click',() => {
console.log('自定义事件处理函数2');
})
eh.$emit('click')
观察者模式:
与发布订阅模式的区别是没有事件中心,只有发布者与订阅者,并且发布者要知道订阅者的存在;观察者模式中,订阅者又叫做观察者,所有的订阅者自身都有一个 update 方法(当事件发生的时候,具体要做的事情,当事件发生的时候,会调用所有的 update 方法),在 Vue 的响应式机制中,当数据变化时,会调用观察者的 update 方法,update 方法去更新视图;观察者模式中订阅者的 update 方法是由发布者调用,发布者又叫做目标,发布者内部记录所有的订阅者,当事件发生时,是由发布者去通知所有的订阅者
实现简单地观察者模式,忽略传参的情况:
// 模拟实现观察者模式
// 订阅者-观察者
class Watcher {
update () {
console.log('update方法执行了');
}
}
// 发布者-目标
class Dep {
constructor(){
// 记录所有的订阅者
this.subs = []
}
// 添加订阅者
addSub (watcher) {
if(watcher && watcher.update){
this.subs.push(watcher)
}
}
// 发布通知
notify () {
this.subs.forEach(subs => {
subs.update()
})
}
}
// 测试
const dep = new Dep()
const watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
总结: