image.png

前言

Vue 最显著的一个特点就是数据响应式,通过改变 ModelJavaScript 对象) 中数据的值,可以自动触发相应试图的更新。这也是 MVVM 框架的好处。

那如何实现数据响应式呢?

vue2.0 时,尤大使用了 ES5 Object.defineProperty 方法实现。

Object.defineProperty

**Object.defineProperty()** 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

就是说使用了 **Object.defineProperty()**方法,允许添加和修改该对象的属性,这些属性的值可以被改变,也可以被删除。

  1. // 使用方法
  2. Object.defineProperty(obj, prop, description)
  3. // obj: 要在其上定义属性的对象
  4. // prop: 要定义或者修改的属性的名称
  5. // description: 将被定义或者修改的属性描述符
  6. // 属性描述符(数据描述符和存取描述符(get、set))

响应式代码实现

基础版(对象只有一层属性)

  1. // 数据响应式
  2. // 更新试图方法
  3. function updateView () {
  4. console.log('试图更新')
  5. }
  6. function observer (target) {
  7. // 如果不是对象,则返回
  8. if (typeof target !== 'object' || target === null) {
  9. return target
  10. }
  11. // 遍历对象枚举
  12. for (let key in target) {
  13. // 重新定义属性
  14. defineProperty(target, key, target[key])
  15. }
  16. }
  17. // 重新定义属性
  18. function defineProperty (target, key, value) {
  19. Object.defineProperty(target, key, {
  20. get () {
  21. return value
  22. },
  23. set (newValue) {
  24. if (newValue !== value) {
  25. updateView()
  26. value = newValue
  27. }
  28. }
  29. })
  30. }
  31. // 定义对象
  32. let data = {
  33. name: 'zhoujiawei'
  34. }
  35. // 添加侦听器
  36. observer(data)
  37. // 改变属性值
  38. data.name = 'new allen'
  39. console.log(data.name) // new allen

image.png

对象属性多层嵌套

比如 data = { ``name: 'zhoujiawei', msg: { age: 12 }``}

  1. // 数据响应式
  2. // 更新试图方法
  3. function updateView () {
  4. console.log('试图更新')
  5. }
  6. function observer (target) {
  7. // 如果不是对象,则返回
  8. if (typeof target !== 'object' || target === null) {
  9. return target
  10. }
  11. // 遍历对象枚举
  12. for (let key in target) {
  13. // 重新定义属性
  14. defineProperty(target, key, target[key])
  15. }
  16. }
  17. // 重新定义属性
  18. function defineProperty (target, key, value) {
  19. // ************ 递归遍历 *************
  20. observer(value)
  21. // *********************************
  22. Object.defineProperty(target, key, {
  23. get () {
  24. return value
  25. },
  26. set (newValue) {
  27. if (newValue !== value) {
  28. updateView()
  29. value = newValue
  30. }
  31. }
  32. })
  33. }
  34. // 定义对象
  35. let data = {
  36. name: 'zhoujiawei',
  37. msg: {
  38. age: 12
  39. }
  40. }
  41. // 添加侦听器
  42. observer(data)
  43. // 改变属性值
  44. // data.name = 'new allen'
  45. data.msg.age = 13
  46. console.log(data.msg.age) // 14

image.png

属性特殊赋值操作

❌错误代码

  1. // 数据响应式
  2. // 更新试图方法
  3. function updateView () {
  4. console.log('试图更新')
  5. }
  6. function observer (target) {
  7. // 如果不是对象,则返回
  8. if (typeof target !== 'object' || target === null) {
  9. return target
  10. }
  11. // 遍历对象枚举
  12. for (let key in target) {
  13. // 重新定义属性
  14. defineProperty(target, key, target[key])
  15. }
  16. }
  17. // 重新定义属性
  18. function defineProperty (target, key, value) {
  19. // ************ 递归遍历 *************
  20. observer(value)
  21. // *********************************
  22. Object.defineProperty(target, key, {
  23. get () {
  24. return value
  25. },
  26. set (newValue) {
  27. if (newValue !== value) {
  28. updateView()
  29. value = newValue
  30. }
  31. }
  32. })
  33. }
  34. // 定义对象
  35. let data = {
  36. name: 'zhoujiawei',
  37. msg: {
  38. age: 12
  39. }
  40. }
  41. // 添加侦听器
  42. observer(data)
  43. // *********** 改变属性值 ***********
  44. data.msg = {
  45. sex: 'man'
  46. }
  47. data.msg.sex = 'woman'
  48. // ********************************
  49. console.log(data.msg.sex) // man(这边应该是 woman 才是对的,但实际是 man)

这时候需要在 set 中添加 observer 方法。

  1. Object.defineProperty(target, key, {
  2. get () {
  3. return value
  4. },
  5. set (newValue) {
  6. if (newValue !== value) {
  7. // *********************
  8. observer(newValue)
  9. // *********************
  10. updateView()
  11. value = newValue
  12. }
  13. }
  14. })

image.png

改变数组

我们知道在 Vue2.0 中,改变数组比如:myArray[2] = 2。给数组 myArray 添加一个值是不会触发视图更新的。但是使用一些能改变原数组的数组对象方法,则可以更新视图。

列举一下:push、pop、shift、unshift、reverse、sort、splice

Vue2.0 中通过对这几个数组方法的重写,进行函数劫持,在调用原函数之前,触发视图更新。

  1. // 数据响应式
  2. let ArrayProperty = Array.prototype; // Array的原型
  3. let proto = Object.create(ArrayProperty) // 继承
  4. ;['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach((method) => {
  5. proto[method] = function () {
  6. // 更新视图
  7. updateView()
  8. ArrayProperty[method].call(this, ...arguments)
  9. }
  10. })
  11. // 更新试图方法
  12. function updateView () {
  13. console.log('试图更新')
  14. }
  15. function observer (target) {
  16. // 如果不是对象,则返回
  17. if (typeof target !== 'object' || target === null) {
  18. return target
  19. }
  20. if (Array.isArray(target)) {
  21. // 如果是数组,将 target 的链指向 proto
  22. target.__proto__ = proto
  23. }
  24. // 遍历对象枚举
  25. for (let key in target) {
  26. // 重新定义属性
  27. defineProperty(target, key, target[key])
  28. }
  29. }
  30. // 重新定义属性
  31. function defineProperty (target, key, value) {
  32. // ************ 递归遍历 *************
  33. // 考虑对象属性多层嵌套
  34. observer(value)
  35. // *********************************
  36. Object.defineProperty(target, key, {
  37. get () {
  38. return value
  39. },
  40. set (newValue) {
  41. if (newValue !== value) {
  42. // 考虑属性特殊赋值操作
  43. observer(newValue)
  44. updateView()
  45. value = newValue
  46. }
  47. }
  48. })
  49. }
  50. // 定义对象
  51. let data = {
  52. name: 'zhoujiawei',
  53. msg: {
  54. age: 12
  55. }
  56. }
  57. // 添加侦听器
  58. observer(data)
  59. // *********** 改变属性值 ***********
  60. data.msg = {
  61. sex: 'man',
  62. log: [1, 2, 3]
  63. } // 更新第一次
  64. data.msg.log.push(4) // 更新第二次
  65. // ********************************

image.png

完整代码

  1. // 数据响应式
  2. let ArrayProperty = Array.prototype; // Array的原型
  3. let proto = Object.create(ArrayProperty) // 继承
  4. ;['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach((method) => {
  5. proto[method] = function () {
  6. // 更新视图
  7. updateView()
  8. ArrayProperty[method].call(this, ...arguments)
  9. }
  10. })
  11. // 更新试图方法
  12. function updateView () {
  13. console.log('试图更新')
  14. }
  15. function observer (target) {
  16. // 如果不是对象,则返回
  17. if (typeof target !== 'object' || target === null) {
  18. return target
  19. }
  20. if (Array.isArray(target)) {
  21. // 如果是数组,将 target 的链指向 proto
  22. target.__proto__ = proto
  23. }
  24. // 遍历对象枚举
  25. for (let key in target) {
  26. // 重新定义属性
  27. defineProperty(target, key, target[key])
  28. }
  29. }
  30. // 重新定义属性
  31. function defineProperty (target, key, value) {
  32. // ************ 递归遍历 *************
  33. // 考虑对象属性多层嵌套
  34. observer(value)
  35. // *********************************
  36. Object.defineProperty(target, key, {
  37. get () {
  38. return value
  39. },
  40. set (newValue) {
  41. if (newValue !== value) {
  42. // 考虑属性特殊赋值操作
  43. observer(newValue)
  44. updateView()
  45. value = newValue
  46. }
  47. }
  48. })
  49. }
  50. // 定义对象
  51. let data = {
  52. name: 'zhoujiawei',
  53. msg: {
  54. age: 12
  55. }
  56. }
  57. // 添加侦听器
  58. observer(data)
  59. // *********** 改变属性值 ***********
  60. data.msg = {
  61. sex: 'man',
  62. log: [1, 2, 3]
  63. } // 更新第一次
  64. data.msg.log.push(4) // 更新第二次
  65. // ********************************

Vue2.0 响应式缺点

  1. Vue 实例化后新增的属性,不会被监听。但可以使用 Vue.set(data, 'a', 1) 设置新属性(对象不存在的属性不能被拦截)。
  2. Object.defineProperty的一个缺陷是无法监听数组变化(数组的7个改变原数组本身能被监听)。同样可以使用 Vue.set 来设置数组项。
  3. 默认递归,有性能问题。

注:Vue3.0 将会使用 Proxy 来实现数据绑定响应式,将解决上述的缺点。