让我们聊聊Vue的响应式原理

53 阅读3分钟

聊的话题

  • 响应式原理在面试的常问问题
  • 实际项目中出问题的原理层面的解决
    1. 给 Vue 实例新增一个成员是否是响应式的?
    2. 给属性重新赋值成对象,是否是响应式的?
  • 为学习Vue源码做铺垫

准备工作

  • 数据驱动
  • 响应式的核心原理
  • 发布订阅模式和观察者模式

数据驱动

  • 数据响应式、双向绑定、数据驱动
  • 数据响应式
    1. 数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,试图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
  • 双向绑定
    1. 数据改变,视图改变;视图改变,数据也随之改变
    2. 我们可以使用 v-model 在表单元素上创建双向数据绑定
  • 数据驱动是 Vue 最独特的特性之一
    1. 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

数据响应式的核心原理

Vue 2.x

// 模拟 Vue 中的 data 选项 
let data = { msg: 'hello' } 
// 模拟 Vue 的实例 
let vm = {} 
// 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
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 = 'Hello World' 
 console.log(vm.msg)
  • 如果有一个对象中多个属性需要转换 getter/setter 如何处理?

Vue 3.x

  • MDN - Proxy
  • 直接监听对象,而非属性。
  • ES 6中新增,IE 不支持,性能由浏览器优化
// 模拟 Vue 中的 data 选项
let data = {
    msg: 'hello',
    count: 0
}

// 模拟 Vue 实例
let vm = new Proxy(data, {
    // 当访问 vm 的成员会执行
    get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
    },
    // 当设置 vm 的成员会执行
    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)

发布订阅模式和观察者模式

发布/订阅模式

  • 发布/订阅模式
    1. 订阅者
    2. 发布者
    3. 信号中心

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

let vm = new Vue()

vm.$on('dataChange', () => {
    console.log('dataChange')
})

vm.$on('dataChange', () => {
    console.log('dataChange1')
})

vm.$emit('dataChange')

  • 兄弟组件通信过程
// eventBus.js
// 事件中心
let eventHub = new Vue()

// ComponentA.vue
// 发布者
addTodo: function () {
    // 发布消息(事件)
    eventHub.$emit('add-todo', { text: this.newTodoText })
    this.newTodoText = ''
}
// ComponentB.vue
// 订阅者
created: function () {
    // 订阅消息(事件)
    eventHub.$on('add-todo', this.addTodo)
}

  • 模拟 Vue 自定义事件的实现
class EventEmitter {
    constructor () {
    // { eventType: [ handler1, handler2 ] }
    this.subs = {}
}

// 订阅通知
$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()
        })
     }
}

// 测试
var bus = new EventEmitter()

// 注册事件
bus.$on('click', function () {
    console.log('click')
})
bus.$on('click', function () {
    console.log('click1')
})

// 触发事件
bus.$emit('click')

观察者模式

  • 观察者(订阅者)--Watcher
    1. update(): 当事件发生时,具体要做的事情
  • 目标(发布者)-- Dep
    1. subs数组:存储所有的观察者
    2. addSub():添加观察者
    3. notify():当事件发生时,调用所以观察者的Update()方法
  • 没有事件中心
// 目标(发布者)
// Dependency
class Dep {
    constructor () {
        // 存储所有的观察者
        this.subs = []
    }
    // 添加观察者
    addSub (sub) {
        if (sub && sub.update) {
        this.subs.push(sub)
        }
    }
    // 通知所有观察者
    notify () {
        this.subs.forEach(sub => {
        sub.update()
        })
    }
}

// 观察者(订阅者)
class Watcher {
    update () {
    console.log('update')
    }
}

// 测试
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()

总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。

  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

  • 未完待续 希望各位读者一键三连,我将有更多作品呈现给你们