Skip to content

Vue 响应式原理

第一章:响应式

一、是什么

响应式是指当数据发生变化时,视图(UI)会自动更新,无需手动操作 DOM。

二、原理

1. vue2.x 的响应式

实现原理:

  • 对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)。

  • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

    javascript
    Object.defineProperty(data, 'count', {
      get () {},
      set () {}
    })

存在问题:

  • 新增属性、删除属性,界面不会更新。
  • 直接通过下标修改数组,界面不会自动更新。

2. Vue3.x 的响应式

实现原理:

第二章:实现

1. 依赖收集

1)数组版

要点:就是把使用它的地方看成为函数,在变化的时候调用下。

1)定义一个数组,专门收集用到的地方。

2)数据改变的时候,依此调用这个数组里的函数。

javascript
// 设置一个专门执行响应式函数的一个函数
const reactiveFns = []
function watchFn(fn) {
  reactiveFns.push(fn)
  fn()
}

// 业务代码 ---------------------------------------

const obj = {
  name: "why",
  age: 18
}

watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo", obj.age)
  console.log("foo function")
})

watchFn(function bar() {
  console.log("bar:", obj.name + " hello")
  console.log("bar:", obj.age + 10)
  console.log("bar function")
})

// 修改obj的属性
console.log("name 发生变化 -----------------------")
obj.name = "kobe"
reactiveFns.forEach(fn => {
  fn()
})

2)类

要点:定义一个类,类中有一个数组以及一些依次调用数组里的函数与往数组里添加函数的方法。可以把这个类看成监听类。

javascript
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 设置一个专门执行响应式函数的一个函数
const dep = new Depend()
function watchFn(fn) {
  dep.addDepend(fn)
  fn()
}

// 业务代码 ---------------------------------------

const obj = {
  name: "why",
  age: 18
}

watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo", obj.age)
  console.log("foo function")
})

watchFn(function bar() {
  console.log("bar:", obj.name + " hello")
  console.log("bar:", obj.age + 10)
  console.log("bar function")
})

// 修改obj的属性
console.log("name 发生变化 -----------------------")
obj.name = "kobe"
dep.notify()

console.log("age 发生变化 -----------------------")
dep.notify()

console.log("name 发生变化 -----------------------")
obj.name = "james" // 如果忘记调用 dep.notify() 怎么办?

2. 监听属性变化

要点:给数据添加 set 修饰符。在数据修改的时候调用下使用的地方(函数)。

javascript
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 设置一个专门执行响应式函数的一个函数
const dep = new Depend()
function watchFn(fn) {
  dep.addDepend(fn)
  fn()
}

// 方案一: Object.defineProperty() -> Vue2
Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    set: function(newValue) {
      value = newValue // 此处千万不要 obj[key]=newValue, 会形成死循环。要么使用闭包, 要么赋值为一个新对象
      dep.notify()
    },
    get: function() {
      return value // 虽然 set 方法里没有修改原对象里面的数据,但这里取数据是获取到了上层 let value = obj[key] 的数据。获取到的是修改后的数据。这里体现了闭包的作用
    }
  })
})

// 业务代码 ---------------------------------------

const obj = {
  name: "why",
  age: 18
}

watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo", obj.age)
  console.log("foo function")
})

watchFn(function bar() {
  console.log("bar:", obj.name + " hello")
  // console.log("bar:", obj.age + 10)
  console.log("bar function")
})

// 修改obj的属性
console.log("name 发生变化 -----------------------")
obj.name = "kobe"

console.log("age 发生变化 -----------------------")
obj.age = 20  // bar 函数没有依赖 age, 但是也调用了, 怎么办? 每个属性都要有 dep 对象。而不是 obj 对象对应一个 dep 对象

console.log("name 发生变化 -----------------------")
obj.name = "james"

3. 自动收集依赖

假如页面数据依赖 obj 这个对象,为了实现发生哪个属性的改变只调用使用了这个属性的函数。所以每个属性都要有 dep 对象。

一个 obj 还有其他属性,也需要对应一个 dep 对象。那么怎么管理多个 dep 对象?用 Map 存起来,key 为属性名,value 为对应的 dep 对象。

假如页面依赖多个数据对象,我们需要有一个总的数据结构来管理每一个数据对象的多个 dep 对象所组成的 Map,这时候就使用 WeakMap,其中 key 为数据对象,value 为每一个数据对象的多个 dep 对象所组成的 Map。

1)基本实现

javascript
/**
  * 1. dep 对象数据结构的管理 (最难理解)
  *    每一个对象的每一个属性都会对应一个 dep 对象
  *    同一个对象的多个属性的 dep 对象是存放一个 map 对象中
  *    多个对象的 map 对象, 会被存放到一个 objMap 的对象中
  * 2. 依赖收集: 当执行 get 函数, 自动的添加 fn 函数
*/
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  fn() // 当执行完此代码的时候, 依赖就已经收集好了
  reactiveFn = null
}

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const weakMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据对象obj, 找到对应的map对象
  let objMap = weakMap.get(obj)
  if (!objMap) {
    objMap = new Map()
    weakMap.set(obj, objMap)
  }

  // 2.根据key, 找到对应的depend对象
  let dep = objMap.get(key)
  if (!dep) {
    dep = new Depend()
    objMap.set(key, dep)
  }

  return dep
}

// 方案一: Object.defineProperty() -> Vue2
Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    set: function(newValue) {
      value = newValue
      const dep = getDepend(obj, key)
      dep.notify()
    },
    get: function() {
      // 找到对应的obj对象的key对应的dep对象
      const dep = getDepend(obj, key)
      dep.addDepend(reactiveFn)

      return value
    }
  })
})

// 业务代码 ---------------------------------------

const obj = {
  name: "why",
  age: 18
}

watchFn(function foo() {
  console.log("foo function")
  console.log("foo:", obj.name)
  console.log("foo", obj.age)
})

watchFn(function bar() {
  console.log("bar function")
  console.log("bar:", obj.age + 10)
})

// 修改obj的属性
console.log("name 发生变化 -----------------------")
obj.name = "kobe"
console.log("age 发生变化 -----------------------")
obj.age = 20

2)细节优化

目前版本存在的问题:

  • 如果 bar 函数中多次使用了 age,那么会调用两次 bar 函数在同一时刻 --> 把 reactiveFns 的数组结构改为 set
  • 在 getter 里面 dep.addDepend(reactiveFn) 可以进行优化。因为 reactiveFn 是自由变量,因此可以在 Depend 类中添加 depend 方法。
javascript
class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  fn()
  reactiveFn = null
}

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const weakMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据对象obj, 找到对应的map对象
  let objMap = weakMap.get(obj)
  if (!objMap) {
    objMap = new Map()
    weakMap.set(obj, objMap)
  }

  // 2.根据key, 找到对应的depend对象
  let dep = objMap.get(key)
  if (!dep) {
    dep = new Depend()
    objMap.set(key, dep)
  }

  return dep
}

// 方案一: Object.defineProperty() -> Vue2
Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    set: function(newValue) {
      value = newValue
      const dep = getDepend(obj, key)
      dep.notify()
    },
    get: function() {
      // 找到对应的obj对象的key对应的dep对象
      const dep = getDepend(obj, key)
      // dep.addDepend(reactiveFn)
      dep.depend()

      return value
    }
  })
})

// ========================= 业务代码 ========================

const obj = {
  name: "why",
  age: 18,
  address: "广州市"
}

watchFn(function() {
  console.log(obj.name)
  console.log(obj.age)
  console.log(obj.age)
})

watchFn(function() {
  console.log(obj.address)
})

watchFn(function() {
  console.log(obj.age)
  console.log(obj.address)
})

// 修改name
console.log("--------------")
obj.name = "kobe"
// 修改age
console.log("--------------")
obj.age = 20
// 修改address
console.log("--------------")
obj.address = "上海市"

4. 多个对象响应式

目前版本存在的问题:

  • 只能对 obj 对象实现响应式,怎么办?把遍历 obj 对象,添加 setter / getter 方法的代码封装为一个函数。
javascript
class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  fn()
  reactiveFn = null
}

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const weakMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据对象obj, 找到对应的map对象
  let objMap = weakMap.get(obj)
  if (!objMap) {
    objMap = new Map()
    weakMap.set(obj, objMap)
  }

  // 2.根据key, 找到对应的depend对象
  let dep = objMap.get(key)
  if (!dep) {
    dep = new Depend()
    objMap.set(key, dep)
  }

  return dep
}

// 方案一: Object.defineProperty() -> Vue2
function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
  
    Object.defineProperty(obj, key, {
      set: function(newValue) {
        value = newValue
        const dep = getDepend(obj, key)
        dep.notify()
      },
      get: function() {
        // 找到对应的obj对象的key对应的dep对象
        const dep = getDepend(obj, key)
        dep.depend()
  
        return value
      }
    })
  })  
  return obj
}

// ========================= 业务代码 ========================

const obj = reactive({
  name: "why",
  age: 18,
  address: "广州市"
})

watchFn(function() {
  console.log(obj.name)
  console.log(obj.age)
  console.log(obj.age)
})

// 修改name
console.log("--------------")
// obj.name = "kobe"
obj.age = 20
// obj.address = "上海市"

console.log("=============== user =================")
const user = reactive({
  nickname: "abc",
  level: 100
})

watchFn(function() {
  console.log("nickname:", user.nickname)
  console.log("level:", user.level)
})

user.nickname = "cba"

5. Proxy 实现响应式

javascript
class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  fn()
  reactiveFn = null
}

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const weakMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据对象obj, 找到对应的map对象
  let objMap = weakMap.get(obj)
  if (!objMap) {
    objMap = new Map()
    weakMap.set(obj, objMap)
  }

  // 2.根据key, 找到对应的depend对象
  let dep = objMap.get(key)
  if (!dep) {
    dep = new Depend()
    objMap.set(key, dep)
  }

  return dep
}

// 方式二: new Proxy() -> Vue3
function reactive(obj) {
  const objProxy = new Proxy(obj, {
    set: function(target, key, newValue, receiver) {
      // target[key] = newValue
      Reflect.set(target, key, newValue, receiver)
      const dep = getDepend(target, key)
      dep.notify()
    },
    get: function(target, key, receiver) {
      const dep = getDepend(target, key)
      dep.depend()
      return Reflect.get(target, key, receiver)
    }
  })
  return objProxy
}

// ========================= 业务代码 ========================

const obj = reactive({
  name: "why",
  age: 18,
  address: "广州市"
})

watchFn(function() {
  console.log(obj.name)
  console.log(obj.age)
  console.log(obj.age)
})

// 修改name
console.log("--------------")
// obj.name = "kobe"
obj.age = 20
// obj.address = "上海市"

console.log("=============== user =================")
const user = reactive({
  nickname: "abc",
  level: 100
})

watchFn(function() {
  console.log("nickname:", user.nickname)
  console.log("level:", user.level)
})

user.nickname = "cba"
preview
图片加载中
预览

Released under the MIT License.