深入 Vue 响应式原理

发布于 6 年前
2 分钟阅读

在 Vue 3 之前,Vue 使用了 Object.defineProperty() 来实现响应式。


什么是响应式?

简单来说就是当数据发生修改时,视图会进行相应的更新。


观察者模式

在分析 Object.defineProperty() 的 API 之前,我们先来看下观察者模式,这样对响应式中的依赖收集,分发有个感性的认知。

观察者模式在前端开发中很常见,例如事件模型。当我们给 DOM 对象添加一个事件监听器时,DOM 对象就增加了一个新的依赖,这个过程就被称为依赖收集。当触发 DOM 的事件时,所有相关的事件监听器都会被执行,这个过程就被称为事件分发。

下面是一个简单的事件模型。

class Target {
  constructor () {
    this.listener = {} // 监听器列表
  }
  /**
   * 添加监听器到监听器列表
   * @param event 事件名称
   * @param fn 监听器
   */
  addListener (event,fn){
    if(!this.listener[event]){
      this.listener[event] = []
    }
    this.listener[event].push(fn)
  }

  /**
   * 分发事件
   */
  dispatch () {
    let event = Array.prototype.shift.call(arguments)
    let listeners = this.listener[event]
    if(!listeners || listeners.length ===0)
      return
    listeners.forEach((fn)=>{
      fn.apply(this,arguments)
    })
  }
  remove (event,fn){
    let fns = this.listener[event]
    if (!fns) {
      return false
    }else if (!fn) {
      fns.length = 0
    }else {
      for(let i = fns.length-1; i>=0;i--){
        let _fn = fns[i];
        if(_fn === fn){
          fns.splice(i,1)
        }
      }
    }
  }
}
let target = new Target()
target.addListener('click',(info)=> {
  console.log(info)
})
target.dispatch('click','foo')
target.remove('click')

Object.defineProperty()

Object.defineProperty() 中很重要的两个概念就是 getter 和 setter。当属性值修改时,触发 setter。当访问属性值时,触发 getter。相当于属性值的两个生命周期函数。

当视图渲染的时候,会读取属性值,也就是读取 setter 的返回值,我们可以在这个时候收集依赖。当属性值修改时,会触发 setter,这个时候可以进行依赖分发。

Vue 官方文档提供的图很清晰地展示了这个过程。


源码分析

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  // ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // ...
  })
}
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}
export default class Watcher {
  // ...     
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
 // ...
}

Vue 中的 Watcher 类就是观察者模式中的观察者,主要主责视图更新。负责存储观察者列表的类为 Dep。

当视图更新时,调用 Dep 中的 depend 方法将 Watcher 添加到依赖列表。当数据修改时,会触发 this.getter.call(vm, vm)this.getter 对应就是 updateComponent 函数,会触发视图更新。


参考资料

Vue 技术内幕

Vue.js 技术揭秘

「 Vue 官方文档 」深入响应式原理

  • 什么是响应式?
  • 观察者模式
  • Object.defineProperty()
  • 源码分析
  • 参考资料