前言

Vue 是目前流行前端框架,其独特的特性是其非侵入性的响应式系统。当侦测到数据的变化来更新视图,原理核心是使用 Object.defineProperty 方法。本文对响应式原理进行分析,参照 vue 源码实现简易版的数据响应式。

代码实现

本文完整代码点击这里

import { def, isObject } from '../util/index'

class Observer {
  constructor (value) {
    this.value = value
    // 通过 defineProperty 为对象添加 __ob__ 属性,并且配置为不可枚举
    // 这样做的意义是对象遍历时不会遍历到 __ob__ 属性
    def(value, '__ob__', this)
    this.walk(value)
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

function defineReactive (obj, key, val) {
  // 如果 val 是对象的话递归监听
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true, // 可枚举
    configurable: true, // 可配置
    get: function reactiveGetter () {
      return val
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val || (newVal !== newVal && val !== val)) {
        return
      }
      val = newVal
      // 如果赋值是对象的话也要递归监听
      observe(newVal)
      console.log('侦测到数据变化', newVal);
    }
  })
}

export function observe (value) {
  // 类型判断,不是对象类型直接返回
  if (!isObject(value)) {
    return
  }
  let ob = new Observer(value)
  return ob
}

上面这段代码主要作用在于:observe 函数传入一个 value (需要被追踪变化的对象),作为 Observer 类的参数实例化,遍历所有属性对该对象的每一个属性都通过 defineReactive 处理, 在 defineReactive 方法内 observe 会进行递归调用,以此来达到实现侦测对象变化。

接下来实现 Vue 类,对 options 中的 data 传入 observe 开发进行初始化数据侦测。

import { observe } from '../observer/index'

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: function () {},
  set: function () {}
}

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function Vue (options) {
  let vm = this
  let data = options.data
  vm._data = data

  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // 可以让 vm._data.x 通过 vm.x 访问
    proxy(vm, `_data`, key)
  }

  observe(data)
}

export default Vue

实例化 Vue 类,查看控制台的输出 vm。

import Vue from './instance/index'

const vm = new Vue({
  data: {
    message: 'hello',
    location: { x: 100, y: 100 },
    arr: [1]
  }
})

window.vm = vm

// 在控制台输入: vm.location = { x: 10, y: 10 } 
// 输出:侦测到数据变化 {__ob__: Observer}
// 输入:vm.location.z = 10
// 输出:10
// 输入:vm.arr.push(2)
// 输出:2

从中可以看到几个问题:

  • 无法检测到对象属性的添加或删除

通过 Object.defineProperty 来将对象的key转换成 getter/setter 的形式来追踪变化,但 getter/setter 只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。

实现 Vue 提供的 setdelete 方法向嵌套对象添加/删除响应式属性。

  • 不能监听数组的变化,需要进行数组方法的重写

解决以上问题,具体代码如下:

// array.js
// 获得数组原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 创建一个自己的原型 并且重写 methods 这些方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  arrayMethods[method] = function (...args) {
    const result = original.apply(this, args)
    return result
  }
})

observer/index.js 中导入 arrayMethods 重写的原型对象。

import { arrayMethods } from './array'
import { def, isObject } from '../util/index'

class Observer {
  constructor (value) {
    this.value = value
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

以上代码实现了数据劫持,接下来需要实现收集依赖以及数据更新时派发更新,其中的核心思想就是发布-订阅模式。关于订阅者 Dep 和观察者 Watcher 相关代码。

订阅者 Dep

import { remove } from '../util/index'

export default class Dep {
  constructor () {
    /* 用来存放 Watcher 对象的数组 */
    this.subs = []
  }

  /* 在 subs 中添加一个 Watcher 对象 */
  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }
  
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  /* 通知所有 Watcher 对象更新视图 */
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

/* 在 watcher.js 中调用,将 Watcher 实例赋值给 Dep.target */ 
Dep.target = null

观察者 Watcher

import Dep from './dep'

export default class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = expOrFn || function () {}
    this.cb = cb
    this.value = this.get()
  }

  get () {
    Dep.target = this

    const vm = this.vm
    let value = this.getter.call(vm, vm)
    return value
  }

  addDep(dep) {
    dep.addSub(this)
  }

  update () {
    console.log('更新value:', this.value)
  }
}

在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher,然后执行 update 函数。

最后对 defineReactive 函数和 Vue 类进行改造

export function defineReactive (obj, key, val) {
  const dep = new Dep()
  let childOb = observe(val)
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 将 Watcher 添加到订阅  
      dep.depend()
      return val
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val || (newVal !== newVal && val !== val)) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      // 执行 watcher 的 update 方法
      dep.notify()
    }
  })
}
function Vue (options) {
  let vm = this
  let data = options.data
  vm._data = data

  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    proxy(vm, `_data`, key)
  }

  observe(data)
  /* 新建一个 Watcher 观察者对象,这时候 Dep.target 会指向这个 Watcher 对象 */
  new Watcher(data, val => val)
}

当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

总结

本文在阅读 Vue 源码后,根据自己的理解加上参考其他文章,编写的一个精简代码实现。其中代码实现并不严谨以及自身的理解不到位,在此深表惭愧。