欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

解释 Vue2 响应原理

最编程 2024-03-22 19:43:35
...

image.png

数据变化,视图会自动变化。

侵入式就是调用一些api使得当数据变化的时候,视图会跟着变化。然后Vue使用的是非侵入式。

image.png

数据劫持

基础使用:

//数据劫持,数据变化都是由defineProperty 内部的成员控制
Object.defineProperty(data,key, {
    // writable:true,  // writable 会与下面的get和set冲突
    // value:3, // 也不可以同时指定 value 属性
    enumerable:true, // 是否可以被枚举
    configurable: true, // 是否可以被删除
    // 访问 key 属性的时候会触发get
    get () {
        //return 值就是 key属性的值
    },
    // 修改 key 属性的时候会触发set
    set(){
        // 将newValue赋值给临时变量,然后get再将临时变量返回

    }
})

封装:

function defineReactive(data, key, val){
    //数据劫持,数据变化都是由defineProperty 内部的成员控制
    Object.defineProperty(data,key, {
        // writable:true,  // writable 会与下面的get和set冲突
        // value:3, // 也不可以同时指定 value 属性
        enumerable:true, // 是否可以被枚举
        configurable: true, // 是否可以被删除
        // 访问 key 属性的时候会触发get
        get () {
            console.log('访问属性'+key)
            //return 值就是 key属性的值
            return val
        },
        // 修改 key 属性的时候会触发set
        set(newVal){
            console.log('修改属性'+key)
            // 将newValue赋值给临时变量,然后get再将临时变量返回
            if (newVal === val) {
                return
            }
            val = newVal;
        }
    })
}
let obj = {}
defineReactive(obj,'a',1)
console.log(obj.a)
obj.a++
console.log(obj.a)

image.png

递归侦听对象全部属性

image.png

接下来就要上强度了(????呜呜呜~)。

image.png

defineReactive.js

这个方法像上面讲的一样,就是用来做数据劫持的。用observe方法监视子节点的变化(数据劫持),就是为子节点设置__ob__对象属性,而这个属性其实就是Observer类的实例对象。

import observe from "./observe.js";

export default function defineReactive(data, key, val) {
    console.log('我是defineReactive',key)
    // 当有两个参数参入,val就是data子元素(下一层嵌套)
    if (arguments.length === 2) {
        val = data[key]
    }
    // 子元素进行 observe,形成递归函数和类
    let childOb = observe(val)
    //数据劫持,数据变化都是由defineProperty 内部的成员控制
    Object.defineProperty(data, key, {
        // writable:true,  // writable 会与下面的get和set冲突
        // value:3, // 也不可以同时指定 value 属性
        enumerable: true, // 是否可以被枚举
        configurable: true, // 是否可以被删除
        // 访问 key 属性的时候会触发get
        get() {
            console.log('访问属性' + key)
            //return 值就是 key属性的值
            return val
        },
        // 修改 key 属性的时候会触发set
        set(newVal) {
            console.log('修改属性' + key)
            // 将newValue赋值给临时变量,然后get再将临时变量返回
            if (newVal === val) {
                return
            }
            val = newVal;
            // 当设置了新值,这个新值也要被observe
            childOb = observe(newVal)
        }
    });
}

index.js

import observe from "./observe.js";
let obj = {
    a: {
        b: {
            c:2
        },
        d: 1
    },
    g: 4
}

observe(obj)
obj.a.b = 20

observe.js

为传入的对象配置监听(劫持)属性,其实是配置一个告诉其他人这个是被数据劫持的对象属性的属性。而这个配置标志是__ob__,并在内部创建Observer实例。

observe -> new Observer -> defineReactive -> observe -> …

import Observer from "./Observer.js";

export default function observe(value) {
    if (typeof value !== 'object') return
    let ob
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob
}

Observer.js

这是设置响应式的观察者,他主要观察所有的对象属性,为他们添加响应式。

可以发现有一个def方法,她存在的一个原因是为了添加上面的__ob__属性时,保证这个属性不可被遍历,而创建的一个工具函数(配置部分属性);另一个原因就是我们熟知的数据劫持。

//将Object内部的每一个属性都进行数据劫持,使他们都具有响应式

import {def} from "./utils.js";
import defineReactive from "./defineReactive.js";

export default class Observer {
    constructor(value) {
        // 给实例添加__ob__属性,值是 new 的一个实例(构造函数中的this不是表示类本身,而是表示实例本身)
        def(value, '__ob__', this, false)
        console.log('我是Observer构造器', value)
        this.walk(value)
    }

    //遍历每一个成员属性,将每一个属性都设置为defineReactive
    walk(value) {
        for (let valueKey in value) {
            defineReactive(value, valueKey)
        }
    }
}

utils.js

配置不可遍历属性和绑定数据劫持。

//对单个需要进行数据劫持的数据 配置部分属性
export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    })
};

image.png

数组的响应式处理

Vue2对数组的七个方法进行了重写。所有我们自己定义的数组将不再直接调用原型Array.prototype上的这些方法。而是走一条新的原型链:arr__proto__-> arrayMethods__proto__-> Array.prototype

对数组的操作只能通过该响应式处理,直接修改操作都会使其丢失响应式。

array.js

import {def} from "./utils";

//要改写的七个方法
const methodsNeedChange = [
    'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
]

const arrayPrototype = Array.prototype
// arrayMethods.__proto__ = Array.prototype
// 创建新的原型,重写Array 的七个方法
export const arrayMethods = Object.create(arrayPrototype)

methodsNeedChange.forEach(methodName => {
    // 备份原来的方法
    const original = arrayPrototype[methodName]
    //定义新的的方法
    def(arrayMethods, methodName, function () {
        // 把类数组对象变为数组
        const args = [...arguments]
        // 将数组身上的__ob__属性取出来(数组肯定是对象内部的非第一层的某一层的属性,所以__ob__一定已经被添加到了该数组身上)
        const ob = this.__ob__
        // push unshift splice 会插入新项,现在需要把这些新项也变成响应式的
        let inserted = []
        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                //slice(下标,数量,插入的新项)
                inserted = args.slice(2)
                break
        }
        //判断有没有要插入的新项, 将新项变成响应式
        if (inserted) {
            ob.observeArray(inserted)
        }
        // 恢复原来功能
        const res = original.apply(this, arguments)
        return res
    }, false);
})

Observer.js

//将Object内部的每一个属性都进行数据劫持,使他们都具有响应式

import {def} from "./utils.js";
import defineReactive from "./defineReactive.js";
import {arrayMethods} from "./array.js";
import observe from "./observe";

export default class Observer {
    constructor(value) {
        // 给实例添加__ob__属性,值是 new 的一个实例(构造函数中的this不是表示类本身,而是表示实例本身)
        def(value, '__ob__', this, false)
        console.log('我是Observer构造器', value)
        // 判断是数组还是对象
        if (Array.isArray(value)) {
            // 如果是数组,将数组的原型指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods)
            // 对数组进行 observe
            this.observeArray(value)
        } else {
            this.walk(value);
        }
    }

    //遍历每一个成员属性,将每一个属性都设置为defineReactive
    walk(value) {
        for (let valueKey in value) {
            defineReactive(value, valueKey)
        }
    }
    //数组遍历,使每一个方法都被劫持
    observeArray(){
        //l = arr.length防止数组的长度在遍历图中变化
        for (let i = 0,l = arr.length; i < l; i++) {
            observe(arr[i])
        }
    }
}

依赖收集

image.png

image.png

从这里往后又到了另一个难点????

Watcher.js

每个Watcher实例订阅一个或者多个数据,这些数据也被称为wacther的依赖;当依赖发生变化,Watcher实例会接收到数据发生变化这条消息,之后会执行一个回调函数来实现某些功能。

import Dep from "./Dep";

let uid = 0
export default class Watcher {
    constructor(target, expression, callback) {
        // target: 数据对象 obj
        // expression:表达式,如b.c,根据target和expression就可以获取watcher依赖的数据
        // callback:依赖变化时触发的回调
        console.log('Watcher类的构造器')
        this.id = uid++
        this.target = target
        this.getter = parsePath(expression)
        this.callback = callback
        // 订阅数据
        this.value = this.get()
    }

    update() {
        this.value = parsePath(this.target, this.getter) // 对存储的数据进行更新
        this.callback()
    }

    get() {
        //进入依赖收集 让全局的Dep.target设置为Watch本身,那么就是进入了依赖收集
        Dep.target = this
        const obj = this.target
        let value
        try {
            value = this.getter(obj)
        } finally {
            Dep.target = null
        }
        return value
    }

    run() {
        this.getAndInvoke(this.callback)
    }

    getAndInvoke(cb) {
        const value = this.get()
        if (value !== this.value || typeof value === 'object') {
            const oldValue = this.value
            this.value = value
            cb.call(this.target, value, oldValue)
        }
    }
}

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

那么,Watcher 如何与前面劫持的数据发生关系呢?我们进行了如下操作:

  1. 有一个数组来存储watcher实例
  2. watcher实例需要订阅数据,也就是收集依赖
  3. watcher的依赖发生变化时,触发watcher的回调函数,也就是派发更新。

然后,就可以引入dep的概念了。

当我们实例化watcher时,会执行get方法,get方法的作用就是获取自己依赖的数据,也就是触发了getter。我们把watcher收集起来那不就是依赖的收集吗?那么这些依赖收集到哪里呢?答案是收集到dep中。

所以此时应该是watcher收集了依赖,而dep收集了watcher

当我们实例化watcher 的时候,getter读取不到这个实例。解决方法是将watcher放到全局,比如window.target上。

defineReactive.js

import observe from "./observe.js";
import Dep from "./Dep";

export default function defineReactive(data, key, val) {
    const dep = new Dep()
    console.log('我是defineReactive',key)
    if (arguments.length === 2) {
        val = data[key]
    }
    // 子元素进行 observe,形成递归函数和类
    let childOb = observe(val)
    //数据劫持,数据变化都是由defineProperty 内部的成员控制
    Object.defineProperty(data, key, {
        // writable:true,  // writable 会与下面的get和set冲突
        // value:3, // 也不可以同时指定 value 属性
        enumerable: true, // 是否可以被枚举
        configurable: true, // 是否可以被删除
        // 访问 key 属性的时候会触发get
        get() {
            console.log('访问属性' + key)
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                }
            }
            //return 值就是 key属性的值
            return val;
        },
        // 修改 key 属性的时候会触发set
        set(newVal) {
            console.log('修改属性' + key)
            // 将newValue赋值给临时变量,然后get再将临时变量返回
            if (newVal === val) {
                return
            }
            val = newVal;
            // 当设置了新值,这个新值也要被observe
            childOb = observe(newVal)
            //发布订阅模式,通知 dep
            dep.notify()
        }
    });
}

说了这么多,是时候将dep放出来了。

Dep.js

let uid = 0
export default class Dep {
    constructor() {
        console.log('Dep类的构造器')
        this.id = uid++
        //用数组存储自己的订阅者 -> Watcher实例
        this.subs = []
    }
    //添加订阅
    addSub(sub) {
        this.subs.push(sub)
    }
    //添加依赖
    depend(){
        //Dep.target是我们自己指定的一个全局位置
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    }
    //通知更新
    notify() {
        console.log('触发notify')
        //拷贝一份
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

总结一下

image.png

推荐阅读