Vue2数据响应式原理

539 阅读9分钟

一、数据劫持

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

1. 对象数据劫持

defineProperty介绍

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

defineProperty的第三个参数是一个对象,表示该key的描述,其中包括的属性有:

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

数据劫持思路:

  1. 定义observe方法,检测对象是否是object,是否已经被监听,未被监听则在该对象上新增Observer实例
  2. 在Observe类中,循环遍历对象或数组的key,为每个key调用数据劫持方法defineReactive
  3. 在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中不能用下标的方式改变数组

思路:

  1. 定义一个arrayMethods对象,其原型指向性Array.prototype
  2. arrayMethods对象上重写7个数组方法
  3. 将数组数据的原型指向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用于收集依赖。

依赖收集与执行的时机:

  1. 在get中收集依赖,也就是访问数据时,收集依赖
  2. 在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重写方法
// 需要重写的方法
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)