Vue响应式原理浅析

作为Vue的核心特性之一,响应式可以说是Vue的灵魂,是数据驱动视图的关键所在,通过数据响应式,能够将对JavaScript数据对象的修改作用于视图,驱动视图进行更新,这使得开发变得尤为简单方便

在使用Vue响应式的同时,理解其原理更能帮助我们在开发中得心应手,Vue的响应式原理大致上可以分为数据劫持发布-订阅模式两部分

Vue2.x 数据劫持

在Vue2.x版本中,数据劫持通过 Object.defineProperty方法进行实现,通过对data 对象上的数据进行递归遍历,对每一个属性设置 getter/setter方法,通过getter返回属性值,而setter方法负责监听数据变化,一旦数据发生修改就进行视图的更新

下面我们来简单模拟一下Vue2.x的数据劫持:

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>
    // 模拟data
    const data = {
      name: 'Jerry',
      age: '18',
      height: '178'
    }
    //模拟vue实例
    const Vm = {}

    //遍历data中的每一个属性
    Object.keys(data).forEach(key => {
      //进行数据劫持
      Object.defineProperty(Vm,key,{
        //设置是否可枚举,即是否可以被遍历
        enumerable: true,
        //设置属性是否可以删除或编辑
        configurable: true,
        get() {
          return data[key]
        },
        set(newValue) {
          if(newValue === data[key]) {
            return
          }
          data[key] = newValue
          document.querySelector('#app').innerText = data[key]
        }
      })
    })
  </script>
</body>
</html>

Object.defineProperty的实现方式有一定的缺陷,需要注意以下几点:

  1. 直接对对象属性的添加、删除并不能被监视到,这是因为getter/setter的方式只能监测到属性的修改,并不能监视属性的添加或删除
  2. 对于数组类型的数据,Object.defineProperty不能监听到数组元素的变化,需要对数组方法进行重写,Vue2.x对于大部分数组方法都进行了重写,例如push pop shift 等,但仍然存在一些情况监视不到,比如直接通过索引修改数组中某一元素,或者直接对数组长度进行更改

Vue3.x 数据劫持

由于ES6中引入了新特性Proxy,Vue3.x 的数据劫持方式跟2.x发生了很大的改变,Proxy通过创建一个对象的代理,从而实现对对象属性的监视和拦截,需要注意的是Proxy的代理是以对象为单位,而不是其中的某一个属性(这也是为什么Vue3.x版本中对于基本类型数据的响应式仍然需要将其包装为对象的原因)

同样我们来模拟一下Vue3.x的数据劫持

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>
    //模拟数据
    const obj = {
      name: 'Jerry',
      age: 18,
      height:178
    }
    const Vm = new Proxy(obj, {
      get(target,key){
        return target[key]
      },
      set(target,key,newValue){
        if(target[key] === newValue){
          return
        }
        target[key] = newValue
      }
    })
    Vm.name = 'Tom'
    console.log(Vm.name)
  </script>
</body>
</html>

发布-订阅者模式

发布者-订阅者模式大致流程是,监听器负责监听数据状态变化, 当数据发生变化,就通知对应的订阅者,订阅者再进行业务逻辑的处理

而在Vue的响应式中,首先通过上述方法要对数据进行劫持监听,构建一个监听器Observer,它负责对所有数据进行监听,当属性发生变化时,就通知订阅者Watcher,在Vue中同一个数据我们可能在很多地方都会使用,所有订阅同一数据的Watcher通常存在多个,需要构建一个订阅器Dep来存储这些Watcher,每一个数据都会维护一个自身的Dep对象

指令解析器Compile负责对节点进行扫描,简单来说就是知道哪个数据在哪个位置,然后初始化Watchwer并将更新视图的方法加入到对应的Watcher中,此时当Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图

接下来让我们实现一下这几个功能:

首先是Observer,它主要由遍历属性的方法walk和添加响应式的方法defineReactive组成

JavaScript 复制代码
//Observer
class Observer {
  constructor(data) {
    // 遍历 data
    this.walk(data)
  }
  // 遍历 data 转为响应式
  walk(data) {
    if (!data || typeof data !== 'object') {
      return 
    }
    Object.keys(data).forEach((key) => {
      // 转为响应式
      this.defineReactive(data, key, data[key])
    })
  }
  // 转为响应式
  defineReactive(obj, key, value) {
    // 对于复杂数据类型,递归调用
    this.walk(value)
    const self = this
    // 每个数据维护一个自身的Dep对象
    let dep = new Dep()
    Object.defineProperty(obj, key, {
       //设置是否可枚举,即是否可以被遍历
       enumerable: true,
       //设置属性是否可以删除或编辑
       configurable: true,

      // 获取值
      get() {
        // 将Watcher加入对应的Dep
        if(Dep.target){
          dep.addSub(Dep.target)
        }
        return value
      },
      // 设置值
      set(newValue) {
        // 判断旧值和新值是否相等
        if (newValue === value) return
        // 设置新值
        value = newValue
        // 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的
        self.walk(newValue)
        // 触发通知 更新视图
        dep.notify()
      },
    })
  }
}

接下来简单实现一下Dep

JavaScript 复制代码
class Dep {
  constructor () {
      //用于存放Watcher的数组
      this.subs = [];
  }
  //添加Watcher的方法
  addSub (sub) {
      this.subs.push(sub);
  }
 //通知所有Watcher进行更新
  notify () {
      this.subs.forEach((sub) => {
          sub.update();
      })
  }
}

然后是Watcher

JavaScript 复制代码
class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
  
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    //这里触发看getter,已经存储了Watcher
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 更新视图
    this.cb(this.value)
  }
}

总结

在Vue响应式的整个流程中,执行new Vue()生成Vue对象后, Vue 会调用 _init 函数进行初始化,在 这个过程Data 通过Observer转换成了getter/setter的形式,实现对数据的劫持

另一方面,在对DOM的解析过程中,Compile会初始化对应的Watcher对象,当进行页面渲染时,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到Dep对象中进行依赖收集

在数据的值的时候,会触发对应的settersetter通知之前依赖收集得到的 Dep 中的每一个 Watcher对象,并调用对应的update方法重新渲染视图

相关推荐
蜗牛快跑2135 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy6 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR2 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式