vue2响应式原理分析

vue响应式原理分析之vue2


前言

vue学到后面需要对响应式源码原理进行剖析,里面蕴含着观察者模式和发布-订阅的模式,对提高编程思维有很多的帮助哦


vue2.x版本和vue3.x版本的响应式实现有所不同,我们将分别进行详解。首先我们来了解这两个基本事实

(1)vue2.x版本响应式基于ES5的Object.defineProperty实现

(2)Vue3.x版本响应式基于ES6(es2015)的Proxy实现

接下来我们先认识Object.defineProperty基本用法

一、知识储备:Object.defineProperty

1.obj对象自定义设置

我们定义一个obj对象

var obj = {
  name: 'william',
  age: 18
}

如果我们想给obj对象增加一个值,通常是使用【obj.name2=“张三”】方式新建一个键值对。如果我们希望对新键值对进行一些配置,比如说不可修改、不可枚举、不可重新定义,这又该怎么办呢?

此时需要用到Object.defineProperty

var obj = {
  name: 'william',
  age: 18
}

// Object.defineProperty() 操作演示与回顾
Object.defineProperty(obj, 'gender', {
  value: '男', // 设置键gender对应的值
  writable: true, // 设置键gender的值可修改
  enumerable: true, // 设置gender可遍历的,比如可以被for遍历
  configurable: true // 可以配置的。即可以重新定义gender,就可以执行下面的代码了
 })

当设置enumerable(可枚举)、configurable(可重新配置)都为false时,那么以下两种方法都不可使用:

以下代码,【第1种方法】:重新对obj.gender进行配置不生效,因为一开始就设置了configurable: false不可重新配置;【第2种方法】:遍历obj不生效,因为设置了enumerable: false不可枚举

var obj = {
  name: 'william',
  age: 18
}

// Object.defineProperty() 操作演示与回顾
Object.defineProperty(obj, 'gender', {
  value: '男', // 设置键gender对应的值
  writable: false, // 设置键gender的值不可修改
  enumerable: false, // 设置gender不可枚举,比如不可以被for遍历
  configurable: false // 设置gender不可以重新配置的。 
 })
// 【第1种方法】:重新配置obj.gender可以枚举
Object.defineProperty(obj, 'gender', {
  enumerable: false // 重新设置gender不可枚举。前提是上面代码设置了configurable: true,因此这个重新给gender进行配置无效。
})
// 【第2种方法】:遍历obj不生效,因为设置了enumerable: false不可枚举
for (var k in obj) {
  console.log(k, obj[k])
} 

接下来我们再来看看如何对obj对象进行自定义操作

1.obj对象自定义操作

对象自定义操作可以通过get和set实现,接下来我们通过代码去理解他们的用法

var obj = {
  name: 'william',
  age: 18
}

var genderValue = '男'
Object.defineProperty(obj, 'gender', {
  get () { // 获取gender值时候触发
    console.log('任意获取时需要的自定义操作')
    return genderValue
  },
  set (newValue) { // 设置gender值时候触发,newValue就是设置的值
    console.log('任意设置时需要的自定义操作')
    genderValue = newValue
  }
})

当我们尝试获取obj.gender时,就会触发get方法
在这里插入图片描述
当我们尝试给obj对象进行设置值时,就会触发set方法
在这里插入图片描述
由此不难看出,get方法在获取对象值时触发,set方法在给obj设置时触发,这个意味着我们可以在获取/设置元素时候自定义一些操作逻辑。

另外,get()除了obj.gender获取值时候触发,通过点击查看值就会触发,因为这样也是一种get值

请添加图片描述
容易犯的一个细节错误:

var obj = {
  name: 'william',
  age: 18
}

Object.defineProperty(obj, 'gender', {
  get () { // 获取gender值时候触发
    console.log('任意获取时需要的自定义操作')
    return '男'
  },
  set (newValue) { // 设置gender值时候触发
    this.gender = newValue // this指向obj,因为是obj调用gender
  }
})

newValue是设置新值的参数,那么this.gender = newValue又重新设置值,那么又会重新触发set方法,由此进入死循环了。

在这里插入图片描述
认识了Object.defineProperty基本用法后,我们开始写一个简单的vue2响应式原理

二、实践:通过Object.defineProperty写一个简单vue2响应式

1.单个属性数据驱动视图基础效果

代码如下(示例):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始内容</div>
  <script>
    // 声明数据对象,模拟 Vue 实例的 data 属性
    let data = {
      msg: 'hello'
    }
    // 模拟 Vue 实例的对象
    let vm = {}
    
    // 通过数据劫持的方式,将 data 的属性设置为 getter/setter
    Object.defineProperty(vm, 'msg', { // 前两个参数连起来表示为vm对象设置键msg,最后1个参数表示获取/设置msg键时候会触发的操作。
      // 可遍历
      enumerable: true,
      // 可配置
      configurable: true,
      get () {
        console.log('访问了属性')
        return data.msg
      },
      set (newValue) {
        // 更新数据
        data.msg = newValue
        // 数据更改,更新视图中 DOM 元素的内容
        document.querySelector('#app').textContent = data.msg
      }
    })
  </script>
</body>
</html>

我们简单讲一下这个代码实现功能:给vm实例对象注入"msg"键,然后通过(get)访问msg返回的是data内的msg值,(set)设置新的值时候,修改data内的msg值,并且更新到视图。

问题:该版本只是雏形,操作中只监听了一个属性,多个属性无法处理 → 问题解决:下面是处理多个属性方法,修改了某个属性就会更新视图。

2.多个属性数据驱动视图基础效果

代码如下(示例):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">原始内容</div>
    <script>
      // 声明数据对象,模拟 Vue 实例的 data 属性
      let data = {
        msg: "hello",
        ms2: "world",
      };
      // 模拟 Vue 实例的对象
      let vm = {};

      // 遍历被劫持对象的所有属性
      Object.keys(data).forEach((key) => { // Object.keys可以获取所有data对象所有属性值
        // Vue实例背后底层逻辑:通过数据劫持的方式,将 data 的属性设置为 getter/setter
        Object.defineProperty(vm, key, { // key是被劫持对象data的其中一个属性
          // 可遍历
          enumerable: true,
          // 可配置
          configurable: true,
          get() {
            console.log("访问了属性");
            return data[key];
          },
          set(newValue) {
            console.log("更新/修改了属性");
            // 更新数据
            data[key] = newValue;
            // 数据更改,更新视图中 DOM 元素的内容
            document.querySelector("#app").textContent = data[key];
          },
        });
      });
    </script>
  </body>
</html>

多个属性数据驱动视图和单个属性驱动视图是基本一致的,只需要额外关注一下这个方法:Object.keys可以获取所有data对象所有属性值。

另外,需要注意的是【我们给vm新增了1个属性,但是vm的属性,并不是data的】 → 因此,通过访问data是无法访问到新属性的

在这里插入图片描述
在这里插入图片描述
问题又出现:我们无法监听data.arr数组内部数据变化,比如
在这里插入图片描述
这需要更深层次地鉴定数组内部元素变化

3.(deep)更深层次地监听数组内部元素变化

代码如下(示例):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始内容</div>
  <script>
    // 声明数据对象,模拟 Vue 实例的 data 属性
    let data = {
      msg1: 'hello',
      msg2: 'world',
      arr: [1, 2, 3]
    }
    // 模拟 Vue 实例的对象
    let vm = {}

    // --- 添加数组方法支持 ---
    const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

    // 用于存储处理结果的对象,准备替换掉数组实例的原型指针 __proto__
    const customProto = {}

    // 为了避免数组实例无法再使用其他的数组方法
    customProto.__proto__ = Array.prototype

    arrMethodName.forEach(method => {
      customProto[method] = function () { //思想:customProto = { "push" : push方法+自定义更新视图 } → arr实例._proto__ = customProto(__proto__指针:就是往上面找方法) => 开始操作:arr实例.push(100,200) → data['arr']: [1,2,3,100,200]
        // 确保原始功能可以使用(this 为数组实例)
        const result = Array.prototype[method].apply(this, arguments)
        // 进行其他自定义功能设置,上一行代码更新arr后,再用新的this更新视图
        document.querySelector('#app').textContent = this
        // 有的数组操作有返回值比如pop,有的数组操作没有返回值,比如push
        return result
      }
    })

    // 遍历被劫持对象的所有属性
    Object.keys(data).forEach(key => {
      // 检测是否为数组
      if (Array.isArray(data[key])) {
        // 将当前数组实例的 __proto__ 更换为 customProto 即可
        data[key].__proto__ = customProto // __proto__指针:就是往上面找方法
      }

      // 通过数据劫持的方式,将 data 的属性设置为 getter/setter
      Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        get () {
          console.log('访问了属性')
          return data[key]
        },
        set (newValue) {
          // 更新数据
          data[key] = newValue
          // 数据更改,更新视图中 DOM 元素的内容
          document.querySelector('#app').textContent = data[key]
        }
      })
    })

  </script>
</body>
</html>

简单梳理一下代码方法:

为数组方法push、pop等方法增加一个方法,此处只是增加了document.querySelector(‘#app’).textContent = this,其他保持push原始方法不变。→ 然后遍历data,如果是数组就更改指针__proto__指针指向customProto。

如果data数据内部的内部还有对象,那么再继续进行数据劫持即可,因此下面实现封装与递归操作:

4.封装与递归操作。

代码如下(示例):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>vue2响应式原理</title>
  </head>
  <body>
    <div id="app">原始内容</div>
    <script>
      // vuejs程序看到的data数据
      let data = {
        msg1: "hello",
        msg2: "world",
        arr: [1, 2, 3],
        obj: {
          name: "苏苏",
          age: 18,
        },
      };
      // 真正操作的对象,间接操作
      let vm = {};
      // 封装为函数,用于对数据进行响应式处理。在外部函数嵌套了一个内部函数,并且使用了外部函数的变量,由此就会形成一个闭包。
      const createReactive = (function () {
        const arrMethodName = [
          "push",
          "pop",
          "shift",
          "unshift",
          "splice",
          "sort",
          "reverse",
        ];
        const customProto = {};
        customProto.__proto__ = Array.prototype;
        arrMethodName.forEach((method) => {
          customProto[method] = function () {
            const result = Array.prototype[method].apply(this, arguments);
            document.querySelector("#app").textContent = this;
            return result;
          };
        });
        // 需要进行数据劫持的主体功能,也是递归时需要的功能
        return function (data, vm) {
          Object.keys(data).forEach((key) => {
            // 检测是否为数组。typeof数组返回也是object,所以先判断是否为数组;其次,data数据中只有数组or对象,因此排除了数组,那么下一个else if必然是对象。最后,需要注意的是,typeof null返回的也是object,因此需要加上data[key] != null判断
            if (Array.isArray(data[key])) {
              // 将当前数组实例的__proto__更换为customProto即可
              data[key].__proto__ = customProto;
            } else if (typeof data[key] === "object" && data[key] !== null) {
              // 检测是否为对象,如果为对象,进行递归操作。
              vm[key] = {}; // 类似【let vm={}】,在内部继续增加一个实例。★新增一个内存地址,Object.defineProperty操作就是这个内存地址。
              createReactive(data[key], vm[key]);
              return;
            }
            // 通过数据劫持的方式,将data的属性设置为getter/setter
            Object.defineProperty(vm, key, {
              // vm={},key是data每个键
              enumerable: true,
              configurable: true,
              // writable: true, // 配置了这个就无法配置set
              get() {
                document.querySelector("#app").textContent = data[key];
                return data[key];
              },
              set(newValue) {
                data[key] = newValue;
                document.querySelector("#app").textContent = data[key];
              },
            });
          });
        };
      })();
      createReactive(data, vm);
    </script>
  </body>
</html>

总结

vue2响应式原理基于简单的Object.defineProperty方法,由此实现vm实例通过劫持data数据实时更新视图。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值