一、数据劫持

数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。 比较典型的是 Object.defineProperty () 和 ES2015 中新增的 Proxy 对象。

1. 对象数据劫持

defineProperty介绍

Vue2中的数据劫持主要通过Object.defineProperty(obj, prop, descriptor)方法的get和set方法

defineProperty的第三个参数是一个对象,其中包括的属性有:

  1. value 值,默认为 undefined。
  2. enumerable 是否可以被枚举,默认false。
  3. writable 是否可写,默认false。为true时value才能被赋值运算符(en-US)改变。
  4. configurable 描述值是否可以配置或删除,默认为false。只有为true时,属性才能被删除,或其描述值(比如:writable,enumerable等)才能被改变。
  5. get 方法,当访问该属性,会调用该方法,该函数的返回值会被用作属性的值。
  6. set 方法,当属性值被改变时,会调用该方法,该方法接受一个参数,该参数是被赋予的新值。

    1. let obj = {}
    2. let value ; // 使用闭包,让get和set的变量周转,以便能成功更改数据
    3. Object.defineProperty(obj,"a",{
    4. // value:'1212',
    5. writable:true, //是否可写
    6. enumerable:true, // 是否可以被枚举
    7. configurable:true // 是否可配置
    8. get(){ // 访问属性就会被触发get方法,value,writable和get不能一起使用
    9. console.log("访问obj的a属性")
    10. return value // 返回值为变量的值
    11. },
    12. set(val){
    13. console.log("你试图改变obj的a属性",val)
    14. value = val
    15. }
    16. })

    数据劫持思路:

  7. 定义observe方法,检测对象是否是object,是否已经被监听,未被监听则在该对象上新增Observer实例

  8. 在Observe类中,循环遍历对象或数组的key,为每个key调用数据劫持方法defineReactive
  9. 在defineReactive方法中,封装defineProperty,检测数据是否变化,每个val还需再调用observe方法

vue2的响应式原理 - 图1

observe方法

判断数据是否是对象,是否被监听

  1. import Observer from "./Observer"; // 引入Observer类
  2. export default function(value){
  3. if (typeof value != "object") return // 如果数据不是对象,则不做处理
  4. let ob;
  5. if (value.__ob__){ // 如果数据存在__ob__,则表示已经被监听了
  6. ob = value.__ob__
  7. }else{
  8. ob = new Observer(value) // 未被监听,则实例化Observer
  9. }
  10. return ob // 返回数据的__ob__属性
  11. }

Observe类

将Observe的实例,挂载到数据的ob属性上,ob属性不可被枚举

  1. import { def } from "./utils"
  2. import defineReactive from "./defineReactive"
  3. export default class Observer {
  4. constructor(value) {
  5. def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为this
  6. if (Array.isArray(value)) { // 如果是数组,需要单独处理
  7. // 下面数组处理会讲到
  8. } else {
  9. this.walk(value)
  10. }
  11. }
  12. walk(value) { // 遍历key,为每个key都添加数据劫持
  13. for (let k in value) {
  14. defineReactive(value, k)
  15. }
  16. }
  17. }
  1. export function def(value,key,val,enumerable,configurable=true){
  2. Object.defineProperty(value,key,{
  3. value:val,
  4. enumerable,
  5. configurable, // 默认可配置
  6. })
  7. }

defineReactive

为对象添加数据劫持方法

  1. import observe from "./observe"
  2. export default function defineReactive(data, key, val) {
  3. if (arguments.length == 2) {
  4. val = data[key] // val是闭包
  5. }
  6. let obCh = observe(val) // 值需要在调用observe,判断是否是对象
  7. Object.defineProperty(data, key, {
  8. enumerable: true,
  9. configurable: true,
  10. get() {
  11. console.log(`你试图访问obj${key}属性`)
  12. return val
  13. },
  14. set(newVal) {
  15. console.log(`你试图改变obj${key}属性`)
  16. observe(newVal) // 新值也需要observe判断是否需要监听
  17. val = newVal
  18. }
  19. })
  20. }

2. 重写数组的7个方法

在vue2中,数组的响应式是通过重写数组的7个方法实现的,因此在vue2中不能用下标的方式改变数组

思路:

  1. 定义一个arrayMethods对象,其原型指向性Array.prototype
  2. arrayMethods对象上重写7个数组方法
  3. 将数组数据的原型指向arrayMethods

vue2的响应式原理 - 图2

定义arrayMethods

  1. import { def } from "./utils" // 该方法在上面有写,这里就不贴了
  2. // 需要重写的方法
  3. let methods = ["push", "shift", "unshift", "pop", "reverse", "sort", "splice"]
  4. const proto = Array.prototype // 将数组原型先存起来
  5. export const arrayMethods = Object.create(proto) // arrayMethods原型指向Array.prototype
  6. // 重写数组方法
  7. methods.forEach(methodName => {
  8. let origin = proto[methodName] // 暂存数组的原方法
  9. // 重新定义数组的方法
  10. def(arrayMethods, methodName, function () {
  11. console.log("调用了数组的方法")
  12. let ob = this.__ob__ // 此时数组上已有__ob__了,后面可以调用Observe实例对应的方法
  13. let res = origin.apply(this, arguments) //执行数组的原方法
  14. // 处理特殊方法:push,unshift,splice,这几个方法会新增值
  15. let insert = [], args = [...arguments]
  16. switch (methodName) {
  17. case "push":
  18. case "unshift":
  19. insert = args
  20. break
  21. case "splice":
  22. insert = args.slice(2)
  23. }
  24. ob.observeArray(insert) // 新增的值也需要observe判断是否需要监听
  25. return res // 将原方法的返回值作为当前改写方法的返回值
  26. }, false)
  27. })

在Observer类中新增方法

  1. import { def } from "./utils"
  2. import defineReactive from "./defineReactive"
  3. import { arrayMethods } from "./array"
  4. import observe from "./observe"
  5. export default class Observer {
  6. constructor(value) {
  7. def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为this
  8. if (Array.isArray(value)) { // 如果是数组,需要单独处理
  9. // 将该数组的原型改变为arrayMethods
  10. Object.setPrototypeOf(value, arrayMethods)
  11. this.observeArray(value)
  12. } else {
  13. this.walk(value)
  14. }
  15. }
  16. walk(value) { // 遍历key,为每个key都添加数据劫持
  17. for (let k in value) {
  18. defineReactive(value, k)
  19. }
  20. }
  21. observeArray(arr) {
  22. // 遍历数组的每一个值,用observe判断是否需要监听
  23. for (let i = 0, l = arr.length; i < l; i++) {
  24. observe(arr[i])
  25. }
  26. }
  27. }

二、依赖收集

依赖收集涉及到两个类,Watcher和Dep,Watcher就是依赖,Dep用于收集依赖。 依赖收集与执行的时机:

  1. 在get中收集依赖,也就是访问数据时,收集依赖
  2. 在set中触发依赖,数据改变时触发依赖的update方法,执行回调(这个回调可以是更新视图等)

例子:淘宝某个商品(需要监听的数据)暂时无货,某用户A定制了有货提醒(Watcher的一个实例),以便有货时下单(也就是watcher实例中的回调函数),商家就收集了多个用户的提醒(用Dep收集了多个Watcher实例),商品有货时,就提醒每个用户有货了,用户收到信息后下单或者加入购物车(循环dep中的多个watcher,调用它的回调方法)。

触发流程:

vue2的响应式原理 - 图3

1. Watcher

  1. import Dep from "./Dep"
  2. export default class Watcher {
  3. constructor(obj, expression, callback) {
  4. this.target = obj // 目标对象
  5. this.callback = callback // 保存回调函数
  6. this.getter = parsePath(expression) // 解析表达式并得到this.getter函数
  7. this.value = this.get()
  8. }
  9. get() {
  10. Dep.target = this // 将Watcher的实例存放在全局,也可以存在Window上
  11. // 获取对象的属性值,此时会触发目标对象上的getter(收集依赖)
  12. let value = this.getter(this.target)
  13. Dep.target = null // 收集完成依赖后,将全局的this置为空
  14. // 如果获取的值是数组,则部分深拷贝该值
  15. // 解决update方法中数组的oldValue和newValue一致的问题
  16. if (Array.isArray(value)) value = [...value]
  17. return value
  18. }
  19. update() {
  20. // 获取新值与旧值
  21. let oldValue = this.value
  22. this.value = this.getter(this.target)
  23. // 执行回调,this指向为目标对象
  24. this.callback.call(this.target, this.value, oldValue)
  25. }
  26. }
  27. // 获取对象的属性值,exp是表达式,如:a.b.c
  28. function parsePath(exp) {
  29. let exps = exp.split(".")
  30. return (obj) => {
  31. exps.forEach(i => {
  32. obj = obj[i]
  33. });
  34. return obj
  35. }
  36. }

2. Dep

  1. export default class Dep {
  2. constructor() {
  3. this.subs = [] // 用于收集依赖
  4. }
  5. depend() {
  6. if (Dep.target) {
  7. console.log("收集依赖watcher")
  8. this.subs.push(Dep.target)
  9. }
  10. }
  11. notify() {
  12. this.subs.forEach(sub => {
  13. console.log("执行watcher的回调函数")
  14. sub.update()
  15. });
  16. }
  17. }

3. 对应方法中实例化Dep 收集依赖

Observer类

实例化Observer类时

  1. export default class Observer {
  2. constructor(value) {
  3. def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为this
  4. value.__ob__.dep = new Dep // 在__ob__上新增一个dep属性,便于数组的依赖执行
  5. ......
  6. }

defineReactive

封装的数据劫持方法上

  1. import Dep from "./Dep"
  2. import observe from "./observe"
  3. export default function defineReactive(data, key, val) {
  4. ......
  5. let obCh = observe(val) // 值需要在调用observe,判断是否是对象
  6. let dep = new Dep() // 创建Dep实例,收集依赖
  7. Object.defineProperty(data, key, {
  8. ......
  9. get() {
  10. dep.depend() // 收集依赖
  11. // 数组收集依赖
  12. if (obCh) {
  13. obCh.dep.depend()
  14. }
  15. ......
  16. },
  17. set(newVal) {
  18. dep.notify() // 执行收集的依赖中的回调

arrayMethods

重写数组的方法上

  1. ......
  2. methods.forEach(methodName => {
  3. ......
  4. def(arrayMethods, methodName, function () {
  5. let ob = this.__ob__ // 此时数组上已有__ob__了,后面可以调用Observe实例对应的方法
  6. ......
  7. ob.dep.notify() // 执行数组上收集的依赖中的回调
  8. }, false)

三、完整代码

  1. // observe
  2. function observe(value){
  3. if (typeof value != "object") return // 如果数据不是对象,则不做处理
  4. let ob;
  5. if (value.__ob__){ // 如果数据存在__ob__,则表示已经被监听了
  6. ob = value.__ob__
  7. }else{
  8. ob = new Observer(value) // 未被监听,则实例化Observer
  9. }
  10. return ob // 返回数据的__ob__属性
  11. }
  12. // defineReactive
  13. function defineReactive(data, key, val) {
  14. if (arguments.length == 2) {
  15. val = data[key] // val是闭包
  16. }
  17. let obCh = observe(val) // 值需要在调用observe,判断是否是对象
  18. let dep = new Dep() // 创建Dep实例,收集依赖
  19. Object.defineProperty(data, key, {
  20. enumerable: true,
  21. configurable: true,
  22. get() {
  23. console.log(`你试图访问obj${key}属性`)
  24. dep.depend() // 收集依赖
  25. // 数组收集依赖
  26. if (obCh) {
  27. obCh.dep.depend()
  28. }
  29. return val
  30. },
  31. set(newVal) {
  32. console.log(`你试图改变obj${key}属性`)
  33. observe(newVal) // 新值也需要observe判断是否需要监听
  34. val = newVal
  35. dep.notify() // 执行收集的依赖中的回调
  36. }
  37. })
  38. }
  39. // Observer类
  40. class Observer {
  41. constructor(value) {
  42. def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为this
  43. value.__ob__.dep = new Dep // 在__ob__上新增一个dep属性,便于数组的依赖执行
  44. if (Array.isArray(value)) { // 如果是数组,需要单独处理
  45. // 将该数组的原型改变为arrayMethods
  46. Object.setPrototypeOf(value, arrayMethods)
  47. this.observeArray(value)
  48. } else {
  49. this.walk(value)
  50. }
  51. }
  52. walk(value) { // 遍历key,为每个key都添加数据劫持
  53. for (let k in value) {
  54. defineReactive(value, k)
  55. }
  56. }
  57. observeArray(arr) {
  58. // 遍历数组的每一个值,用observe判断是否需要监听
  59. for (let i = 0, l = arr.length; i < l; i++) {
  60. observe(arr[i])
  61. }
  62. }
  63. }
  64. // def工具函数
  65. function def(value,key,val,enumerable,configurable=true){
  66. Object.defineProperty(value,key,{
  67. value:val,
  68. enumerable,
  69. configurable, // 默认可配置
  70. })
  71. }
  72. // array重写方法
  73. import { def } from "./utils"
  74. // 需要重写的方法
  75. let methods = ["push", "shift", "unshift", "pop", "reverse", "sort", "splice"]
  76. const proto = Array.prototype // 将数组原型先存起来
  77. const arrayMethods = Object.create(proto) // arrayMethods的原型指向Array.prototype
  78. // 重写数组方法
  79. methods.forEach(methodName => {
  80. let origin = proto[methodName] // 暂存数组的原方法
  81. // 重新定义数组的方法
  82. def(arrayMethods, methodName, function () {
  83. console.log("调用了数组的方法")
  84. let ob = this.__ob__ // 此时数组上已有__ob__了,后面可以调用Observe实例对应的方法
  85. let res = origin.apply(this, arguments) //执行数组的原方法
  86. // 处理特殊方法:push,unshift,splice,这几个方法会新增值
  87. let insert = [], args = [...arguments]
  88. switch (methodName) {
  89. case "push":
  90. case "unshift":
  91. insert = args
  92. break
  93. case "splice":
  94. insert = args.slice(2)
  95. }
  96. ob.observeArray(insert) // 新增的值也需要observe判断是否需要监听
  97. ob.dep.notify() // 执行数组上收集的依赖中的回调
  98. return res // 将原方法的返回值作为当前改写方法的返回值
  99. }, false)
  100. })
  101. // Dep
  102. class Dep {
  103. constructor() {
  104. this.subs = [] // 用于收集依赖
  105. }
  106. depend() {
  107. if (Dep.target) {
  108. console.log("收集依赖watcher")
  109. this.subs.push(Dep.target)
  110. }
  111. }
  112. notify() {
  113. this.subs.forEach(sub => {
  114. console.log("执行watcher的回调函数")
  115. sub.update()
  116. });
  117. }
  118. }
  119. // Watcher类
  120. class Watcher {
  121. constructor(obj, expression, callback) {
  122. this.target = obj // 目标对象
  123. this.callback = callback // 保存回调函数
  124. this.getter = parsePath(expression) // 解析表达式并得到this.getter函数
  125. this.value = this.get()
  126. }
  127. get() {
  128. Dep.target = this // 将Watcher的实例存放在全局,也可以存在Window上
  129. // 获取对象的属性值,此时会触发目标对象上的getter(收集依赖)
  130. let value = this.getter(this.target)
  131. Dep.target = null // 收集完成依赖后,将全局的this置为空
  132. // 如果获取的值是数组,则部分深拷贝该值
  133. // 解决update方法中数组的oldValue和newValue一致的问题
  134. if (Array.isArray(value)) value = [...value]
  135. return value
  136. }
  137. // 执行回调
  138. update() {
  139. // 获取新值与旧值
  140. let oldValue = this.value
  141. this.value = this.getter(this.target)
  142. // 执行回调,this指向为目标对象
  143. this.callback.call(this.target, this.value, oldValue)
  144. }
  145. }
  146. // 获取对象的属性值,exp是表达式,如:a.b.c
  147. function parsePath(exp) {
  148. let exps = exp.split(".")
  149. return (obj) => {
  150. exps.forEach(i => {
  151. obj = obj[i]
  152. });
  153. return obj
  154. }
  155. }
  156. // index.js 测试代码
  157. import observe from "./observe"
  158. import Watcher from "./Watcher"
  159. let obj = {
  160. a:{
  161. m:12,
  162. n:{
  163. x:12
  164. }
  165. },
  166. b:"小明",
  167. c:[12,32,12]
  168. }
  169. observe(obj) // 数据劫持
  170. new Watcher(obj,"b",function(newValue,oldValue){
  171. console.log("你监听的数据更新了",newValue,oldValue)
  172. })
  173. // new Watcher(obj,"c",function(newValue,oldValue){
  174. // console.log("数据更新了222",newValue,oldValue)
  175. // })
  176. obj.b = "小红"
  177. // obj.c.push(333)