复习vue响应式原理

64 阅读5分钟

Object.defineProperty

  • Object.defineProperty 添加属性
         let p = {
            name: 'slc',
            age: 16
        }
        // 给p添加一个新的属性sex
        Object.defineProperty(p, 'sex', {
            value: '男宝宝',
            configurable: true, // 控制属性是否能被删除,默认false
            writable: true, // 控制属性是否能被修改,默认false
            enumerable: true // 控制添加的值是否可以被枚举,默认false
        })
        console.log(p);// {name: 'slc', age: 16, sec: '男宝宝'}
        for (const key in p) {
            console.log(key); //name, age
        }
  • Object.defineProperty getter和setter
 let p = {
            name: 'slc',
            age: 16
        }
        let x = 15
        Object.defineProperty(p, 'age', {
            // 当age被读取,get函数(getter)就被调用,并且返回值就是age的值
            get() {
                console.log('我被读取');
                return x
            },
            // 当age被修改,set函数(setter)就被调用,并且能接受到修改的值
            set(val) {
                console.log('我被读取');
                return x
            },
        })
  • 什么是数据代理 通过一个对象代理对另一个对象属性的操作(读和写)
  • vue中的数据代理

image.png

我们看到vue实例对象身上有这两个属性,但是一开始的状态是…(三个点),这三个点其实就表示的是该数据是通过Object.defineProperty()来创建的;当然在下面也可以看到name和address这两个数据的getter和setter。

  • 在data中定义name 和 address的时候有如下步骤:

在data中定义name 和 address的时候,vue实例对象在其自身添加了一个_data属性(可以理解为就是data),用来存储data中定义的name 和 address; 此时页面只能是通过_data.name的形式才能访问,但是上述代码并不是这样的,是直接访问vue实例对象中的name和address,有意思的来了; 这时候vue会在该vue实例对象上开始加东西(会将data中定义的数据都加到vue实例对象中),首先会先加一个name属性,name的值则是通过getter来读取_data中name的值;如果修改vue实例对象中name的值,则通过setter映射到_data中的name来进行修改; 同理address也是如此,这种通过getter 和 setter将_data中的数据放到vue实例对象中即为数据代理。(目的就是想让开发者编码相对比较方便一些)

原文链接:blog.csdn.net/weixin_4511…

VUE2.x中的数据响应式原理

我的demo源码地址

  • 定义对象,导入入口函数并调用
// 定义一个被赋予响应式的对象
let a = {
    b: 'NIHAO',
    c: [1, 2],
    d: {
        e: '',
        f: {
            g: undefined
        }
    },
    h: 0
}
// 入口
observe(a)
  • observe入口函数
import Observer from "./Observer"; // Observer类
export default function observe(data) {
    // || data === null   后面的中断,是我自己加的,因为当值为null的时候,typeof也是object,会导致报错,先放着看看
    if (typeof data != "object" || data === null) return
    let ob
    if (typeof data.__ob__ !== 'undefined') {
    // 没搞懂的地方
        ob = data.__ob__
    } else {
        ob = new Observer(data)
    }
    return observe
}
  • Observer类
import defineReactive from "./defineReactive";
import { def } from "./utils";
import { arrayMethods } from "./array";
export default class Observer {
    // 这个类的作用就是把一个普通的对象,变成一个被object.defineProperty 监听的对象
    constructor(obj) {
        def(obj, '__ob__', this, false) // 工具函数  第四个参数,不可枚举, 将这个对象,添加一个不可枚举的__ob__,值就是当前这个Observer类
        this.obj = obj
        if (Array.isArray(obj)) {
            // 判断是否是数组,如果是,进行方法的重写
            obj.__proto__ = arrayMethods
        } else {
            this.walk()
        }
    }
    walk() {
        // 将传进来的对象,每一个属性也进行object.defineProperty
        // 例如 {a:1,b:2},  对象中的a,b值也是需要响应式监听的
        Object.keys(this.obj).forEach(item => defineReactive(this.obj, item, this.obj[item]))
    }
}

defineReactive方法, 主要是给每一个传进来的对象,赋予被getter和setter的能力,并且在被getter时,收集依赖,并且在setter时通知这些依赖(调用回调函数,在vue中可能是去diff,渲染dom)

import Dep from "./Dep";
import observe from "./observe";
export default function defineReactive(data, key, val = data[key]) {
    //    在这里是继续递归对象的val,并不是递归自己,在函数层面,,也是多个类组合进行递归
    //    顺序是observe =》 Observer =》 defineReactive => 回到observe
    observe(val)
    let dep = new Dep() // 创建一个实例,供下面depend使用

    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        // 什么是依赖:也就是被watch的地方
        // 我们在getter中,组件访问这些数据的时候,我们帮他收集起来(要加以判断,并不是无脑收集)(在getter中收集依赖)
        // 当这些数据被改变setter,我们就去通知这些依赖
        // 把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个observer的实例成员中都有一个Dep的实例;
        // watcher是一个中介,数据发生变化时通过watcher中转,通知组件

        get() {
            // 在getter中收集依赖(也就是收集watch的实例,也就是收集被观察的对象)对应的就是Dep类中的depend方法
            // 在这个方法里可以看到,并不是每次getter都会被收集,而是在vue中有watch的才收集
            dep.depend()
            console.log(`访问${key}属性`);
            return val
        },
        set(newVal) {

            if (newVal === val) return
                // console.log(`${key}属性被修改为${newVal}`);
            dep.notify() // 在setter中通知依赖
            val = newVal
            observe(val)

        }
    })
}

Dep类,主要定义一个subs数组,用来存放依赖,也就是watch的实例,notify就是通知watch,被你watch的数据改变了

export default class Dep {
    constructor() {
        // 先定义一个subs,用来存放watch的实例(依赖)(被watch的数据)
        // 因为我们在vue中,可能多个数据被watch,这个就是用来放watch的
        this.subs = []
    }
    depend() {
        // console.log('无条件搜集依赖');
        // Dep.target 是watch实例,也只有数据被watch的项,才会被收集
        if (Dep.target) {
            console.log('有条件搜集依赖');
            this.subs.push(Dep.target)

        }
        // console.log(this.subs);
    }
    notify() {
        this.subs.forEach((s) => {
            // s 是每一个watch实例,update是实例中的方法
            s.update()
        })
    }
}

Watch类 只有被以下方式调用,才能被watch监听。响应式基础框架和这个无关,只是为了更好的使用vue中watch方法

demo中的调用方式是
new Watch(a, 'b', (val, oldVal) => {
    // 这边就是观察数据变化后的回调,在vue中可能是diff dom
    console.log(`a.b 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`)
})
vue中调用
watch:{
 a(newVal,oldVal){...}
}
import Dep from "./Dep";

export default class Watch {
    constructor(data, expression, cb) {
        // data: 数据(实例)对象,如obj
        // expression:表达式,如b.c,根据data和expression就可以获取watcher依赖的数据
        // cb:依赖变化时触发的回调
        this.data = data
        this.expression = expression
        this.cb = cb
            // 初始化watcher实例时订阅数据
        this.value = this.get()
    }
    get() {
        Dep.target = this
        const value = parsePath(this.data, this.expression)
            // console.log(value, '这个是Watch中的get方法的返回值');
        return value
    }
    update() {
        // 当收到数据变化的消息时执行该方法,从而调用cb
        const oldValue = this.value
            // setTimeout(() => {
        this.value = parsePath(this.data, this.expression)
        this.cb.call(this.data, this.value, oldValue)
            // }, 1000);
            // this.value, oldValue 对应vue watch中参数的两个值

    }
}

function parsePath(obj, expression) {
    const segments = expression.split('.')
    for (let key of segments) {
        if (!obj) return
        obj = obj[key]
    }
    return obj
}

工具函数以及数组重写

// 数组重写
import { def } from "./utils";
const methodsNeedChange = [
    'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'
]
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
methodsNeedChange.forEach(arrayMethodsName => {
    const original = arrayPrototype[arrayMethodsName]
    def(arrayMethods, arrayMethodsName, function(params) {
        console.log('方法' + arrayMethodsName + '被调用');
        return '数组'
    }, false)
})
// 工具
export const def = function(obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        enumerable,
        configurable: true,
        writable: true,
        value
    })
}