Vue.js响应式原理

2021-04-17  本文已影响0人  amanohina

数据驱动

在我们学习Vue.js的过程中,我们经常看到三个概念

核心原理分析

回顾defineProperty

我们先定义一个对象

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

在defineProperty中,第一个参数为需要进行操作的对象,第二个参数为属性,第三个为对应的操作

    Object.defineProperty(obj, 'gender', {
      // 值
      value: '男',
      // 是否可写
      writable: true,
      // 控制是否可以枚举(遍历
      enumerable: true,
      // 本次定义之后,再次进行重新配置
      configurable: true
    })

    Object.defineProperty(obj, 'gender', {
      enumerable: false
    })

解释一下代码:
赋予值:value
是否可以编辑:writable(这条属性默认值为false,表示只可以读,不可以写入)


如图,打印结果并未发生改变

是否可以枚举(遍历):enumerable(这条属性默认值也为false)

    for (var k in obj) {
      console.log(k, obj[k])
    }
在遍历中,如果设置的是false值,我们是无法读取到他的值的

在本次定义之后,可否再次进行重新配置:configurable:默认值为false,true时可以进行再次的配置

false值,再次进行配置会报错
进行属性操作时,可以通过getter,setter实现,访问器和设置器,在访问和设置时进行相应的功能设置
value,writable和get,set无法共存,逻辑冲突
getter指的是:
当我们访问对象的属性时,会执行这个函数
    Object.defineProperty(obj, 'gender', {
      get () {
        // 甚至可以进行额外的操作
        console.log('任意需要的自定义操作')
        return '男'
      },
属性访问时也可以设置一个事件

setter指的是:
当我们设置某个属性时触发的函数

      set (newValue) {
        console.log('新的值是',newValue)
        this.gender = newValue
      }

这样写是一个误区,设置时触发setter,就会造成递归


造成溢出

解决办法:
通过第三方数据,来存取数据

    var genderValue = '男'
    Object.defineProperty(obj, 'gender', {
      get () {
        console.log('任意需要的自定义操作')
        return genderValue
      },
      set (newValue) {
        console.log('新的值是',newValue)
        genderValue = newValue
      }
    })
解决问题

模拟Vue2响应式原理

我们来写写模拟代码:

<!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>
    // 声明一个对象用于进行数据存储
    let data = {
      msg: 'hello'
    }
    // 模拟一个vue实例
    let vm = {}
    // 通过数据劫持的方式,将data的属性设置给getter与setter,并且设置给vm
    Object.defineProperty(vm, 'msg', {
      // 可遍历
      enumerable: true,
      // 可配置
      configurable: true,
      // get方法
      get () {
        console.log('访问数据')
        return data.msg
      },
      // set方法
      set (newValue) {
        // 更新数据
        data.msg = newValue
        // 数据更改,更新视图中DOM元素内容
        document.querySelector('#app').textContent = data.msg
      }
    })
  </script>
</body>
</html>

解释一下代码,vm的作用就是通过数据劫持将data中的数据设置给get与set,并且设置给vm,最后更改的还是data


改进

处理多个属性的情况

<!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>
    // 声明一个对象用于进行数据存储
    let data = {
      msg1: 'hello',
      msg2: 'world'
    }
    // 模拟一个vue实例
    let vm = {}
    Object.keys(data).forEach(key => {
      // 通过数据劫持的方式,将data的属性设置给getter与setter,并且设置给vm
      Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        // get方法
        get () {
          console.log('访问数据')
          return data[key]
        },
        // set方法
        set (newValue) {
          // 更新数据
          data[key] = newValue
          // 数据更改,更新视图中DOM元素内容
          document.querySelector('#app').textContent = data[key]
        }
      })
    })
  </script>
</body>
</html>

这里我们使用到了Object.keys()方法,该方法可以返回一个由内部参数对象的自身可枚举属性构成的一个数组,然后我们再将其进行forEach遍历,得到每一个属性,然后进行多个属性的处理,详细逻辑可以通过代码看的一清二楚

检测数组的方法

对数组的操作是无法实现响应式数据实现的
Vue通过特定的方法处理可以解决这种问题

   const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    // 存储处理结果的对象,准备替换到数组数组实例的原型指针 _proto_
    const customProto = {}
        // 确保原始功能可以使用,this为数组实例
        const result = Array.prototype[method].apply(this, arguments)
        // 进行其他自定义功能设置,比如,更新视图
        document.querySelector('#app').textContent = this
        return result
    // 为了避免数组实例无法再使用其他的数组方法
    customProto.__proto__ = Array.prototype
<!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>
    // 声明一个对象用于进行数据存储
    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 () {
        // 确保原始功能可以使用,this为数组实例
        const result = Array.prototype[method].apply(this, arguments)
        
        // 进行其他自定义功能设置,比如,更新视图
        document.querySelector('#app').textContent = this
        return result
      }
    })

    Object.keys(data).forEach(key => {
      // 检测是否为数组,是的话单独处理
      if (Array.isArray(data[key])) {
        // 将当前数组实例的__proto__更换为customProto就行了
        data[key].__proto__ = customProto
      }

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

改进:封装与递归

使用立即执行函数,全部包裹起来,如果对象内部还含有对象的话就进行递归处理,很简单的逻辑:

<!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],
      obj: {
        name: 'jack',
        age: 18
      }
    }
    // 模拟 Vue 实例的对象
    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 => {
          // 检测是否为数组
          if (Array.isArray(data[key])) {
            // 将当前数组实例的 __proto__ 更换为 customProto 即可
            data[key].__proto__ = customProto
          } else if (typeof data[key] === 'object' && data[key] !== null) {
            // 检测是否为对象,如果为对象,进行递归操作
            vm[key] = {}
            createReactive(data[key], vm[key])
            return
          }

          // 通过数据劫持的方式,将 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]
            }
          })
        })
      }

    })()
  
    createReactive(data, vm)
  </script>
</body>
</html>

这就是Vue2版本的响应式原理分析

回顾Proxy

ES6提供的一个功能,对一个对象提供代理操作

  <script>
    const data = {
      msg1: '内容',
      arr: [1, 2, 3],
      obj: {
        name: 'willam',
        age: 19
      }
    }
    
    const P = new Proxy(data, {
      get (target, property, receiver) {
        console.log(target, property, receiver)
        return target[property]
      },
      set (target, property, value, receiver) {
        console.log(target, property, value, receiver)
        target[property] = value
      }
    })
  </script>

通过代理,访问P也就是访问了data的代理,同样的数据,get方法中,target参数表示原数据data,property表示访问的哪条属性,receiver表示通过代理之后的数据
set方法中新添了一个value参数,表示当前设置的数值
我们来通过控制台打印一探究竟


Vue3响应式原理

与2版本的区别为数据响应式是Proxy实现的,其他相同,接下来进行演示

<!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>
    const data = {
      msg1: '内容',
      arr: [1, 2, 3],
      content: 'world',
      obj: {
        name: 'willam',
        age: 19
      }
    }

    const vm = new Proxy(data, {
      get (target, key) {
        return target[key]
      },
      set (target, key, newValue) {
        // 数据更新
        target[key] = newValue
        // 视图更新
        document.querySelector('#app').textContent = target[key]
      }
    })
  </script>
</body>
</html>

对深层监控啊,属性监控啊,遍历啊都不需要在Vue3进行操作了,通过Proxy代理可以轻松解决,但是由于ES6的Proxy方法兼容性不是那么的好,所以市面上Vue3的普及度并不是太高,一切走向都需要根据市场来确定

相关设计模式

设计模式:针对软件设计中普遍存在的各种问题所提出的解决方案

观察者模式

指的是在对象间定义一个一对多(被观察者与多个观察者)的关联,当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新

就像是超市有一堆顾客,超市出了促销活动,会通知顾客(观察者),又因为当前是否想要购物,进行不同的选择行动

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // 被观察者(观察目标)
    // 1.需要能够添加观察者
    // 2.通知所有观察者的功能
    class Subject {
      constructor () {
        // 存储所有的观察者
        this.observers = []
      }
      // 添加观察者功能
      addObserver (observer) {
        // 检测传入的参数是否为观察者实例
        if (observer && observer.update) {
          this.observers.push(observer)
        }
      }
      // 通知所有的观察者
      notify () {
        // 调用观察者列表中的每个观察者的更新方法
        this.observers.forEach(observer => {
          observer.update()
        })
      }
    }
    // 观察者
    // 1.被观察者发生状态变化时,做一些对应的操作“更新”
    class Observer {
      update () {
        console.log('事件发生了,进行一个相应的处理...')
      }
    }

    // 功能测试
    const subject = new Subject()
    const ob1 = new Observer()
    const ob2 = new Observer()

    // 将观察者添加给要观察的观察目标
    subject.addObserver(ob1)
    subject.addObserver(ob2)

    // 通知观察者进行操作(某些具体的场景下)
    subject.notify()
  </script>
</body>
</html>

通过观察者模式为不同的数据设置不同的观察者,监视被观察者的情况,通过特定的方法进行更新操作等等

发布-订阅模式

可以认为是为观察者模式的解耦的进阶版本,特点是:

核心概念:

<body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
  <script>
    // 创建了一个Vue实例(消息中心)
    const eventBus = new Vue()

    // 注册事件(设置订阅者)
    eventBus.$on('dataChange', () => {
      console.log('事件处理功能1')
    })

    eventBus.$on('dataChange', () => {
      console.log('事件处理功能2')
    })

    // 触发事件(设置发布者)
    eventBus.$emit('dataChange')
  </script>
</body>

设计模式小结

响应式原理模拟

整体分析

要模拟Vue实现响应式数据,首先我们需要观察一下Vue实例的结构,分析要实现哪些属性和功能


Vue类

Observer类

Dep类

Watcher 类

Complier类

功能回顾与总结

上一篇 下一篇

猜你喜欢

热点阅读