一、数据劫持
数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。 比较典型的是 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__属性,值为this
if (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 = args
break
case "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__属性,值为this
if (Array.isArray(value)) { // 如果是数组,需要单独处理
// 将该数组的原型改变为arrayMethods
Object.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.value
this.value = this.getter(this.target)
// 执行回调,this指向为目标对象
this.callback.call(this.target, this.value, oldValue)
}
}
// 获取对象的属性值,exp是表达式,如:a.b.c
function 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__属性,值为this
value.__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)
三、完整代码
// observe
function 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__属性
}
// defineReactive
function 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 = newVal
dep.notify() // 执行收集的依赖中的回调
}
})
}
// Observer类
class Observer {
constructor(value) {
def(value, "__ob__", this, false) // 为数据创建__ob__属性,值为this
value.__ob__.dep = new Dep // 在__ob__上新增一个dep属性,便于数组的依赖执行
if (Array.isArray(value)) { // 如果是数组,需要单独处理
// 将该数组的原型改变为arrayMethods
Object.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 = args
break
case "splice":
insert = args.slice(2)
}
ob.observeArray(insert) // 新增的值也需要observe判断是否需要监听
ob.dep.notify() // 执行数组上收集的依赖中的回调
return res // 将原方法的返回值作为当前改写方法的返回值
}, false)
})
// Dep
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()
});
}
}
// 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.value
this.value = this.getter(this.target)
// 执行回调,this指向为目标对象
this.callback.call(this.target, this.value, oldValue)
}
}
// 获取对象的属性值,exp是表达式,如:a.b.c
function 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)