Vue响应式原理

本文讲述了Vue如何通过数据劫持、编译模板和Watcher机制实现数据驱动的视图更新。

Vue响应式原理

1 目的

Vue 通过数据劫持获取数据变化,利用发布-订阅模式,在当数据发生改变时自动响应到界面上。

2 原理

  1. **Obeserve 建立数据劫持(观察)。**通过js的Object.defineProperty(vue3使用Proxy)监听劫持数据对象的每一个属性的Getter和Setter(如果属性是对象,对于内部的修改将无法监听到,如何需要深度监听每一个对象可以使用递归遍历每一个对象类型的属性建立Getter和Setter,实现深度监听)。
  2. **Compile 编译和解析 html 模板。**将界面引用的变量进行替换并通过添加订阅者Watcher与界面建立关系,将建立的Watcher添加到对于数据的Dep(属性订阅器:就是发布订阅模式的中间器代理,内部维护着所有绑定此属性所有的Watcher,当属性发生改变可以统一一起通知Watcher进行界面修改)。Watcher中必须有一个update函数用于通知更新界面。
  3. **响应式过程。**当数据发生改变时,通过Obeserve 中Setter获取到改动动作和新数据,通过Dep属性订阅器通知每一个订阅者Watcher更新界面。Vue的MVVM模型便是包含Obeserve(观察的数据) 、Compile(解析模板将变量替换成数据) 、Watcher(建立Obeserve与Compile关系,实现依赖收集,统一更新)。

3 实现

Obeserve 数据劫持

/**
 * 实现数据监听
 * 使用Object.defineProperty实现响应式坏处:
 * 1.无法监听对象属性的添加、删除(可以使用vue中$set)
 * 2.对于数组无法监听下标变化,所以通过下标添加属性无法监听到(vue内部做了优化数组常用的操作函数如
 * push/pop/shift/unshift/splice/sort/reverse可以响应)
 * @param obj
 * @param deep 深度监听
 */
function observe(obj,deep=false) {
  if (obj==null||typeof obj !== 'object'){
    console.log('not is a object')
    return
  }
  for (let objKey in obj) {
    const value=obj[objKey]
    if (deep&&value!=null&&typeof value ==='object'){
      observe(value)
    }
    defineObserve(obj,objKey,value,deep)
  }
}

function defineObserve(obj,key,val,deep) {
  Object.defineProperty(obj,key,{
    get() {
      //可以在这里实现收集订阅者,因为Compile需要获取数据,必然需要调用Getter
      return val
    },
    set(v) {
      //新旧相同 pass
      if (val === v){
        return
      }
      if (deep&&v!=null&&typeof v ==='object'){
        observe(v)
      }
      //可以在这里调用Dep中所有Watcher的update通知所有订阅界面更新
      console.log('update view')
      //更新数据
      val = v
    }
  })
}

/**
 * vue3
 * 使用 ES6新特性 Proxy实现响应式:
 * 1.可以劫持对象、数组的元素的添加和删除;
 * 2.需要使用new操作符,并返回一个新的Proxy对象。
 * @param obj
 * @param deep
 */
function observeVue3(obj,deep=false) {
  if (obj==null||typeof obj !== 'object'){
    console.log('not is a object')
    return
  }
  if (deep){
    for (const objKey in obj) {
      const value=obj[objKey]
      if (value&&typeof value === 'object'){
        obj[objKey]=observeVue3(value,deep)
      }
    }
  }
  return new Proxy(obj,{
    get(target, p, receiver) {
      return Reflect.get(target,p,receiver)
    },
    set(target, p, newValue, receiver) {
      const old = Reflect.get(target,p,receiver)
      if (old===newValue){
        return
      }

      if (deep&&newValue&&typeof newValue === 'object'){
        newValue=observeVue3(newValue,deep)
      }

      //更新
      Reflect.set(target,p,newValue,receiver)
      //通知
      console.log('update view',p)
    },
    deleteProperty(target, p) {
      Reflect.defineProperty(target,p)
    }
  })
}


const obj={
  name:1,
  object:{
    test:1,
  }
}
function testVue2() {
  observe(obj,true)
  obj.object={name:1}
  obj.object.name=2
}

function testVue3() {
  const objProxy = observeVue3(obj,true)
  objProxy.object2={name:1}
  console.log(objProxy.object2)
  objProxy.object2.name2=2
}

testVue3()

testVue3效果

image-20231011231534420

Compile 解析

class Compile{

  /**
   *
   * @param el 根元素
   * @param vm vue instance
   */
  constructor(el,vm) {
    this.$vm=vm
  }

  /**
   * 更新界面数据
   * @param el
   */
  compile(el=new HTMLElement){
    //使用伪代码写哈
    const childNodes = el.childNodes

    for (const node of childNodes) {
      //1. vue 通过 nodeType 判断节点类型 具体可以看看此文章 https://www.cnblogs.com/wyongz/p/11446477.html
      //2. 根据不同类型分开渲染的(例如html元素或纯文本节点)。
      //3. 无论经过怎样解析需要调用
      this.update()

    }

  }

  // 通用update方法
  update(node, exp, dir) {
    // 获取更新函数 this[dir + 'Updator']是vue源码操作
    let updator = this[dir + 'Updator'];
    // 初始化,首次页面赋值
    //...
    //...
    // 创建Watcher  obj   key    function
    new Watcher(this.$vm, exp, function(value) {
      updator && updator(node, value);
    })
  }

}

class Watcher{
  constructor($vm, key, updateFunc) {
    this.$vm=$vm
    this.key=key
    this.updateFunc=updateFunc
    //实现将key添加到对相应Dep中 可以使用多种办法这里使用Getter实现
    Reflect.set(Dep,'watcher',this)
    $vm[key]
    Reflect.set(Dep,'watcher',null)
  }

  update(){
    this.updateFunc.call(this.$vm,this.$vm[this.key])
  }
}
class Dep{
  watchers=[]
  constructor(name) {
    this.depName=name
  }

  add(w){
    this.watchers.push(w)
  }

  notify(){
    this.watchers.forEach(e=>{
      e.update()
    })
  }
}

也需要修改 obeseve,这里以vue2 举例

function defineObserve(obj,key,val) {
  const dep=new Dep(key)
  Object.defineProperty(obj,key,{
    get() {
      //可以在这里实现收集订阅者,因为Compile需要获取数据,必然需要调用Getter
      if (Dep['watcher']){
        dep.add(Dep['watcher'])
      }
      return val
    },
    set(v) {
      //新旧相同 pass
      if (val === v){
        return
      }
      //可以在这里调用Dep中所有Watcher的update通知所有订阅界面更新
      console.log('update view')
      dep.notify()
      //更新数据
      val = v
    }
  })
}
Vue 框架的响应式机制是其核心特性之一,它使得开发者能够专注于数据本身,而无需手动管理数据与视图之间的同步。Vue响应式系统在不同版本中有着不同的实现方式,其中 Vue 2.x 和 Vue 3.x 的实现差异尤为显著。 ### Vue 2.x 的响应式原理Vue 2.x 中,响应式机制主要依赖于 `Object.defineProperty` 方法来劫持数据对象的属性访问器(getter/setter)[^2]。当一个数据对象被传入 Vue 实例时,Vue 会递归地遍历该对象的所有属性,并使用 `Object.defineProperty` 将它们转换为响应式的。具体来说,每个属性都会被赋予一个 getter 和 setter。当某个属性被读取时,getter 会被触发,从而触发依赖收集过程;而当属性值发生变化时,setter 会被调用,进而通知所有依赖于该属性的观察者(watcher)进行更新。 ```javascript // Vue 2.x 中使用 Object.defineProperty 创建响应式属性的简化示例 function defineReactive(obj, key, val) { const dep = []; // 依赖收集器 Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.push(Dep.target); // 收集依赖 } return val; }, set(newVal) { if (newVal === val) return; val = newVal; dep.forEach(watcher => watcher.update()); // 通知依赖更新 } }); } ``` ### Vue 3 的响应式原理 Vue 3 对响应式系统进行了重构,引入了 ES6 的 `Proxy` 特性来替代 `Object.defineProperty`,从而实现了更高效、更简洁的响应式机制[^3]。`Proxy` 提供了一种更现代的方式来拦截和自定义对象的基本操作,如属性查找、赋值、枚举等。与 `Object.defineProperty` 不同的是,`Proxy` 可以直接作用于整个对象,而不是单独处理每个属性,这不仅减少了代码量,还提高了性能。 ```javascript // Vue 3 中使用 Proxy 创建响应式对象的简化示例 function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { // 拦截属性读取操作 const result = Reflect.get(target, key, receiver); // 收集依赖 track(target, key); return result; }, set(target, key, value, receiver) { // 拦截属性设置操作 const result = Reflect.set(target, key, value, receiver); // 触发依赖更新 trigger(target, key); return result; } }); } ``` 在 Vue 3 的响应式系统中,`track` 和 `trigger` 函数分别用于依赖的收集和更新。每当一个响应式属性被访问时,`track` 被调用以记录当前的副作用函数(effect)作为该属性的依赖;而当属性值发生改变时,`trigger` 会通知所有相关的副作用函数重新执行,以此来更新视图。 此外,Vue 3 的响应式系统还引入了 `watchEffect` 和 `watch` 等 API,允许开发者以声明式的方式创建和管理副作用函数,进一步增强了响应式编程的能力。 ### 相关问题 1. Vue 2.x 和 Vue 3 在响应式机制上的主要区别是什么? 2. 如何在 Vue 3 中使用 `watchEffect` 来创建响应式的副作用? 3. `Proxy` 在 Vue 3 的响应式系统中扮演什么角色? 4. Vue响应式系统如何处理数组的变更? 5. 什么是依赖收集(Dependency Collection),它是如何工作的?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值