vue-响应式系统(原理篇)

371 阅读10分钟

前言

1. MVVM 模式

模板

<p>我今年{{age}}岁了

数据

this.age ++

model <=view-model=> view

2. 侵入式和非侵入式

侵入式

(React-数据变化) this.setState({ n: this.n + 1 })

(小程序-数据变化) this.setData({ m: this.m + 1 })

非侵入式

(Vue-数据变化) this.num ++

非侵入式实现原理

核心是object.definefroperty()进行数据劫持/数据代理

利用javascript引擎方法,检测对象属性变化

image.png

3. 什么是响应式系统?

数据改变时,视图对应的更新,就是响应式系统。

实现响应式系统的核心内容

1. 数据劫持

2. 依赖收集

3. 派发更新

一、数据劫持

1. Object.defineProperty

Object.defineProperty进行数据劫持

const data = {}
let value = 100

Object.defineProperty(data, key, {
  get() { 
    console.log(`访问key属性`)
    return value
  },
  set(newValue) { 
    if (value === newValue) return
    console.log(`改变key属性`, newValue)
    value = newValue
  }
})

如上所示

get()时能监听到数据的访问(取值),set()时能监听到数据的改变(赋值)。

我们可以在get()set()时对数据做一些处理,重写原有的行为,这就是数据劫持的意义。

2. defineReactive

为方便使用,对Object.defineProperty做封装处理:defineReactive()

function defineReactive (data, key, value) {
  if (arguments.length === 2) {
    value = data[key]
  }
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get () {
      // console.log(`访问${key}属性`)
      return value
    },
    set (newValue) {
      // console.log(`改变${key}属性`, newValue)
      if (value === newValue) return
      value = newValue
    }
  })
}

使用 defineReactive(data, key, value)

const obj = {
    a: 100,
    b: 200
};

// 添加属性
defineReactive(obj, c, 300)
console.log(obj) // {a: 100, b: 200, c: 300}

// 修改属性
defineReactive(obj, a, 1000)
console.log(obj) // {a: 1000, b: 200, c: 300}

由上可知,我们在访问、添加、修改时能检测到数据。

3. Observer 类

问题1:如果obj有多个属性时,我们需要怎么监测???

我们需要新建一个类Observer 类来遍历该对象的所有属性和属性下的每个层级的属性。

Observer就是一个观察者,用来做数据侦测。就是将一个正常的object转换为每个层级的属性都是响应式的(可以被侦测的)object

class Observer {
  constructor(value) {
    
    // 给value添加__ob__属性,值为new的实例ob,并设置不可枚举。
    // 【ob = Observe,即 ob = value.__ob__ 或者 new OBserver(value)】
    def(value, '__ob__', this, false) // 给对象每层添加__ob__属性

    this.walk(value)
  }
  walk (value) {
    // 对象遍历,对 value 所有子元素进行defineReactive绑定
    for (const key in value) {
      defineReactive(value, key)
    }
  }
}

创建Observer类时,我们用到了def函数,def函数是用来设置对象操作设置的,这里是用来设置属性值枚举权限的。还有是否可删除、修改等设置...

const def = (data, key, value, enumerable) => {
  Object.defineProperty(data, key, {
    value,
    enumerable, // 是否可枚举
    writable: true, // 是否可修改
    configurable: true // 是否可删除
  })
}

测试一下 Observer,遍历所有属性

const obj = { a: 1, b: 2 } 
new Observer(obj)

console.log(obj.a); // 访问a属性,1
console.log(obj.b); // 访问a属性,2
obj.a = 11 // 改变a属性,11
obj.b = 22 // 改变b属性,22

以下为测试结果:

image.png

如图所示,所有属性都被检测到,切给对象添加了__ob__属性

4. Observe 函数

问题2:如果obj内有嵌套的属性时,我们需要怎么监测???

我们需要Observ函数,来帮助我们完成对子属性的监测绑定

Observe 函数,是用来在绑定对象监测的,是Observer的扩展工具函数,用于设置Observer观察者。(给对象添加__ob__属性)

function Observe (value) {
  // 不是对象不处理
  if (typeof value != 'object') return
  // 定义ob
  let ob
  if (typeof value.__ob__ != 'undefined') {
    // 判断当前对象上是否有__ob__属性,有则直接使用
    ob = value.__ob__
  } else {
    // 若对象本层没有__ob__就 new Oberser() 添加
    ob = new Observer(value)
  }
  return ob
}

修改 defineReactive 函数,进入defineReactive函数就立即进行observe(),在发生改变set()时,对新值也要进行observe()

function defineReactive (data, key, value) {
  if (arguments.length === 2) {
    value = data[key]
  }

  `对当前对象和对象子元素进行Oberse,形成循环递归`
  `let childOb = Observe(value)`

  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get () {
      // console.log(`访问${key}属性,${value}`)
      return value
    },
    set (newValue) {
      // console.log(`改变${key}属性,${newValue}`)
      if (value === newValue) return
      value = newValue
      `假如有新值,那么Oberse绑定新值`
      `childOb = Observe(newValue)`
    }
  })
}

测试Observe函数

import Observe from './Observe'

let obj = {
  a: {
    a1: {
      a11: 11
    },
    a2: 12
  },
  b: {
    b1: 22
  }
}

Observe(obj) // 监测obj
console.log(obj);

以下为测试结果:

image.png

如图所示,我们给对象的每一层未对象的属性添加了__ob__,实现了对对象的每一层的数据劫持

5. 总结数据劫持的执行过程

image.png

执行observe(obj)

├── new Observer(obj),并执行this.walk()遍历obj的属性,执行defineReactive()
    `├── defineReactive(obj, a)`
        ├── 执行observe(obj.a) 发现obj.a是对象
            ├── 执行 new Observer(obj.a),遍历obj.a的属性,执行defineReactive()
                `├── defineReactive(obj.a, a1)`
                    ├── 执行observe(obj.a.a1) 发现obj.a是对象
                        ├── 执行 new Observer(obj.a.a1) 遍历obj.a.a1的属性
                             ├── defineReactive(obj.a.a1, a11)
                                ├── 执行observe(obj.a.a1.a11) obj.a.a1.a11不是对象,直接返回
                                ├── 执行defineReactive(obj.a.a1, a11)的剩余代码
                `├── defineReactive(obj.a, a2)`
                    ├── 执行observe(obj.a.a2) 发现obj.a不是对象
                    ├── 执行defineReactive(obj.a, a2)的剩余代码
    `├── defineReactive(obj, b)`
        ├── 执行observe(obj.b) 发现obj.b是对象
            ├── 执行 new Observer(obj.b),遍历obj.b的属性,执行defineReactive()
                ├── 执行defineReactive(obj.b, b1)
                    ├── 执行observe(obj.b.b1) 发现obj.b.b1不是对象,直接返回
                    ├── 执行defineReactive(obj.b, b1)的剩余代码
        ├── 执行defineReactive(obj, b)的剩余代码
            
代码执行结束

二、依赖收集

1. 发布和订阅模式

什么是 发布和订阅模式

举个例子,微博关注明星如花 是国内体育明星,粉丝千万,小a 是其粉丝之一,小a 想了解 如花 的动态,于是在微博关注 如花,当 如花 更新微博时,微博就会推送 如花 动态给 小a

通过以上 微博关注明星 的例子,我们可以把其逻辑抽象理解为:发布和订阅模式

粉丝关注 如花 = 在微博登记自己的信息对 如花 进行订阅,微博会把订阅者信息存到一个数组中, 如花 在更新状态时,微博会遍历数组,逐个通知订阅者

2. Watcher 类

在响应式系统中,也有储存粉丝信息的数组,就是Watcher

通过例子我们知道,Watcher有三个核心功能:

  1. 有一个数组来存储Watcher
  2. Watcher实例需要订阅(依赖)数据,也就是获取依赖或者收集依赖
  3. Watcher的依赖发生变化时触发Watcher的回调函数,也就是派发更新。

实现 Watcher

class Watcher {
  constructor(data, expression, callback) {
    this.data = data // 要观察的对象
    this.expression = expression // 要观察的对象具体哪个属性
    this.callback = callback // 发布动作,订阅的属性有变化时触发回调函数
    this.value = this.get() // 默认在实例化时就获取到,要观察对象的具体属性的值
  }
  get () {
    // 进行依赖收集,让全局的Dep.target设置为Watcher本身,那么就是进入了依赖收集阶段
    window.target = this // 存放 Watcher 数组位置的地方
    let value
    try {
      value = parsePath(this.expression)(this.data)
    } finally {
      window.target = null
    }
    return value
  }

  // 更新
  update () {
    const value = this.get()
    // 如果新值不等于旧值,或者新值为对象
    if (value !== this.value || typeof value == 'object') {
      // 存旧值为 oldValue
      const oldValue = this.value
      // 让this.value=新值
      this.value = value
      this.callback.call(this.data, value, oldValue)
    }
  }
}

// 工具函数 parsePath
const parsePath = (str) => {
  let segments = str.split('.')
  return (obj) => {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

测试一下 Watcher

let obj = {
  a: {
    b: 100,
    c: 200
  }
}

const str_b = new Watcher(obj, 'a.b') // 订阅obj对象内的a.b
const str_c = new Watcher(obj, 'a.c') // 订阅obj对象内的a.c

console.log('str_b', str_b.value) // str_b 100
console.log('str_c', str_c.value) // str_c 200

上述测试,我们可以通过对象订阅,获取到对象对应的值。但是在数据变化时我们不知道,所以还需要把数据劫持和依赖收集结合一下

实现 Dep 类

结合defineReactive函数的set()方法,在数据更新时,我们能知道数据发生变化了,能拿到新数据,并通知发布。每个数据都应该维护一个属于自己的数组,该数组来存放依赖自己的watcher,我们可以在defineReactive中定义一个数组dep,这样通过闭包,每个属性就能拥有一个属于自己的dep

修改 defineReactive 函数

function defineReactive (data, key, value) {

  `const dep = new Dep()`

  if (arguments.length === 2) {
    value = data[key]
  }

  // 子元素进行Oberse,形成循环递归
  let childOb = Observe(value)

  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get () {
      // 如果现在处于依赖收集阶段
      `订阅收集,如果有子属性,对子属性也进行收集`
      dep.depend() 
      if (childOb) {
        childOb.dep.depend()
      }
      return value
    },
    set (newValue) {
      if (value === newValue) return
      value = newValue
      // 假如有新值,那么Oberse绑定新值
      childOb = Observe(newValue)
    }
  })
}

实现 Dep 类

class Dep {
  constructor() {
    this.id = uid++
    // subscribes简写,用来储存订阅者,这个数组内放的是watcher的实例
    this.subs = []
  }

  // 通知发布更新
  notfiy () {
    // 通知所有订阅者更新
    [...this.subs].forEach(sub => sub.update())
  }

  // 添加依赖
  depend () {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
    // console.log('depend')
  }

  // 添加订阅
  addSub (sub) {
    this.subs.push(sub)
  }
}

三、派发更新

实现依赖收集后,我们最后要实现的功能是派发更新,也就是依赖变化时触发watcher的回调。从依赖收集部分我们知道,获取哪个数据,也就是说触发哪个数据的get,就说明watcher依赖哪个数据,那数据变化的时候通知watcher:在set中触发派发更新。

set (newValue) {
  if (value === newValue) return
  value = newValue
  // 假如有新值,那么Oberse绑定新值
  childOb = Observe(newValue)
  
  `发布订阅,通过dep.notfiy去通知dep`
  dep.notfiy()
}

四、补充,对数组特殊处理

我们访问和获取arr的值,getset会被触发,但是如果arr.push()呢?数组的每个元素要依次向后移动一位,这就会触发getset,导致依赖发生变化。由于数组是顺序结构,所以索引(key)和值不是绑定的,因此这种护理方法是有问题的。

vuejs中7种会改变数组的方法进行了重写:push, pop, unshift, shift, splice, reverse, sort

改写这7个方法的重要环节就是:代理原型

// 获取数组的原型
const ArrayPrototype = Array.prototype

// 设置array的代理原型
// Object.create() 方法创建一个新对象 ArrayMethods,新对象的__proto__ 为 ArrayPrototype
export const proxyArrayPrototype = Object.create(ArrayPrototype)

// 需要改写的数组方法
const rewriteArrayMothods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]
// 进行改写 proxyArrayPrototype
rewriteArrayMothods.map(methodName => {
  // 获取原型上的方法
  const original = ArrayPrototype[methodName]
  def(proxyArrayPrototype, methodName, function () {
    // 把original的this指向到 proxyArrayPrototype
    const res = original.apply(this, arguments)
    // 因为最外层肯定是对象,所有__ob__一定存在
    const ob = this.__ob__
    // push,shift,splice会插入数据,arguments拿到新插入的数据
    let insert = []
    switch (methodName) {
      case 'push':
      case 'shift':
        insert = [...arguments]
        break;
      case 'splice':
        // splice(index,howmany,'新增的内容') 第三个参数为新增的数组元素,索引为2
        insert = [...arguments].slice(2) 
        break;
    }
    // 如果有新插入的数据,那么对新插入的数据进行observe绑定(监听)
    if (insert.length > 0) ob.observeArray(insert)
    // 数组发生变化,也要通知dep
    ob.dep.notfiy()
    // console.log('操作数组~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
    return res
  }, false)
})

由上代码,我们看出,在 proxyArrayPrototype 代理原型上,我们改写了7个方法 主要意义是在触发这个7个方法时,做两件事:

  1. 进行dep.notfiy()发布更新
  2. 当数组插入新数据时,进行添加依赖(添加订阅) ob.observeArray(insert)

对Array的代理原型上7个方法改造完之后,我们需要修改 Observer 类

Observer观察的数据类型为数组时:

  1. 我们要把 数组的原型指向 代理原型:Object.setPrototypeOf(value, proxyArrayPrototype)
  2. 需要把数组的每一项进行Oberse监听
class Observer {
  constructor(value) {
    this.dep = new Dep()
    // this === 实例本身,而不是类的本身
    // 给value添加__ob__属性,值为new的实例ob,并设置不可枚举。 
    //【ob = Observe,即 ob = value.__ob__ 或者 new OBserver(value)】
    def(value, '__ob__', this, false)
    if (Array.isArray(value)) {
      // value === 数组,将value数组的原型,指向代理原型 proxyArrayPrototype
      Object.setPrototypeOf(value, proxyArrayPrototype)
      // value === 数组,需要把数组的每一项进行Oberse监听
      this.observeArray(value)
    } else {
      // value === 对象或者字符串,对 value 所有子元素进行defineReactive绑定
      this.walk(value)
    }
  }
  walk (value) {
    // 对象遍历,对 value 所有子元素进行defineReactive绑定
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  observeArray (value) {
    for (let i = 0, j = value.length; i < j; i++) {
      Observe(value[i])
    }
  }
}

五、代码整理

defineReactive.js

import Observe from './Observe'
import Dep from "./Dep"

export default function defineReactive (data, key, value) {
  const dep = new Dep()
  if (arguments.length === 2) {
    value = data[key]
  }
  // 子元素进行Oberse,形成循环递归
  let childOb = Observe(value)
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get () {
      // console.log(`访问${key}属性,${value}`)
      // 如果现在处于依赖收集阶段
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
      }
      return value
    },
    set (newValue) {
      // console.log(`改变${key}属性,${newValue}`)
      if (value === newValue) return
      value = newValue
      // 假如有新值,那么Oberse绑定新值
      childOb = Observe(newValue)
      // 发布订阅模式,通过dep.notfiy去通知dep
      dep.notfiy()
    }
  })
}

Observe.js

import Observer from "./Observer"
export default function Observe (value) {
  // 不是对象不处理
  if (typeof value != 'object') return
  // 定义ob
  let ob
  if (typeof value.__ob__ != 'undefined') {
    // 判断当前对象上是否有__ob__,有则直接使用
    ob = value.__ob__
  } else {
    // 若对象本层没有__ob__就 new Oberser() 添加观察者
    ob = new Observer(value)
  }
  return ob
}

Observer.js

import { def } from './utils'
import defineReactive from './defineReactive'
import Observe from './Observe'
import { proxyArrayPrototype } from "./array"
import Dep from "./Dep"
// ? Observer : 观察者,数据侦测。 将一个正常的object转换为每个层级的属性都是响应式的(可以被侦测的)object
export default class Observer {
  constructor(value) {
    this.dep = new Dep()
    // this === 实例本身,而不是类的本身
    // 给value添加__ob__属性,值为new的实例ob,并设置不可枚举。 【ob = Observe,即 ob = value.__ob__ 或者 new OBserver(value)】
    def(value, '__ob__', this, false)
    // console.log(`Observer构造器`, value);
    if (Array.isArray(value)) {
      // value === 数组,将value数组的原型,指向代理原型 proxyArrayPrototype
      Object.setPrototypeOf(value, proxyArrayPrototype)
      // value === 数组,需要把数组的每一项进行Oberse监听
      this.observeArray(value)
    } else {
      // value === 对象或者字符串,对 value 所有子元素进行defineReactive绑定
      this.walk(value)
    }
  }
  walk (value) {
    // 对象遍历,对 value 所有子元素进行defineReactive绑定
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  observeArray (value) {
    for (let i = 0, j = value.length; i < j; i++) {
      Observe(value[i])
    }
  }
}

Dep.js

let uid = 0
export default class Dep {
  constructor() {
    this.id = uid++
    // subscribes简写,用来储存订阅者,这个数组内放的是watcher的实例
    this.subs = []
    // console.log('dep构造器')
  }

  // 通知更新
  notfiy () {
    // console.log('notify')
    // 通知所有订阅者更新
    [...this.subs].forEach(sub => sub.update())
  }

  // 添加依赖
  depend () {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
    // console.log('depend')
  }

  // 添加订阅
  addSub (sub) {
    this.subs.push(sub)
  }
}

Watcher.js

import { parsePath } from "./utils";
import Dep from './Dep'
let uid = 0

Dep.target = null // 一个全局变量,用来储存订阅数据,可以是任何变量,如:window.target

const TargetStack = []

const pushTarget = (_target) => {
  TargetStack.push(Dep.target)
  Dep.target = _target
}

const popTarget = (_target) => {
  Dep.target = TargetStack.pop()
}

export default class Watcher {
  constructor(data, expression, callback) {
    // console.log('Watcher')
    this.id = uid++
    this.data = data // 要观察的对象
    this.expression = expression // 要观察的对象具体哪个属性
    this.callback = callback // 发布动作,订阅的属性有变化时触发回调函数
    this.value = this.get() // 默认在实例化时就获取到,要观察对象的具体属性的值
  }
  get () {
    // 进行依赖收集,让全局的Dep.target设置为Watcher本身,那么就是进入了依赖收集阶段
    pushTarget(this)
    let value
    try {
      value = parsePath(this.expression)(this.data)
    } finally {
      popTarget(this)
    }
    return value
  }
  // 更新
  update () {
    const value = this.get()
    // 如果新值不等于旧值,或者新值为对象
    if (value !== this.value || typeof value == 'object') {
      // 存旧值为 oldValue
      const oldValue = JSON.parse(JSON.stringify(this.value))
      // 让this.value=新值
      this.value = value

      this.callback.call(this.data, value, oldValue)
    }
  }
}

array.js

import { def } from "./utils";
// 获取数组的原型
const ArrayPrototype = Array.prototype
// 设置array的代理原型
// Object.create() 方法创建一个新对象 ArrayMethods,新对象的__proto__ 为 ArrayPrototype
export const proxyArrayPrototype = Object.create(ArrayPrototype)
// 需要改写的数组方法
const rewriteArrayMothods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]
rewriteArrayMothods.map(methodName => {
  // 获取原型上的方法
  const original = ArrayPrototype[methodName]
  def(proxyArrayPrototype, methodName, function () {
    // 把original的this指向到 proxyArrayPrototype
    const res = original.apply(this, arguments)
    // 因为最外层肯定是对象,所有__ob__一定存在
    const ob = this.__ob__
    // push,shift,splice会插入数据,arguments拿到新插入的数据
    let insert = []
    switch (methodName) {
      case 'push':
      case 'shift':
        insert = arguments
        break;
      case 'splice':
        insert = arguments.slice(2) // splice(index,howmany,'新增的内容') 第三个参数为新增的数组元素,索引为2
        break;
    }
    // 如果有新插入的数据,那么对新插入的数据进行observe绑定(监听)
    if (insert.length > 0) ob.observeArray(insert)
    // 数组发生变化,也要通知dep
    ob.dep.notfiy()
    return res
  }, false)
})

utils.js

export const def = (data, key, value, enumerable) => {
  Object.defineProperty(data, key, {
    value,
    enumerable,
    writable: true,
    configurable: true
  })
}
export const parsePath = (str) => {
  let segments = str.split('.')
  return (obj) => {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

index.js 入口文件

import Observe from './Observe'
import Watcher from './Watcher'
let obj = {
  a: {
    a1: {
      a11: 11
    },
    a2: 12
  },
  b: {
    b1: 22
  },
  arr: [{ k: 111 }, 222, 333, 444]
}
Observe(obj)
// obj.a.ab1.ab1c1.ab1c1d1 = 1

// console.log('obj', obj);
// new Watcher(obj, 'a.a1.a11', (n, o) => {
//   console.log('新值:', n, '旧值:', o,);
// })
// obj.a.a1.a11 = 1
// new Watcher(obj, 'a.a2', (n, o) => {
//   console.log('新值:', n, '旧值:', o);
// })
// obj.a.a2 = 2

new Watcher(obj, 'arr', (n, o) => {
  console.log(`新值:`, n, `旧值:`, o);
})
obj.arr.push(1000)