vue2 数据响应式原理——数据劫持(对象篇)

前言

本系列查阅顺序:

  1. vue2 数据响应式原理——数据劫持(初始篇)

  2. vue2 数据响应式原理——数据劫持(对象篇)

  3. vue2 数据响应式原理——数据劫持(数组篇)

  4. vue2 数据响应式原理——依赖收集和发布订阅

通过上一篇想必你已经对 Object.defineProperty() 有了一定的理解,这一篇我们就在前面的基础上探讨如何通过 Object.defineProperty() 来对对象,甚至是嵌套的对象进行数据劫持,以便我们能够侦听到对象的变化。

对 Object.defineProperty() 了解之后我们就可以对其进行封装,形成一个可以侦听到对象变化的函数:

数据劫持(对象篇)

defineReactive

function defineReactive(data, key, val) {    Object.defineProperty(data, key, {        enumerable: true,        configurable: true,        // 读取key时触发 getter        get: function() {            return val;        },        //更改key时触发 setter        set: function(newVal) {            if (val === newVal) {                return;            }            val = newVal;        },    });}

复制代码

这里的 defineReactive 用来对 Object.defineProperty 进行封装。从函数名字可以看出,其作用是定义一个响应式数据。也就是在这个函数中进行变化追踪,封装后只需要传入 data 、 key 和 val 就行了。

封装后之后,每当从 data 的 key 中读取数据时, get 函数会被触发;每当 data 中的 key 改变时, set 函数会被触发

这里再看开头尤大大的那句话 在 getter 中收集依赖,在 setter 中触发依赖。

你应该就能明白我们之后需要在 defineReactive 的 get 函数中收集依赖,在 set 函数中触发依赖,也就是在访问对象的属性时收集依赖,在更新对象的属性时触发依赖,什么是依赖,我们之后再说。

这里演示一下 defineReactive 的用法:

function defineReactive(data, key, val) {    Object.defineProperty(data, key, {        //可枚举        enumerable: true,        //可以被配置,比如delete        configurable: true,        // 读取key时触发 getter        get: function() {            console.log(`你试图访问${data}的${key}属性,它的值为:${val}`);            return val;        },        //更改key时触发 setter        set: function(newVal) {            console.log(`你试图改变${data}的${key}属性,它的新值为:${newVal}`);            if (val === newVal) {                return;            }            val = newVal;        },    });}
let obj = {    a: 6,    b: 9,};//将obj变成响应式数据console.log(obj);defineReactive(obj, "a", 6);defineReactive(obj, "b", 9);console.log(obj);
console.log("a", obj.a);console.log("b", obj.b);obj.a = "改变后的a";console.log(obj.a);

复制代码

vue2 数据响应式原理——数据劫持(对象篇)_第1张图片

这样 obj 的 a 和 b 就都变成响应式的了

这里有个有意思的点就是在将 obj 中的属性使用 defineReactive 变成响应式数据前后打印出的 obj 不同,但当它们展开后又是相等的:

vue2 数据响应式原理——数据劫持(对象篇)_第2张图片

但是这里你可能会发现 defineReactive 函数好像只能将对象的第一层数据变成响应式的,如果 obj 是多层嵌套:

let obj = {    a: {        b: {            m: {                n: 66,            },        },    },};

复制代码

这时我们该怎么使用 defineReactive 函数呢?

上面我们通过 defineReactive(obj, "a", 6); 处理了 a 属性,这个 6 是 a 属性的值,但如果 obj 嵌套了, a 就不是一个确定的值了,而是一个对象,此时 defineReactive 的第三个参数怎么传呢?

这样?

defineReactive(obj, "a", {    b: {        m: {            n: 66,        },    },});

复制代码

这样显然是不对的,正确的做法是 不传 !只需稍微更改一下 defineReactive 函数:

function defineReactive(data, key, val) {    if (arguments.length == 2) {        val = data[key];    }    Object.defineProperty(data, key, {      //省略    });}

复制代码

这个时候:

defineReactive(obj, "a");console.log(obj.a.b.m.n);

复制代码

只显示试图访问 a 属性,但我们具体在访问 n 属性,这样数据响应就不够准确了。

为什么呢?

因为 defineReactive 第二个参数我们传的是 a ,即侦听的是 a 而不是 n ,怎么办呢?

这样? defineReactive(obj, "a.b.m.n") ,这 显然扯淡!

为了 递归侦测对象的全部属性 ,即 将对象的每一层属性全都变成响应式数据 我们需要引入 Observer 类和 observe 函数(注意 observer 函数不带 r )

我们之后要用到 es6 的 import 导入语法,需要对现在的文件做一下处理: index.html 在引入 index.js 文件时加上 type="module"



之后 vscode 安装 Live Server ,之后在 index.html 右键 选择 Open with Live Server 打开即可

vue2 数据响应式原理——数据劫持(对象篇)_第3张图片

先提前看一下我们之后进行操作后的文件目录:

vue2 数据响应式原理——数据劫持(对象篇)_第4张图片

介绍一下各文件:

  • index.html 入口文件,测试所用

  • index.js js 主文件,测试所用

  • observe.js observe 函数,整个响应式系统的入口,判断指定的对象身上是否含有代表响应式数据的‘ __ob__ ’属性,如果没有则 new Observer()

  • Observer.js Observer 类,将 new 出的自己这个实例绑定到指定对象的 __ob__ 属性上,并遍历对象的子属性调用 defineReactive

  • defineReactive.js 侦测对象内的指定属性,利用 Object.defineProperty 将其变成响应式数据,并调用 observe 函数形成递归调用。

  • utils.js 工具函数,含有 def 函数:将指定值绑定到指定对象的指定属性上,并可自定义 enumerable 配置

我们先从 index.js 文件开始:

import observe from "./observe.js";let obj = {    a: {        b: {            m: {                n: 66,            },        },    },    b: 99,};
observe(obj);obj.b++;obj.a.b.m.n++;

复制代码

vue2 数据响应式原理——数据劫持(对象篇)_第5张图片

可以看到这里我们引入了 observe.js ,使用 observe(obj) 将 obj 这个对象的各个层级的所有属性都变成了响应式的,所以这里这个 observe() 函数的作用就很明确了,那么我们就继续深入,看一下这个 observe() 函数是怎么做到将一个对象的各个层级的属性变成响应式的。

observe.js

import Observer from "./Observer.js";export default function observe(value) {    //如果value不是对象    if (typeof value !== "object") {        return;    }    let ob;    if (typeof value.__ob__ !== "undefined") {        ob = value.__ob__;    } else {        ob = new Observer(value);    }    return ob;}

复制代码

observe 函数首先在传入的值为 object 对象的情况下,判断了它有没有 __ob__ 这个属性,如果有就赋值给 ob ,如果没有就 new 了一个 Observer 实例赋值给了 ob ,之后 observe 函数将 ob 返回了。

看到这里我们并没有发现 observe 函数是怎样将一个对象的各个层级的属性变成响应式的,但是它 new 了一个 Observer 实例,那么我们就能 大胆猜测 :

  • 将一个对象的各个层级的属性变成响应式的活是 Observer 实例干的

  • 而这个 __ob__ 应该就是 Observer 实例! 。

那 __ob__ 有什么用呢?用处就是可以进行一个响应式数据的 判断依据 ,如果一个数据已经含有 __ob__ 这个属性,那么就不再 new Observer(value) , 避免重复侦测 value 的变化。

至于 observe 最后为什么要返回一个 ob 不用纠结! 不用纠结! 不用纠结!这个之后在研究 数组的响应式处理 和 收集依赖 时可能会用到,现在这里其实不返回也行。

顺着我们的猜测继续深入去看,但是在看 Observer.js 之前我们先看 utils.js :

utils.js

export const def = function(obj, key, value, enumerable) {    Object.defineProperty(obj, key, {        value,        enumerable,        writable: true,        configurable: true,    });};

复制代码

utils.js 就是一个简单的工具文件,它里面导出了一个 def 函数, def 函数通过 defineProperty 将传入的 value 赋值给传入的 obj 的 key 属性,并且可以根据传入的 enumerable 来配置 key 属性是否可以被枚举。

Observer.js

import { def } from "./utils.js";import defineReactive from "./defineReactive.js";export default class Observer {    constructor(value) {            console.log(value);            console.log("我是this", this);            //构造函数中的this不是表示类本身,而是表示实例            //给实例添加了__ob__属性,值是这次new的实例            def(value, "__ob__", this, false);            //Observer类的目的是:将一个正常的object转换为            //每个层级的属性都是响应式的object            this.walk(value);        }        //遍历    walk(value) {        for (let k in value) {            defineReactive(value, k);        }    }}

复制代码

Observer 是一个类,它干了什么呢?

它调用了 utils.js 中的 def 函数,并传入了 value, "__ob__", this, false ,作用就是利用 def 函数将 this 绑定到了 value 的 __ob__ 属性上,并且 __ob__ 这个属性不可被枚举。

绑定的这个 this 指的就是当前的 Observer 这个实例,通过打印我们可以看到:

vue2 数据响应式原理——数据劫持(对象篇)_第6张图片

这就印证了我们在看 observe.js 时的猜测:《 这个 __ob__ 应该就是 Observer 实例 》

并且前面我们也猜测《 将一个对象的各个层级的属性变成响应式的活是 Observer 实例干的 》

当我们看到 Observer 类的 walk 调用了 defineReactive ,我们的猜测就全部印证了,因为我们知道 defineReactive 的作用不就是将一个数据变成响应式的吗?

看到这你应该有了大致的理解,让我们继续看下去:

observe 在 new Observer 类的时候会调用 walk 函数, walk 函数会遍历传来的 value 的 key ,然后调用 defineReactive 将 value 的这些 key 变成响应式的,这就是为什么前面我们通过 observe(obj) 能够将 obj 变成响应式的了。

但是从这里我们还不能发现它们能够将 value 这个 对象参数 的 所有层的属性 都变成响应式的,因为 walk 通过 for in 遍历 value 的属性 只能遍历一层 ,那么怎么办呢?

到这里我们就剩一个我们最熟悉的 defineReactive.js 还没有看,会不会是它里面做了一些什么,能够让咱们分析的这个过程对一个对象的子属性也执行一遍,即 递归调用 ,那么我们就赶紧来看一下这个 defineReactive.js :

defineReactive.js

import observe from "./observe.js";export default function defineReactive(data, key, val) {    if (arguments.length == 2) {        val = data[key];    }    //子元素要进行observe,至此形成了递归调用。(多个函数循环调用)    let childOb = observe(val);    Object.defineProperty(data, key, {        //可枚举        enumerable: true,        //可以被配置,比如delete        configurable: true,        // 读取key时触发 getter        get: function() {            console.log(`你试图访问${data}的${key}属性,它的值为:${val}`);            return val;        },        //更改key时触发 setter        set: function(newVal) {            console.log(`你试图改变${data}的${key}属性,它的新值为:${newVal}`);            if (val === newVal) {                return;            }            val = newVal;            childOb = observe(newVal);        },    });}

复制代码

当我们看到 defineReactive.js 中又引入了 observe ,我们就应该恍然大悟了,除了入口文件 index.js ,我们第一个分析的就是 observe ,分析到最后 defineReactive 又调用了 observe ,这不就是一个 循环调用 吗!

看到 defineReactive 执行了:

let childOb = observe(val);

复制代码

当然现在也可以不让 observe(val) 的返回值赋给 childOb ,我们这里赋值给 childOb 是为了之后能用到,这里不用纠结为什么。

defineReactive 在对指定对象的 key 进行响应式处理的时候先执行了 observe(val) ,这个 val 就是这个 key 对应得的值,即 data[key] ,如果这个值也是一个对象,那么 observe 就会对其再次进行处理, 以此类推,循环调用 ,就将一个对象所有层的属性转换成了响应式数据。

并且在 set 方法中,当设置了一个新值 newVal 后,对这个新值也执行一下 observe ,使新的数据也变成响应式数据。

总结一下

整个过程:

vue2 数据响应式原理——数据劫持(对象篇)_第7张图片

  1. observe(obj) 判断到 obj 不含 __ob__ 这个属性,知道了 obj 还不是响应式数据,所以在 observe 函数内执行了 new Observer(obj) (因为 obj 是传入的参数,所以之后的 value 都是指这个 obj )

  2. new Observer(obj) 将 new 出的这个 Observer 实例通过 def函数 绑定到了 obj 的 __ob__ 不可枚举属性上,然后调用 walk(obj) 遍历 obj 上含有的第一层属性,逐个执行 defineReactive(obj, k) ( k 指遍历到的属性)

  3. defineReactive 将 obj 上的这些属性(上一步遍历到的所有 k )变成了响应式数据, defineReactive(obj, k )内又执行了 observe(obj['k']) 使用 observe 对 k 属性的  再次执行这整个过程,形成一个循环的过程。

自此 obj 本身和它的所有对象属性上都有了这个 __ob__ 属性,后续如果 observe(obj) 再执行就不会再 new Observer(obj) 了。

说到这里后细心的你可能会发现,上面我们所做的操作在对数组的响应式处理中会有一些问题,比如:

let obj = {    c: [1, 2, 3],};observe(obj);obj.c.push(9);

你可能感兴趣的:(计算机,Java,程序员,javascript,前端,vue.js)