【Vue2】详解 Vue2 响应式原理

image.png

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

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

image.png

响应式数据的目标就是,当对象本身或者对象属性发生变化时,就会运行一些函数,比如 render 函数。

数据劫持

基础使用:

//数据劫持,数据变化都是由 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)
        }
    });
}

Vue3 中这里使用的是 Proxy 配合 Reflect。

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__属性时,保证这个属性不可被遍历,而创建的一个工具函数(配置部分属性);另一个原因就是我们熟知的数据劫持。

Vue提供的静态方法 Vue.observable(object) 将一个对象的所有属性变为响应式。
组件生命周期中,这件事发生在 beforeCreate 之前,created 之后。

但是此时的响应式(Vue2)不能监听属性的新增和删除,所有就有了 this.$set(obj, 'c', 'xxx')this.$delete(obj, 'b') 方法补充新增和删除的响应式。

对于数组,Vue 重写了一些方法,并使 arr.__proto__ 指向 他自己重写的Array (对象),然后 重写的这个 自己的Array.__proto__ 指向 Array.prototype。所以对于数组,直接下标修改赋值是监听不到的,还是需要使用 $set()

//将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

dep(下面会讲)是怎么知道谁在使用某个响应式数据的捏?也就是如何让响应式数据知道使用了自己的函数是谁捏?为了解决这个问题,就出现了 watcher。(这个watcher 其实就是我们经常使用的 watch 函数!)

obj.a
function get() {
   // dep.depend()
}
render(){
    obj.a   // dep.notify()
}
new Watcher(render)

watcher 会设置一个全局变量,让全局变量记录当前负责执行的 watcher 等于自己,再去执行函数。函数执行过程中,如果发生依赖记录dep.depend(),那么 dep 就会把这个全局变量记录下来,即有一个 watcher 用到了我这个属性。

每一个 vue 实例都至少对应一个 watcher ,用于记录该组件的 render 函数。

watcher 首先会把render 函数运行一次以收集依赖,那些 render 中使用到的响应式数据就会记录这个 watcher。数据变化时,dep 会通知该 watcher ,而 watcher 将重新运行 render 函数,让界面重新渲染同时记录当前依赖。

每个Watcher实例订阅一个或者多个数据,这些数据也被称为wacther的依赖;当依赖(dep)发生变化,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

现提出一个问题:当一个响应式数据在 render、watch1、watch2 中都使用到了,那么我们如何知道当数据改变时,需要运行哪些函数呢?这时就需要 dep。一句话来说就是属性变化时要做什么事,需要 dep 来解决。

vue 会为响应式对象的每个属性、对象本身、数组本身创建一个 dep 实例,这个 dep 实例做两件事:

  1. 收集依赖(depend):谁在用我
  2. 派发更新(notify):我改变,要通知那些使用到我的对象、数组、属性等
    在这里插入图片描述
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()
        }
    }
}

Scheduler

想一个问题,如果一个交给了 watcher 的函数,里面用到了属性 a, b, c, d (修改数值),那么这四个属性都会收集依赖,并触发四次更新。

所以这样就会导致函数运行频繁,而实际上watcher 收到派发更新的通知后,不会立即执行对应的函数,而是把自己交给调度器 Scheduler。

调度器维护一个执行队列,该队列同一个 watcher 仅会存在一次,队列中的watcher不是立即执行,它会通过一个叫做 nextTick 的工具方法,把这些需要执行的 watcher 放入到事件循环的微队列中, nextTick 的具体做法是通过Promise 完成的。(当然,nextTick 通过 this.$nextTick 被暴露给开发者)

也就是说,响应式数据变化,render 函数的执行时异步的,并且在微任务队列中。

总结一下

在这里插入图片描述

在这里插入图片描述

image.png

  • 41
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Vue2的响应式原理是基于Object.defineProperty来实现的。它通过在Vue实例创建过程中,遍历data对象的属性,将每个属性都转换为getter和setter函数。当我们访问data中的属性时,会触发getter函数,而当我们修改data中的属性值时,会触发setter函数。这样就实现了数据的响应式更新。 具体来说,Vue2会在初始化阶段遍历data对象的属性,并使用Object.defineProperty方法将每个属性转化为getter和setter。当我们访问一个data属性时,getter函数会被调用,用来收集依赖,将当前的观察者对象添加到该属性的依赖列表中。而当我们修改一个data属性时,setter函数会被调用,触发依赖列表中所有观察者对象的更新方法。 这种响应式原理的优点是能够实时追踪数据的变化,当数据发生改变时,相关的视图会自动更新。而缺点是只能对已经存在的属性进行响应式处理,无法处理动态添加的属性,也无法处理数组的变化。此外,Object.defineProperty的浏览器兼容性也有一定的限制。 总结起来,Vue2的响应式原理通过Object.defineProperty实现,能够实现数据的双向绑定和自动更新,但存在一些限制和兼容性问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [vue2.0响应式原理及缺点](https://blog.csdn.net/weixin_52400118/article/details/109597468)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [Vue响应式原理详解](https://download.csdn.net/download/weixin_38574410/13203853)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小秀_heo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值