# 一、数据响应式

# 1、需求分析

  当我们的数据 A 发现改变时,希望所以受数据 A 影响的方法等会同步重新执行。

提示

  可以结合 react 的数据更新机制思考一下

// 数据
let num = 6

// 视图
const render = () => {
  return console.log(num * num) // 36
}
render()

// 数据更新
num = 8 // 数据并没有响应式更新,console并没有重新执行

  但是更多的情况下,我们是需要对象能够实现响应式:

// 数据
let numb = 18
let person = {
  name: '张三',
  set: '男',
  age: numb
}

// 视图
const render = () => {
  return console.log(person.age) // 18
}
render()

// 数据更新
person.age = numb = 20 // 数据并没有响应式更新,console并没有重新执行

# 2、实现逻辑(手动)

  我们可以编写一个函数去监听数据的变化,一旦发生变化就让其执行 render 函数

  下面我们可以简单的实现一下(使用手动触发 render 重新执行)

基本实现
// 监听所有render
const reactiveFns = []
function watchFn(fn) {
  reactiveFns.push(fn)
  fn()
}
// ……

// 视图
const render = () => {
  return console.log(person.age)
}
// render()
watchFn(render)

// ……
// 重新触发所有render
reactiveFns.forEach((fn) => {
  fn()
})
基本实现 Plus(使用 class 封装)
  • 封装
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  // 监听render
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn)
    }
  }

  // 触发render更新
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn()
    })
  }
}
  • 使用
const dep = new Depend()

// 监听所有render
function watchFn(fn) {
  dep.addDepend(fn)
  fn()
}

// ……

// 重新触发所有render
dep.notify()

  为了方便与后面的内容做对比,先截取部分代码:

const dep = new Depend()

// 监听所有render
function watchFn(fn) {
  dep.addDepend(fn)
  fn()
}

// ……

// 重新触发所有render
dep.notify()

# 3、vue2 响应式

使用 Object.defineProperty() ----> vue2

简约版
const dep = new Depend()

// 2、重新触发所有render(自动✍执行)
Object.keys(obj).forEach((key) => {
  // 知识点:闭包、防递归
  let value = obj[key]

  Object.defineProperty(obj, key, {
    set: function (newValue) {
      value = newValue
      dep.notify()
    },
    get: function (value) {
      return value
    }
  })
})

  上面的简约版中,还有一个问题就是,它每次都会重新触发所有的 render,而不是与更改数据相关的 render:

解决方案

  使用数据结构:

  • 核心代码
// const dep = new Depend()

const objMap = new WeakMap()
function getDepend(obj, key) {
  // 根据对象obj,找对应的map对象
  let map = objMap.get(obj)
  if (!map) {
    map = new Map()
    objMap.set(obj, map)
  }

  // 根据key,找对应的depend对象
  let dep = map.get(key)

  if (!dep) {
    dep = new Depend()
    map.set(key, dep)
  }

  return dep
}
// 1、监听所有render
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  fn()
  reactiveFn = null
}

// ……

// 2、重新触发所有render(自动✍执行)

// 这里其实还有优化的空间(提示:将这里封装成一个函数reactive(obj),这样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 (value) {
      const dep = getDepend(obj, key)
      dep.addDepend(reactiveFn) // 这里其实还有优化的空间(提示:使用Set代替原class中的[])

      return value
    }
  })
})
优化 1
class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  // 监听render
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.add(fn)
    }
  }

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

  // 触发render更新
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn()
    })
  }
}

# 4、vue3 响应式

使用 new Proxy() ---- vue3

  其实 vue2 中已经完成了基本所有的功能,vue3 中对其的优化点大方向有两点:

  • 组合式 api

数据对象可以放在任何位置

优化 2
function reactive(obj) {
  Object.keys(obj).forEach((key) => {
    // 使用 Object.defineProperty()
    // ……
  })

  return obj
}
  • 新采用 new Proxy

new Proxy 支持 obj,不用像 Object.defineProperty()那样对 obj 监听还要使用 foreach

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(obj, key)
      dep.notify()
    },
    get: function (target, key, receiver) {
      const dep = getDepend(obj, key)
      dep.depend()

      // return target[key]
      return Reflect.get(target, key, receiver)
    }
  })

  return objProxy
}

# 二、响应式体验

# 1、数据代理

  一听到代理,我们就可以想到我在 JavaScript 的 ES6 部分做的笔记(里面做了详细的介绍):

Proxy 代理 (opens new window)

  在开始前,我们先补充一个知识点:

# 前提:参考下面的案例
vm._data === data # true

# 所以操作data可以有两种方式:
# 方式1
vm._data.username = 'one' # vue构造函数收集的data数据(并且数据是响应式的:数据劫持)
# 方式2
vm.username = 'one' # 数据代理

  上面只能是一个猜想,下面来验证一下是否存在数据代理:

vm 对象 代理对 _data 对象中的属性进行操作(读/写)

<div id="app">{{username}}</div>

<script>
  let data = {
    username: 'lencamo'
  }

  let vm = new Vue({
    el: '#app',
    data
  })

  // 1、getter验证
  data.username = 'ren'
  console.log(vm.username) // "ren"

  // 2、setter验证
  //   vm对象可以🤔对data进行操作
  vm.username = 'lili'
  console.log(data.username) // "lili"
</script>

结论:

所以 vue 对 vm._data 使用数据代理,可以简化或者说更方便的操作 data 中的数据

<div id="app">
  <p>{{_data.username}}</p>

  <!-- 简化 -->
  <p>{{username}}</p>
</div>

# 2、数据劫持

  vue 中最核心的一个点就是响应式数据,上面的案例中,我们可以发现一个问题:

vm._data 的内容并不是一个 data 的一个拷贝,转而好像做了数据代理(但实际上是为了实现响应式数据而做的数据拦截)

  前面我们也说了,在 vue 中 数据驱动视图。也就是说 vue 实现:

例如:如果 data 数据中的 username 发生改变,视图中的\{\{username}}也要同步更新

  vue 中最核心的一个点就是响应式数据(若数据改变,视图要同步更新)。

<div id="app">
  <p>{{username}}</p>
</div>

<script>
  let vm = new Vue({
    el: '#app',
    data: {
      username: 'lencamo'
    }
  })

  console.log(vm)
</script>

  其实,我们通过观察它们的 getter 和 setter 命名也可以看出不同(前面:reactiveSetter 响应式、后面:proxySetter 代理式):

vm.username

  如图(数据代理):

# 3、实现原理

  数据代理主要用于将组件的数据和属性访问(_data)委托给 Vue 实例本身,以便在访问和修改组件数据时可以触发 Vue 的响应式系统;

数据代理

效果如下:

let vm = {
  //

  _data: {
    title: '数据代理'
  }
}

// 数据代理🤔
Object.defineProperty(vm, 'title', {
  get: function proxyGetter() {
    return vm._data.title
  },
  set: function proxySetter(val) {
    vm._data.title = val
  }
})

console.log(vm)
console.log(vm.title) // 数据代理

// 1、getter验证
vm._data.title = 'one'
console.log(vm.title) // one

// 2、setter验证
vm.title = 'two'
console.log(vm._data.title) // two
let vm = {
  //

  _data: {
    username: 'lencamo',
    age: 21
  }
}

// 数据代理🤔
Object.keys(vm._data).forEach((key) => {
  Object.defineProperty(vm, key, {
    get: function proxyGetter() {
      return vm._data[key]
    },
    set: function proxySetter(val) {
      vm._data[key] = val
    }
  })
})

console.log(vm)

  数据劫持主要用于拦截和处理组件数据的访问和修改操作,以便在数据变化时可以通知 Vue 响应式系统更新视图

数据劫持

效果如下:

let msg = '数据劫持'

let vm = {
  //
  _data: {
    title: msg
  }
}

// 数据代理🤔
Object.defineProperty(vm._data, 'title', {
  get: function reactiveGetter() {
    return msg
  },
  set: function reactiveSetter(newVal) {
    msg = newVal
  }
})

console.log(vm)

// 验证:
msg = 'lencamo'
console.log(vm._data.title)
let username = '数据劫持'
let age = 21

let vm = {
  //
  _data: {
    username: username,
    age: age
  }
}

// 数据代理🤔
Object.keys(vm._data).forEach((key) => {
  Object.defineProperty(vm._data, key, {
    get: function reactiveGetter() {
      return key // 注意这里的意思
    },
    set: function reactiveSetter(newVal) {
      key = newVal
    }
  })
})

console.log(vm)

   通过上面对数据劫持和数据代理的代码还原,我们发现如果要对对象和数组进行响应式的话,是不可以的。解决办法:见后面

完整模拟版
// 视图
let username = 'lencamo'
let age = 21

// new Vue中的data
let data = {
  username: username,
  age: age
}

// vm实例对象
let vm = {
  //
  _data: data
}

// 数据劫持

Object.keys(data).forEach((key) => {
  Object.defineProperty(this, key, {
    get: function reactiveGetter() {
      return key // 这就是为什么要使用this🤔的原因
    },
    set: function reactiveSetter(newVal) {
      key = newVal
    }
  })
})

// 数据代理
Object.keys(data).forEach((key) => {
  Object.defineProperty(vm, key, {
    get: function proxyGetter() {
      return data[key]
    },
    set: function proxySetter(val) {
      data[key] = val
    }
  })
})

console.log(data)
console.log(vm)

// 验证:
// 2、数据双向绑定
username = 'success'
console.log(data.username)
console.log(vm.username)

// 1、数据驱动视图
data.username = 'ren'
console.log(data.username)
console.log(vm.username)

# 4、全局 set ✨

# ① 问题描述:

  前面我们看到了,vue 会自动对我们的 data 数据进行处理(数据代理、数据拦截等)。

  但是如果我们的数据没有在 data 中定义,而且通过后续追加的话,我们发现这些追加的新数据并没有实现响应式 🤔。

<div id="app">
  <p>{{username}}</p>
  <button @click="addAge">追加age数据</button>
</div>

<script>
  let vm = new Vue({
    el: '#app',
    data: {
      username: 'lencamo'
    },
    methods: {
      addAge() {
        // vm._data.age = 21
        this._data.age = 21
      }
    }
  })

  console.log(vm)
</script>

# ② vm.$set

  对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

注意

  上面的那个问题,是无法解决的,官方已经有详细说明 (opens new window)

注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

<div id="app">
  <p>{{person.username}}</p>
  <button @click="addAge">追加age数据</button>
  <p>{{person.age}}</p>
  <p>{{person.password}}</p>
</div>

<script>
  let vm = new Vue({
    el: '#app',
    data: {
      person: {
        username: 'lencamo'
      }
    },
    methods: {
      addAge() {
        // 方式1
        // Vue.set(this._data.person, 'age', 21)
        // 简写
        Vue.set(this.person, 'age', 21)

        // 方式2
        // this.$set(this._data.person, 'password', '123456')
        // 简写
        this.$set(this.person, 'password', '123456')
      }
    }
  })

  console.log(vm)
</script>

# 三、数据监测 🎈

提示

  但是,在 vue2 的响应式中,还是存在一些问题的:

  • 比如 vue2 不能检测数组和对象的变化。

  这些问题,vue2 也给我们提供了解决方案:

对象 数组
Vue.set(object, propertyName, value) Vue.set(vm.items, indexOfItem, newValue)
Vue.delete(object, propertyName) 经过 vue 包裹的 Array 数组方法

# 1、监测对象

注意

Vue 无法检测 property 的添加或移除

  Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

即:Vue 不允许动态添加根级别的响应式 property。

  但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

  有时你可能需要为已有对象赋值多个新 property,要保持响应式,必须创建一个新的对象。

let vm = new Vue({
  el: '#app',
  data: {
    person: {
      name: 'ren',
      age: 20
    }
  },
  methods: {
    changeName() {
      // 1、新增属性
      // 无效
      // this.person.sex = '男'
      // 有效
      this.$set(this.person, 'sex', '男')

      // 2、删除属性
      // 无效
      // delete this.person.age
      // 有效
      this.$delete(this.person, 'age')

      // 3、拓展(新增对个)
      // 无效
      // this.person = Object.assign(this.person, { a: 1, b: 2 })
      // 有效
      this.person = Object.assign({}, this.person, { a: 1, b: 2 })
    }
  }
})

# 2、监测数组

注意

Vue 不能检测以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

# ① 官方方案

方式 1:使用 vue 包裹后的 Array 数组方法操作

官网:数组更新检测 (opens new window)

方式 2:使用前面讲的

官网:对于数组 (opens new window)

# ② 代码实操

数组数据下是没有 getter 和 setter 的,转而是使用经过 vue 包裹的 Array 数组方法。

原因分析:

代码演示:

<script>
  let vm = new Vue({
    el: '#app',
    data: {
      list: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' }
      ]
    },
    methods: {
      changeName() {
        // 无效
        // this.list[0] = { id: 1, name: '李明' }

        // 有效
        // 方式1
        this.list.splice(0, 1, { id: 4, name: '李明' })
        // 方式2
        // this.$set(this.list, 0, { id: 4, name: '李明' })
      }
    }
  })

  console.log(vm._data.list.splice === Array.prototype.splice) // false
</script>
更新于 : 8/7/2024, 2:16:31 PM