• Home
  • Archives
  Evan的博客
  • Home
  • Archives
  • 面试
  • 原理笔记
  • 项目实践
  • 其他

Vue 响应式原理

2020/10/09 posted in  面试

Vue 响应式基本是面试必问的内容,但很多同学对响应式的理解仍仅仅停留在 Object.definedProperty 的阶段,一旦面试官稍微问深一点点,就答不出个所以然了。实际上 Vue 的响应式远不止这个简单的数据劫持而已。这篇文章会由浅入深,夹带部分源码,来介绍整个响应式的过程。看完这篇文章,不但对各位看官的面试有帮助,也能帮助你了解发布订阅的设计模式。

基础概念

Vue 的响应式也叫双向绑定,是 MVVM 中的核心,也就是视图 View 和模型 Model 的双向通信过程。Vue 实现双向通信,依赖的基础是数据劫持 + 发布订阅

Model 的变更决定了 View 的渲染,View 的用户交互反向作用于 Model 的变更

很多人对Vue响应式的理解只停留在数据劫持的阶段。其实要实现一个完整的双向绑定框架,除了对数据的劫持之外,还需要一个可靠的发布订阅机制

数据劫持

所谓数据劫持,就是在读取 / 写入数据的时候触发一个钩子,在这个钩子中注入自己的逻辑,以达到一些特定的目的

Vue 2.x 用的是 Object.definedProperty 来劫持数据,Vue 3.x 用的是 Proxy 来劫持数据

// Object.definedProperty 数据劫持
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get() {
    console.log("getter") // 读取属性时劫持数据
    return val
  },
  set(newV) {
    console.log("setter") // 设置属性时劫持数据
    val = newV
  }
})

// Proxy 劫持数据
new Proxy(obj, {
  get(obj, key) {
    console.log('getter')
    return obj[key]
  },
  set(obj, key, newV) {
    console.log('setter')
    obj[key] = newV
  }
})

发布订阅

发布订阅是一种比较常见的设计模式。订阅者向发布者订阅某条消息,当消息发生变更时(或者满足某个条件 / 时机时),发布者向订阅者推送消息。

class Dep {
  constructor() {
    this.eventQueueMap = new Map()
  }
  on(event, cb) {
    this.eventQueueMap.has(event) ? this.eventQueueMap.get(event).push(cb) : this.eventQueueMap.set(event, [cb])
  }
  emit(event, ...args) {
    const eventQueue = this.eventQueueMap.get(event)
    if (!eventQueue) return false
    eventQueue.forEach(cb => {
      cb(args)
    })
  }
  off(event) {
    if (this.eventQueueMap.has(event)) this.eventQueueMap.delete(event)
  }
}

const event = new Dep()

// 订阅
event.on('test', function(name) {
  console.log(name)
})

// 发布
event.emit('test', '二哈', '波斯猫')

实现一个响应式框架

先看一下Vue响应式的实现过程

image

  1. 首先初始化 data 对象,劫持这堆东西,让他们变得可追踪
  2. 初始化 Watvher,订阅自己关注的数据
  3. 读取数据时触发 getter,实现 Model -> View 的渲染
  4. 设置数据时触发 setter,实现 View -> Model 的变更,同时 Model 变更后 Watcher 重新渲染视图

在整个响应式的过程中其实还涉及到 Dep 依赖收集和 Compile 解析器等功能,后面会慢慢讲解

自己做一个

要做一个响应式框架,涉及到监听器 Observer,依赖收集中心 Dep,订阅者 Watcher,解析器 Compile

  1. 监听器 Observer 负责劫持数据。

需要递归把数据对象的所有属性都劫持成可追踪的

  1. 依赖收集中心 Dep 负责管理订阅者,发布消息,是发布订阅中的发布者。

不是所有的数据都是 View 用得上的,只需要派发有人订阅的数据即可。Dep 依赖收集中心就是来负责管理这些订阅者的。

  1. 订阅者 Watcher 负责订阅自身关注的数据,可以收到属性的变化通知并执行相应的函数,从而更新视图,是发布订阅中的订阅者

在订阅者初始化的时候,触发劫持对象的 getter,将自己注入到 Dep 中,表示需要订阅某个数据

  1. 解析器 Compile 负责扫描和解析每个节点的相关指令,来识别哪些数据需要订阅,需要双向绑定

解析器就是发布 - 订阅的桥梁,让订阅者和发布者之间产生联系。比如识别到 {{}} 就初始化一个 Watcher 监听它,并且由 Dep 管理这个 Watcher,篇幅有限,本文就不实现 Compile 了

监听器 Observer

class Observer {
  constructor() {
    this.dep = new Dep()
  }
  observe(obj) {
    if (!obj || typeof obj !== 'object') return obj
    Reflect.ownKeys(obj).forEach(key => {
      // 劫持对象的所有属性
      this.reactiveFn(obj, key, obj[key])
    })
  }
  reactiveFn(obj, key, val) {
    // 递归劫持所有属性的子属性
    this.observe(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: () => {
        // 只有初始化 Watcher 时触发的 getter,才需要订阅
        if (Dep.target) {
          this.dep.addSub(Dep.target) // 订阅数据
        }
        return val
      },
      set: newV => {
        // 通知 Watcher
        if (val !== newV) {
          val = newV
          this.dep.notify()
        }
      }
    })
  }
}

依赖收集中心 Dep

class Dep {
  constructor() {
    this.subs = []
  }
  // 把 watcher 塞入订阅者队列
  addSub(sub) {
    this.subs.push(sub)
  }
  // 通知所有的订阅者数据已变更
  notify() {
    this.subs.forEach(function(sub) {
      sub.update()
    })
  }
}

订阅者 Watcher

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.value = this.watch() // 将自己添加到订阅器
  }
  update() {
    var value = this.vm.data[this.exp]
    var oldVal = this.value
    if (value !== oldVal) {
      this.value = value
      this.cb.call(this.vm, value, oldVal)
    }
  }
  watch() {
    Dep.target = this // 缓存自己
    var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
    Dep.target = null // 释放自己
    return value
  }
}

测试用例

// 这里省去解析器Compile
class MyVue {
  constructor(options) {
    this.data = options.data()
    const observer = new Observer()
    observer.observe(this.data)
    for (let key in this.data) {
      console.log('初始化模板的值为', this.data[key])
      new Watcher(this, key, function(value) {
        console.log('值被修改为', value)
      })
    }
    return this
  }
}

const vm = new MyVue({
  data() {
    return {
      name: 'Evan',
      age: 24
    }
  }
})
setTimeout(function() {
  vm.data.name = 'Fiona'
  vm.data.age = 26
}, 2000)

« 常见 Vue 面试题

Evan的博客

Evan 的博客 - 非典型码农,bug永动机
Instagram Weibo GitHub Email RSS

Categories

面试 原理笔记 项目实践 其他 JS Vue 性能优化 算法 计算机网络

Recent Posts

  • 从 HTTP 发展历程重学计算机网络
  • 应届前端的逆袭(中)
  • 应届前端的逆袭(上)
  • 应届前端的逆袭(下)
  • 前端面经复盘

Copyright © 2015 Powered by MWeb,  Theme used GitHub CSS.