一、数据劫持
数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。 比较典型的是 Object.defineProperty () 和 ES2015 中新增的 Proxy 对象。
1. 对象数据劫持
defineProperty介绍
Vue2中的数据劫持主要通过Object.defineProperty(obj, prop, descriptor)方法的get和set方法
defineProperty的第三个参数是一个对象,其中包括的属性有:
- value 值,默认为 undefined。
- enumerable 是否可以被枚举,默认false。
- writable 是否可写,默认false。为true时value才能被赋值运算符(en-US)改变。
- configurable 描述值是否可以配置或删除,默认为false。只有为true时,属性才能被删除,或其描述值(比如:writable,enumerable等)才能被改变。
- get 方法,当访问该属性,会调用该方法,该函数的返回值会被用作属性的值。
set 方法,当属性值被改变时,会调用该方法,该方法接受一个参数,该参数是被赋予的新值。
let obj = {}let value ; // 使用闭包,让get和set的变量周转,以便能成功更改数据Object.defineProperty(obj,"a",{// value:'1212',writable:true, //是否可写enumerable:true, // 是否可以被枚举configurable:true // 是否可配置get(){ // 访问属性就会被触发get方法,value,writable和get不能一起使用console.log("访问obj的a属性")return value // 返回值为变量的值},set(val){console.log("你试图改变obj的a属性",val)value = val}})
数据劫持思路:
定义observe方法,检测对象是否是object,是否已经被监听,未被监听则在该对象上新增Observer实例
- 在Observe类中,循环遍历对象或数组的key,为每个key调用数据劫持方法defineReactive
- 在defineReactive方法中,封装defineProperty,检测数据是否变化,每个val还需再调用observe方法
observe方法
判断数据是否是对象,是否被监听
import Observer from "./Observer"; // 引入Observer类export default function(value){if (typeof value != "object") return // 如果数据不是对象,则不做处理let ob;if (value.__ob__){ // 如果数据存在__ob__,则表示已经被监听了ob = value.__ob__}else{ob = new Observer(value) // 未被监听,则实例化Observer}return ob // 返回数据的__ob__属性}
Observe类
将Observe的实例,挂载到数据的ob属性上,ob属性不可被枚举
import { def } from "./utils"import defineReactive from "./defineReactive"export default class Observer {constructor(value) {def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为thisif (Array.isArray(value)) { // 如果是数组,需要单独处理// 下面数组处理会讲到} else {this.walk(value)}}walk(value) { // 遍历key,为每个key都添加数据劫持for (let k in value) {defineReactive(value, k)}}}
export function def(value,key,val,enumerable,configurable=true){Object.defineProperty(value,key,{value:val,enumerable,configurable, // 默认可配置})}
defineReactive
为对象添加数据劫持方法
import observe from "./observe"export default function defineReactive(data, key, val) {if (arguments.length == 2) {val = data[key] // val是闭包}let obCh = observe(val) // 值需要在调用observe,判断是否是对象Object.defineProperty(data, key, {enumerable: true,configurable: true,get() {console.log(`你试图访问obj的${key}属性`)return val},set(newVal) {console.log(`你试图改变obj的${key}属性`)observe(newVal) // 新值也需要observe判断是否需要监听val = newVal}})}
2. 重写数组的7个方法
在vue2中,数组的响应式是通过重写数组的7个方法实现的,因此在vue2中不能用下标的方式改变数组
思路:
- 定义一个arrayMethods对象,其原型指向性Array.prototype
- arrayMethods对象上重写7个数组方法
- 将数组数据的原型指向arrayMethods
定义arrayMethods
import { def } from "./utils" // 该方法在上面有写,这里就不贴了// 需要重写的方法let methods = ["push", "shift", "unshift", "pop", "reverse", "sort", "splice"]const proto = Array.prototype // 将数组原型先存起来export const arrayMethods = Object.create(proto) // arrayMethods原型指向Array.prototype// 重写数组方法methods.forEach(methodName => {let origin = proto[methodName] // 暂存数组的原方法// 重新定义数组的方法def(arrayMethods, methodName, function () {console.log("调用了数组的方法")let ob = this.__ob__ // 此时数组上已有__ob__了,后面可以调用Observe实例对应的方法let res = origin.apply(this, arguments) //执行数组的原方法// 处理特殊方法:push,unshift,splice,这几个方法会新增值let insert = [], args = [...arguments]switch (methodName) {case "push":case "unshift":insert = argsbreakcase "splice":insert = args.slice(2)}ob.observeArray(insert) // 新增的值也需要observe判断是否需要监听return res // 将原方法的返回值作为当前改写方法的返回值}, false)})
在Observer类中新增方法
import { def } from "./utils"import defineReactive from "./defineReactive"import { arrayMethods } from "./array"import observe from "./observe"export default class Observer {constructor(value) {def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为thisif (Array.isArray(value)) { // 如果是数组,需要单独处理// 将该数组的原型改变为arrayMethodsObject.setPrototypeOf(value, arrayMethods)this.observeArray(value)} else {this.walk(value)}}walk(value) { // 遍历key,为每个key都添加数据劫持for (let k in value) {defineReactive(value, k)}}observeArray(arr) {// 遍历数组的每一个值,用observe判断是否需要监听for (let i = 0, l = arr.length; i < l; i++) {observe(arr[i])}}}
二、依赖收集
依赖收集涉及到两个类,Watcher和Dep,Watcher就是依赖,Dep用于收集依赖。 依赖收集与执行的时机:
- 在get中收集依赖,也就是访问数据时,收集依赖
- 在set中触发依赖,数据改变时触发依赖的update方法,执行回调(这个回调可以是更新视图等)
例子:淘宝某个商品(需要监听的数据)暂时无货,某用户A定制了有货提醒(Watcher的一个实例),以便有货时下单(也就是watcher实例中的回调函数),商家就收集了多个用户的提醒(用Dep收集了多个Watcher实例),商品有货时,就提醒每个用户有货了,用户收到信息后下单或者加入购物车(循环dep中的多个watcher,调用它的回调方法)。
触发流程:
1. Watcher
import Dep from "./Dep"export default class Watcher {constructor(obj, expression, callback) {this.target = obj // 目标对象this.callback = callback // 保存回调函数this.getter = parsePath(expression) // 解析表达式并得到this.getter函数this.value = this.get()}get() {Dep.target = this // 将Watcher的实例存放在全局,也可以存在Window上// 获取对象的属性值,此时会触发目标对象上的getter(收集依赖)let value = this.getter(this.target)Dep.target = null // 收集完成依赖后,将全局的this置为空// 如果获取的值是数组,则部分深拷贝该值// 解决update方法中数组的oldValue和newValue一致的问题if (Array.isArray(value)) value = [...value]return value}update() {// 获取新值与旧值let oldValue = this.valuethis.value = this.getter(this.target)// 执行回调,this指向为目标对象this.callback.call(this.target, this.value, oldValue)}}// 获取对象的属性值,exp是表达式,如:a.b.cfunction parsePath(exp) {let exps = exp.split(".")return (obj) => {exps.forEach(i => {obj = obj[i]});return obj}}
2. Dep
export default class Dep {constructor() {this.subs = [] // 用于收集依赖}depend() {if (Dep.target) {console.log("收集依赖watcher")this.subs.push(Dep.target)}}notify() {this.subs.forEach(sub => {console.log("执行watcher的回调函数")sub.update()});}}
3. 对应方法中实例化Dep 收集依赖
Observer类
实例化Observer类时
export default class Observer {constructor(value) {def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为thisvalue.__ob__.dep = new Dep // 在__ob__上新增一个dep属性,便于数组的依赖执行......}
defineReactive
封装的数据劫持方法上
import Dep from "./Dep"import observe from "./observe"export default function defineReactive(data, key, val) {......let obCh = observe(val) // 值需要在调用observe,判断是否是对象let dep = new Dep() // 创建Dep实例,收集依赖Object.defineProperty(data, key, {......get() {dep.depend() // 收集依赖// 数组收集依赖if (obCh) {obCh.dep.depend()}......},set(newVal) {dep.notify() // 执行收集的依赖中的回调
arrayMethods
重写数组的方法上
......methods.forEach(methodName => {......def(arrayMethods, methodName, function () {let ob = this.__ob__ // 此时数组上已有__ob__了,后面可以调用Observe实例对应的方法......ob.dep.notify() // 执行数组上收集的依赖中的回调}, false)
三、完整代码
// observefunction observe(value){if (typeof value != "object") return // 如果数据不是对象,则不做处理let ob;if (value.__ob__){ // 如果数据存在__ob__,则表示已经被监听了ob = value.__ob__}else{ob = new Observer(value) // 未被监听,则实例化Observer}return ob // 返回数据的__ob__属性}// defineReactivefunction defineReactive(data, key, val) {if (arguments.length == 2) {val = data[key] // val是闭包}let obCh = observe(val) // 值需要在调用observe,判断是否是对象let dep = new Dep() // 创建Dep实例,收集依赖Object.defineProperty(data, key, {enumerable: true,configurable: true,get() {console.log(`你试图访问obj的${key}属性`)dep.depend() // 收集依赖// 数组收集依赖if (obCh) {obCh.dep.depend()}return val},set(newVal) {console.log(`你试图改变obj的${key}属性`)observe(newVal) // 新值也需要observe判断是否需要监听val = newValdep.notify() // 执行收集的依赖中的回调}})}// Observer类class Observer {constructor(value) {def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为thisvalue.__ob__.dep = new Dep // 在__ob__上新增一个dep属性,便于数组的依赖执行if (Array.isArray(value)) { // 如果是数组,需要单独处理// 将该数组的原型改变为arrayMethodsObject.setPrototypeOf(value, arrayMethods)this.observeArray(value)} else {this.walk(value)}}walk(value) { // 遍历key,为每个key都添加数据劫持for (let k in value) {defineReactive(value, k)}}observeArray(arr) {// 遍历数组的每一个值,用observe判断是否需要监听for (let i = 0, l = arr.length; i < l; i++) {observe(arr[i])}}}// def工具函数function def(value,key,val,enumerable,configurable=true){Object.defineProperty(value,key,{value:val,enumerable,configurable, // 默认可配置})}// array重写方法import { def } from "./utils"// 需要重写的方法let methods = ["push", "shift", "unshift", "pop", "reverse", "sort", "splice"]const proto = Array.prototype // 将数组原型先存起来const arrayMethods = Object.create(proto) // arrayMethods的原型指向Array.prototype// 重写数组方法methods.forEach(methodName => {let origin = proto[methodName] // 暂存数组的原方法// 重新定义数组的方法def(arrayMethods, methodName, function () {console.log("调用了数组的方法")let ob = this.__ob__ // 此时数组上已有__ob__了,后面可以调用Observe实例对应的方法let res = origin.apply(this, arguments) //执行数组的原方法// 处理特殊方法:push,unshift,splice,这几个方法会新增值let insert = [], args = [...arguments]switch (methodName) {case "push":case "unshift":insert = argsbreakcase "splice":insert = args.slice(2)}ob.observeArray(insert) // 新增的值也需要observe判断是否需要监听ob.dep.notify() // 执行数组上收集的依赖中的回调return res // 将原方法的返回值作为当前改写方法的返回值}, false)})// Depclass Dep {constructor() {this.subs = [] // 用于收集依赖}depend() {if (Dep.target) {console.log("收集依赖watcher")this.subs.push(Dep.target)}}notify() {this.subs.forEach(sub => {console.log("执行watcher的回调函数")sub.update()});}}// Watcher类class Watcher {constructor(obj, expression, callback) {this.target = obj // 目标对象this.callback = callback // 保存回调函数this.getter = parsePath(expression) // 解析表达式并得到this.getter函数this.value = this.get()}get() {Dep.target = this // 将Watcher的实例存放在全局,也可以存在Window上// 获取对象的属性值,此时会触发目标对象上的getter(收集依赖)let value = this.getter(this.target)Dep.target = null // 收集完成依赖后,将全局的this置为空// 如果获取的值是数组,则部分深拷贝该值// 解决update方法中数组的oldValue和newValue一致的问题if (Array.isArray(value)) value = [...value]return value}// 执行回调update() {// 获取新值与旧值let oldValue = this.valuethis.value = this.getter(this.target)// 执行回调,this指向为目标对象this.callback.call(this.target, this.value, oldValue)}}// 获取对象的属性值,exp是表达式,如:a.b.cfunction parsePath(exp) {let exps = exp.split(".")return (obj) => {exps.forEach(i => {obj = obj[i]});return obj}}// index.js 测试代码import observe from "./observe"import Watcher from "./Watcher"let obj = {a:{m:12,n:{x:12}},b:"小明",c:[12,32,12]}observe(obj) // 数据劫持new Watcher(obj,"b",function(newValue,oldValue){console.log("你监听的数据更新了",newValue,oldValue)})// new Watcher(obj,"c",function(newValue,oldValue){// console.log("数据更新了222",newValue,oldValue)// })obj.b = "小红"// obj.c.push(333)
