Vue.js响应式原理

数据驱动

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

  • 数据驱动
  • 数据响应式
  • 双向数据绑定

核心原理分析

  • Vue 2.x版本与Vue 3.x版本的响应式实现有所不同,我们可以进行分别讲解
    • Vue 2.x响应式基于ES5的Object.defineProperty实现
    • Vue 3.x响应式基于ES6的Proxy实现

回顾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响应式原理

  • Vue2.x的数据响应式就是由Object.defineProperty()实现的
    • 设置data之后,遍历所有的属性,转换为getter和setter,从而在数据变化时进行视图更新操作

我们来写写模拟代码:

<!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


改进

  • 操作中只监听了一个属性,多个属性无法处理
  • 无法监听数组变化(Vue里也是同样存在这个问题)
  • 无法处理属性也为对象的情况

处理多个属性的情况

<!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的普及度并不是太高,一切走向都需要根据市场来确定

相关设计模式

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

观察者模式

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

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

  • 核心概念:
    • 观察者Observer
    • 被观察者(观察目标)Subject

      设计的核心点就是设置一个被观察者,设置一个或者多个的观察者,在被观察者中设置一个遍历进行操作
<!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>

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

发布-订阅模式

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

  • 在发布者和订阅者之间添加一个消息中心,所有的消息均通过消息中心管理,而发布者与订阅者不会直接联系,实现了两张的解耦

核心概念:

  • 消息中心Dep
  • 订阅者Subscriber
  • 发布者Publisher


<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:
    • 目标:将data数据注入到Vue实例,便于方法内操作
  • Observer(发布者)
    • 目标:数据劫持,监听数据变化,并在变化时通知Dep
  • Dep(消息中心)
    • 目标:存储订阅者以及管理消息的发送
  • Watcher(订阅者)
    • 目标:当订阅数据变化,进行视图更新
  • Compiler
    • 目标:解析模板中的指令与插值表达式,并替换成相应的数据

Vue类

  • 功能:
    • 接受配置信息
    • 将data的属性转换为Getter、setter,并且注入到Vue实例中
    • *监听data中所有属性的变化,设置成响应式数据
    • *调用解析功能(解析模板内的插值表达式,指令等等)


      1.存储配置选项,2.挂载元素,3.设置数据属性,最后通过proxyData将data属性都设置到vue实例

      n _proxyData (target, data) {
      Object.keys(data).forEach(key => {
      Object.defineProperty(target, key,{
      enumerable: true,
      configurable: true,
      get () {
      return data[key]
      },
      set (newValue) {
      data[key] = newValue
      }
      })
      })
      }

Observer类

  • 功能:
  • 通过数据劫持方式监视data中的属性变化,变化时通知消息中心Dep
  • 需要考虑data的属性也可能为对象,也要转换成响应式数据


Dep类

  • Dep是dependency的简写,含义是“依赖”,指的是Dep用于收集与管理订阅者与发布者之间的依赖关系
  • 功能:
    • *为每个数据收集对应的依赖,存储依赖
    • 添加并存储订阅者
    • 数据变化时,通知所有的观察者


Watcher 类

  • 功能:
    • 实例化Watch时,往dep对象中添加自己
    • 当数据变化触发dep,dep通知所有对应的Watcher实例更新视图


Complier类

  • 功能:
    • 进行编译模板,并解析内部指令与插值表达式
    • 进行页面的首次渲染
    • 数据变化后,重新渲染视图


功能回顾与总结

  • Vue类
    • 把data的属性注入到Vue实例
    • 调用Observer实现数据响应式处理
    • 调用Compiler编译模板
  • Observer
    • 将data的属性转换为Getter/setter
    • 为Dep添加订阅者Watcher
    • 数据变化发送时通知Dep
  • Dep
    • 收集依赖,添加订阅者(Watcher)
    • 通知订阅者
  • Watcher
    • 编译模板时创建订阅者,订阅数据变化
    • 接到Dep通知时,调用Compiler中的模板功能更新视图
  • Compiler
    • 编译模板,解析指令与插值表达式
    • 负责页面首次渲染与数据变化后重新渲染


      功能总结图
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,548评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,178评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,950评论 2 7