搜尋
首頁web前端Vue.js一文詳解Vue2/Vue3的響應式原理

一文詳解Vue2/Vue3的響應式原理

在講解之前,我們先來了解一下資料響應式是什麼?所謂資料響應式就是建立響應式資料依賴(呼叫了響應式資料的操作)之間的關係,當響應式資料改變時,可以通知那些使用了這些響應式資料的依賴操作進行相關更新操作,可以是DOM更新,也可以是執行一些回呼函數。從Vue2到Vue3都使用了響應式,那麼它們之間有什麼區別?

#那麼它們之間有什麼差別?為什麼Vue3會選擇Proxy取代defineProperty?我們先來看看下面兩個例子:

// 
defineReactive(data,key,val){
    Object.defineProperty(data,key,{
      enumerable:true,
      configurable:true,
      get:function(){
        console.log(`对象属性:${key}访问defineReactive的get!`)
        return val;
      },
      set:function(newVal){
        if(val===newVal){
          return;
        }
        val = newVal;
        console.log(`对象属性:${key}访问defineReactive的get!`)
      }
    })
}
let obj = {};
this.defineReactive(obj,'name','sapper');
// 修改obj的name属性
obj.name = '工兵';
console.log('obj',obj.name);
// 为obj添加age属性
obj.age = 12;
console.log('obj',obj);
console.log('obj.age',obj.age);
// 为obj添加数组属性
obj.hobby = ['游戏', '原神'];
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

// 为obj添加对象属性
obj.student = {school:'大学'};
obj.student.school = '学院';
console.log('obj.student.school',obj.student.school);

一文詳解Vue2/Vue3的響應式原理從上圖可以看出使用defineProperty定義了包含name屬性的物件obj,然後加入age屬性、新增hobby屬性(陣列)、新增student屬性並分別訪問,都沒有觸發obj物件中的get、set方法。也就是說defineProperty定義物件不能監聽新增額外屬性或修改額外新增的屬性的變更,我們再看看這樣一個例子:

let obj = {};
// 初始化就添加hobby
this.defineReactive(obj,'hobby',['游戏', '原神']);
// 改变数组下标0的值
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

一文詳解Vue2/Vue3的響應式原理

假如我們一開始就為obj添加hobby屬性,我們發現修改數組下標0的值,並沒有觸發obj裡的set方法,也就是說defineProperty定義對像不能監聽根據自身數組下標修改數組元素的變化。那我們繼續看一下Proxy代理的物件範例:

// proxy实现
let targetProxy = {name:'sapper'};
let objProxy = new Proxy(targetProxy,{
    get(target,key){
      console.log(`对象属性:${key}访问Proxy的get!`)
      return target[key];
    },
    set(target,key,newVal){
      if(target[key]===newVal){
        return;
      }
      console.log(`对象属性:${key}访问Proxy的set!`)
      target[key]=newVal;
      return target[key];
    }
})
// 修改objProxy的name属性
objProxy.name = '工兵';
console.log('objProxy.name',objProxy.name);
// 为objProxy添加age属性
objProxy.age = 12;
console.log('objProxy.age',objProxy.age);
// 为objProxy添加hobby属性
objProxy.hobby = ['游戏', '原神'];
objProxy.hobby[0] = '王者';
console.log('objProxy.hobby',objProxy.hobby);
// 为objProxy添加对象属性
objProxy.student = {school:'大学'};
objProxy.student.school = '学院';
console.log('objProxy.student.school',objProxy.student.school);

一文詳解Vue2/Vue3的響應式原理從上圖是不是發現了Proxy與defineProperty的明顯區別之處了,Proxy能支援物件新增或修改觸發get、set方法,不管物件內部有什麼屬性。所以

  • Object.defineProperty():defineProperty定義物件不能監聽新增額外屬性修改額外新增的屬性的變化;defineProperty定義物件不能監聽根據自身陣列下標修改陣列元素的變化。我們來看看Vue裡的用法範例:

     data() {
       return {
         name: 'sapper',
         student: {
           name: 'sapper',
           hobby: ['原神', '天涯明月刀'],
         },
       };
     },
     methods: {
       deleteName() {
         delete this.student.name;
         console.log('删除了name', this.student);
       },
       addItem() {
         this.student.age = 21;
         console.log('添加了this.student的属性', this.student);
       },
       updateArr() {
         this.student.hobby[0] = '王者';
         console.log('更新了this.student的hobby', this.student);
       },
    }

    一文詳解Vue2/Vue3的響應式原理從圖中確實可以修改data裡的屬性,但不能及時渲染,所以Vue2提供了兩個屬性方法解決了這個問題:Vue.$setVue.$delete

    注意不能直接this._ data.age這樣去新增age屬性,也是不支援的。

    this.$delete(this.student, 'name');// 删除student对象属性name
    this.$set(this.student, 'age', '21');// 添加student对象属性age
    this.$set(this.student.hobby, 0, '王者');// 更新student对象属性hobby数组

    一文詳解Vue2/Vue3的響應式原理

  • Proxy:解決了上面兩個弊端,proxy可以實作:

  • 可以直接監聽物件而非物件屬性,可以監聽物件新增額外屬性的變化;

    const user = {name:'张三'}
    const obj = new Proxy(user,{
      get:function (target,key){
        console.log("get run");
        return target[key];
      },
      set:function (target,key,val){
        console.log("set run");
        target[key]=val;
        return true;
      }
    })
    obj.age = 22;
    console.log(obj); // 监听对象添加额外属性打印set run!
  • 可以直接監聽陣列的變化

    const obj = new Proxy([2,1],{
      get:function (target,key){
        console.log("get run");
        return target[key];
      },
      set:function (target,key,val){
        console.log("set run");
        target[key]=val;
        return true;
      }
    })
    obj[0] = 3;
    console.log(obj); // 监听到了数组元素的变化打印set run!
  • Proxy 傳回的是一個新物件,而 Object.defineProperty 只能遍歷物件屬性直接修改。

  • 支援多達13 種攔截方法,不限於 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty 不具備的。

總的來說,Vue3響應式使用Proxy解決了Vue2的響應式的詬病,從原理上說,它們所做的事情都是一樣的,依賴收集依賴更新

1 Vue2響應式原理

這裡基於Vue2.6.14版本進行分析

Vue2響應式:透過Object. defineProperty()對每個屬性進行監聽,當對屬性進行讀取的時候就會觸發getter,對屬性修改的時候就會觸發setter。首先我們都知道Vue實例中有data屬性定義響應式數據,它是一個物件。我們來看看下面範例的data:

data(){
 return {
  name: 'Sapper',
  hobby: ['游戏', '原神'],
  obj: {
    name: '张三',
    student: {
      major: '软件工程',
      class: '1班',
    }
  }
 }
}

一文詳解Vue2/Vue3的響應式原理从上图我们可以看到,data中的每一个属性都会带 __ob__ 属性,它是一个Observer对象,其实Vue2中响应式的关键就是这个对象,在data中的每一个属性都会带get、set方法,而Vue源码中其实把get、set分别定义为reactiveGetter、reactiveSetter,这些东西怎么添加进去的。Vue2又是怎么数据变化同时实时渲染页面?先看看下面的图:

一文詳解Vue2/Vue3的響應式原理给data属性创建Observer实例:通过初注册响应式函数initState中调用了initData函数实现为data创建Observer实例。

一文詳解Vue2/Vue3的響應式原理

function initData(vm: Component) {
  // 获取组件中声明的data属性
  let data: any = vm.$options.data
  // 对new Vue实例下声明、组件中声明两种情况的处理
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  ...
  // observe data
  const ob = observe(data) // 为data属性创建Observer实例
  ob && ob.vmCount++
}

通过Observer实例把data中所有属性转换成getter/setter形式来实现响应性:对data属性分为两种情况处理:对象属性处理(defineReactive实现)和数组属性处理。数组怎么处理(后面再详细说明)

注意地,由于Vue实例的data永远都是一个对象,所以data里面包含的数组类型只有对象属性数组属性

一文詳解Vue2/Vue3的響應式原理在getter收集依赖,在setter中触发依赖:当读取data中的数据时,会在get方法中收集依赖,当修改data中的数据时,会在set方法中通知依赖更新。defineReactive方法中主要是做四件事情:创建Dep实例给对象属性添加get/set方法收集依赖通知依赖更新

一文詳解Vue2/Vue3的響應式原理

从上面我们知道了dep.depend()实现了依赖收集,dep.notify()实现了通知依赖更新,那么Dep类究竟做了什么?我们先看看下面的图:

一文詳解Vue2/Vue3的響應式原理从图中我们得明确一点,谁使用了变化的数据,也就是说哪个依赖使用了变化的数据,其实就是Dep.taget,它就是我们需要收集的依赖,是一个Watcher实例对象,其实Watcher对象有点类似watch监听器,我们先看一个例子:

vm.$watch('a.b.c',function(newVal,oldVal)){....}

怎么监听多层嵌套的对象,其实就是通过.分割为对象,循环数组一层层去读数据,最后一后拿到的就是想要对的数据。

export function parsePath (path){
 const segment = path.split('.');
 return function(obj){
 ...
   for(let i=0;i<segment.length;i++){
     if(!obj) return;
     obj = obj[segment[i]]
   }
   return obj
 }
}

当嵌套对象a.b.c属性发生变化时,就会触发第二个参数中的函数。也就是说a.b.c就是变化的数据,当它的值发生变化时,通知Watcher,接着Watcher触发第二个参数执行回调函数。我们看看Watcher类源码,是不是发现了cb其实就与watch的第二参数有异曲同工之妙。

export default class Watcher implements DepTarget {
  vm?: Component | null
  cb: Function
  deps: Array<Dep>
  ...
  constructor(vm: Component | null,expOrFn: string | (() => any),cb: Function,...) {
    ...
    this.getter = parsePath(expOrFn)// 解析嵌套对象
    ...
  }
  get() { // 读取数据
    ...
    return value
  }

  addDep(dep: Dep) {
    ...
    dep.addSub(this)//添加依赖
    ...
  }
  cleanupDeps() {// 删除依赖
    ...
    dep.removeSub(this)
    ...
  }
  update() {// 通知依赖更新
   this.run()
   ...
  }

  run() {
   ...
   this.cb.call(this.vm, value, oldValue)
  }
  ...
  depend() { // 收集依赖
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  ...
}

实现对数组的监听:从最开始的例子,我们了解对象以及嵌套对象的监听,但是Object.defineProperty是用来监听对象指定属性的变化,不支持数组监听,那么数组又是怎么监听?我们上面说了data中的数据被赋予响应性都是在Observer中实现的,那么监听的实现也是在Observer对象中实现的,先对数组的特定方法做自定义处理,为了拦截数组元素通知依赖更新,然后才通过observeArray函数遍历创建Observer实例,主要分为两种情况:

// 源码Observer类中对数组处理的部分代码
if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
}
  • 当浏览器支持__ proto __ 对象:强制赋值当前arrayMethods给target的__ proto __ 对象,直接给当前target数组带上自定义封装的数组方法,从而实现监听数组变化。其实arrayMethods处理后就是下面这样一个对象:

    一文詳解Vue2/Vue3的響應式原理

    const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
    console.log(arrayKeys);// [&#39;push&#39;, &#39;pop&#39;, &#39;shift&#39;, &#39;unshift&#39;, &#39;splice&#39;, &#39;sort&#39;, &#39;reverse&#39;]
    copyAugment(value, arrayMethods, arrayKeys)
    
    function copyAugment (target: Object, src: Object, keys: Array<string>) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])// 遍历数组元素通过为元素带上
      }
    }
  • 当浏览器不支持__ proto __ 对象:遍历数组元素通过defineProperty定义为元素带上自定义封装的原生数组方法,由于自定义数组方法中做了拦截通知依赖更新,从而实现监听数组的变化。

    const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
    console.log(arrayKeys);// [&#39;push&#39;, &#39;pop&#39;, &#39;shift&#39;, &#39;unshift&#39;, &#39;splice&#39;, &#39;sort&#39;, &#39;reverse&#39;]
    copyAugment(value, arrayMethods, arrayKeys)
    
    function copyAugment (target: Object, src: Object, keys: Array<string>) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])// 遍历数组元素通过为元素带上
      }
    }

    对数组的Array原生方法做了自定义封装的源码如下,在自定义方法中拦截通知依赖更新。一文詳解Vue2/Vue3的響應式原理

    // 遍历target实现创建Observer实例
    observeArray (items: Array<any>) {
     for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
     }
    }

Vue2响应式原理小结:

  • 给data创建Observer实例
  • Observer类实现对数据封装getter、setter的响应性
  • 针对数组类型数据,自定义封装Array原生方法,在封装过程中拦截执行通知依赖更新
  • 真正通过Watcher通知依赖更新,通过run方法中的cb回调函数,实现类似watch侦听器第二参数中监听变化后的操作

Vue3响应式原理

这里基于Vue3.2.41版本进行分析

其实Vue3的响应原理与Vue2的响应原理都差不多,唯一不同的就是它们的实现方式,Vue3通过创建Proxy的实例对象而实现的,它们都是收集依赖、通知依赖更新。而Vue3中把依赖命名为副作用函数effect,也就是数据改变发生的副作用,我们先来看一下例子:

const house = {status:&#39;未出租&#39;,price:1200,type:&#39;一房一厅&#39;};
const obj = new Proxy(house, {
  get (target, key) {
    return target[key];
  },
  set (target, key, newVal) {
    target[key] = newVal;
    return true;
  }
})
function effect () {
  console.log(&#39;房子状态:&#39;+obj.status);
}

effect () // 触发了proxy对象的get方法
obj.status = &#39;已出租!&#39;;
effect ()

通过Proxy创建一个代理对象,把house代理给obj,obj是代理对象,house是被代理对象。house对象中数据改变,由于effect函数读取了对象属性,所以当数据改变,也需要及时更新副作用函数effect。但是问题来了,假如对象中多个属性的,依赖于数据变化的多个副作用函数,数据变化一次都需要执行一次,代码写起来就会很冗余,所以我们需要这样处理:

const objSet = new Set();
const obj = new Proxy(house, {
  // 拦截读取操作
  get (target, key) {
    objSet.add(effect) // 收集effect
    return target[key];
  },
  set (target, key, newVal) {
    target[key] = newVal;
    objSet.forEach(fn=>fn()) // 遍历effect
    return true;
  }
})

把副作用函数都存到Set实例中,Set可以过滤重复数据,然后在获取数据中收集副作用函数,在修改数据中遍历执行副作用函数,这样就简化了代码,不需要每次改变都要执行一次了,也就是修改一次数据及时更新effect。虽然上面已经实现了响应式的雏形了,但是还需要解决很多问题:

假如这个副作用函数是一个匿名函数,这时候需要怎么处理? 添加一个全局变量临时存储。

effect (()=>console.log('房子状态:'+obj.status)) // 上面的例子会直接报not define
// 添加一个全局变量activeEffect存储依赖函数,这样effect就不会依赖函数的名字了
let activeEffect;
function effect (fn) {
 activeEffect = fn;
 // 执行副作用函数
 fn()
}

假如读取不存在的属性的时候,副作用函数发生什么? 副作用函数会被重新执行,由于目标字段与副作用函数没有建立明确的函数联系。所以这就需要引入唯一key辨识每一个数据的副作用函数,以target(目标数据)、key(字段名)、effectFn(依赖)。看下图:

setTimeout(() => {
  obj.notExit = &#39;不存在的属性&#39;;
}, 1000)

分三种情况分析副作用函数存储数据唯一标识

  • 两个副作用函数同时读取同一个对象的属性值

一文詳解Vue2/Vue3的響應式原理

  • 一个副作用函数中读取了同一个对象不同属性

一文詳解Vue2/Vue3的響應式原理

  • 不同副作用函数中读取两个不同对象的相同属性

一文詳解Vue2/Vue3的響應式原理所以为了解决这些不同情况的副作用保存问题,所以Vue3引入了Weak、Map、Set三个集合方法来保存对象属性的相关副作用函数:

一文詳解Vue2/Vue3的響應式原理

const weakMap = new WeakMap();
let activeEffect;
const track = ((target,key)=>{
  if(!activeEffect){
      return;
    }
    // 从weakMap中获取当前target对象
    let depsMap = weakMap.get(target);
    if(!depsMap){
      weakMap.set(target,(depsMap=new Map()))
    }
    // 从Map中属性key获取当前对象指定属性
    let deps = depsMap.get(key)
    if(!deps){
      // 副作用函数存储
      depsMap.set(target,(deps=new Set()))
    }
    deps.add(activeEffect)  
})
const trigger = ((target,key)=>{
  // 从weakMap中获取当前target对象
  const depsMap = weakMap.get(target);
    if(!depsMap) return;
    // 从Map中获取指定key对象属性的副作用函数集合
    const effects = depsMap.get(key);
    effects&&effects.forEach(fn=>fn())
})

WeakMap与Map的区别是? 区别就是垃圾回收器是否回收的问题,WeakMap对象对key是弱引用,如果target对象没有任何引用,可以被垃圾回收器回收,这就需要它了。相对于WeakMap,不管target是否引用,Map都不会被垃圾回收,容易造成内存泄露。我们看一下下面例子:

const map = new Map();
const weakMap = new WeakMap();
(function(){
  const foo = {foo:1};
  const bar = {bar:2};
  map.set(foo,1);
  weakMap.set(bar,2);
})() // 函数执行完,weakMap内的所有属性都被垃圾回收器回收了
setTimeout(() => {
 console.log(weakMap);// 刷新页面发现weakMap里面没有属性了
}, 2000)

假如在一个副作用函数中调用了对象的两个属性,但是有布尔值控制,按正常来说,副作用函数只能执行一次get获取值的,但是我们现有的实现方法还实现不了,我们看看下面例子。

const effectFn = (() => {
  const str = obj.status ? &#39;&#39; : obj.type;
})
const obj = new Proxy(house, {
  get(target, key) {
    console.log(&#39;get run!&#39;);// 打印了两次
    ...
  },
  set(target, key, newVal) {
   ...
  }
})

通过这个例子,我们是不是需要解决这个问题,也就是当每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。我们看看源码例子:

// 清空副作用函数依赖的集合
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

嵌套副作用函数处理:由于副作用函数可能是嵌套,比如副作用函数中effectFn1中有还有一个副作用函数effectFn2,以上面的方法对于嵌套函数的处理用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。看了很多资料举例用effect栈存储,是的没错,当执行副作用函数的时候把它入栈,执行完毕后把它出栈。现在我们一起看一下源码怎么处理的:

  • 按位跟踪标记递归深度方式(优化方案):通过用二进制位标记当前嵌套深度的副作用函数是否记录过,如果记录过就,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集副作用函数的。

    let effectTrackDepth = 0 // 当前副作用函数递归深度
    export let trackOpBit = 1 // 在track函数中执行当前的嵌套副作用函数的标志位
    const maxMarkerBits = 30 // 最大递归深度支持30位,

    为什么需要设置30位,因为31位会溢出。

    // 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1
    trackOpBit = 1 << ++effectTrackDepth
    
    // 执行完副作用函数后会自减
    trackOpBit = 1 << --effectTrackDepth;

    为什么是左移一位,是因为第一位也就是说当前深度只是1,所以保持不变,不用管,从第二位开始。

      if (effectTrackDepth <= maxMarkerBits) {
        // 执行副作用函数之前,使用 `deps[i].w |= trackOpBit`对依赖dep[i]进行标记,追踪依赖
        initDepMarkers(this)
      } else {
        // 降级方案:完全清理
        cleanupEffect(this)
      }

    如何判断当前依赖是否已记录过,通过按位与判断是否有位已经标识,有就大于0:

    //代表副作用函数执行前被 track 过
    export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
    //代表副作用函数执行后被 track 过
    export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0

    清理依赖

    export const finalizeDepMarkers = (effect: ReactiveEffect) => {
      const { deps } = effect
      if (deps.length) {
        let ptr = 0
        for (let i = 0; i < deps.length; i++) {
          const dep = deps[i]
          // 有 was 标记但是没有 new 标记,应当删除
          if (wasTracked(dep) && !newTracked(dep)) {
            dep.delete(effect)
          } else {
            // 需要保留的依赖
            deps[ptr++] = dep
          }
          // 清空,把当前位值0,先按位非,再按位与
          dep.w &= ~trackOpBit
          dep.n &= ~trackOpBit
        }
        // 保留依赖的长度
        deps.length = ptr
      }
    }
  • 完全清理方式(降级方案):逐个清理掉当前依赖集合deps中每个依赖。

    function cleanupEffect(effect: ReactiveEffect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }

响应式可调度性scheduler:trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

Vue3响应式的6个细节我们都了解了,我们可以对副作用工作流做一个全面总结如图:

一文詳解Vue2/Vue3的響應式原理Vue3响应式的关键在于两个函数:track(收集依赖)和trigger(触发依赖)。

// target: 响应式代理对象, type: 订阅类型(get、hase、iterate), key: 要获取的target的键值
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果允许追踪, 并且当前有正在运行的副作用
  if (shouldTrack && activeEffect) {
  // 获取当前target订阅的副作用集合, 如果不存在, 则新建一个
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 获取对应属性key订阅的副作用, 如果不存在, 则新建一个
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }
    ...
    // 处理订阅副作用
    trackEffects(dep, eventInfo)
  }
}

export function trackEffects(dep: Dep,debuggerEventExtraInfo?: DebuggerEventExtraInfo) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) { // 如果当前追踪深度不超过最大深度(30), 则添加订阅
    if (!newTracked(dep)) { // 如果未订阅过, 则新建
      dep.n |= trackOpBit // 据当前的追踪标识位设置依赖的new值
      shouldTrack = !wasTracked(dep) // 开启订阅追踪
    }
  } else {
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!) // 将当前正在运行副作用作为新订阅者添加到该依赖中
    activeEffect!.deps.push(dep) // 缓存依赖到当前正在运行的副作用依赖数组
    ...
  }
}
// 根据不同的type从depsMap取出,放入effects,随后通过run方法将当前的`effect`执行
export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>) {
  const depsMap = targetMap.get(target) // 获取响应式对象的副作用Map, 如果不存在说明未被追踪, 则不需要处理
  if (!depsMap) {
    return
  }
  let deps: (Dep | undefined)[] = []
  // 如果是清除操作,那就要执行依赖原始数据的所有监听方法。因为所有项都被清除了。
  if (type === TriggerOpTypes.CLEAR) { // clear
    // 如果是调用了集合的clear方法, 则要对其所有的副作用进行处理
    deps = [...depsMap.values()]
  } else if (key === &#39;length&#39; && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === &#39;length&#39; || key >= newLength) {
        deps.push(dep)
      }
    })
  } else { // set add delete
    // key不为void 0,则说明肯定是SET | ADD | DELETE这三种操作 
    // 然后将依赖这个key的所有监听函数推到相应队列中
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    switch (type) { // 根据不同type取出并存入deps
      case TriggerOpTypes.ADD:
         // 如果原始数据是数组,则key为length,否则为迭代行为标识符
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          deps.push(depsMap.get(&#39;length&#39;))
        }
        break
      case TriggerOpTypes.DELETE:
       // 如果原始数据是数组,则key为length,否则为迭代行为标识符
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
  ...
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    // 遍历effects元素执行run函数
    triggerEffects(createDep(effects))
  }
}

Vue3响应式原理小结:

Vue3中的副作用函数其实就是Vue2的依赖

  • activeEffect解决匿名函数问题
  • WeakMap、Map、Set存储对象属性的相关副作用函数
  • 处理副作用函数时,假如有多个响应式属性,控制只触发生效的属性或用到的属性
  • 嵌套副作用函数,使用二进制位记录嵌套副作用,通过控制二进制位是否清理嵌套副作用实现层级追踪
  • track()实现依赖收集、层级依赖追踪、依赖清理(解决嵌套副作用)
  • trigger()当某个依赖值发生变化时触发的, 根据依赖值的变化类型, 会收集与依赖相关的不同副作用处理对象, 然后逐个触发他们的 run 函数, 通过执行副作用函数获得与依赖变化后对应的最新值

(学习视频分享:vuejs入门教程编程基础视频

以上是一文詳解Vue2/Vue3的響應式原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:掘金社区。如有侵權,請聯絡admin@php.cn刪除
vue.js和前端:深入研究框架vue.js和前端:深入研究框架Apr 22, 2025 am 12:04 AM

Vue.js被開發者喜愛因為它易於上手且功能強大。 1)其響應式數據綁定係統自動更新視圖。 2)組件系統提高了代碼的可重用性和可維護性。 3)計算屬性和偵聽器增強了代碼的可讀性和性能。 4)使用VueDevtools和檢查控制台錯誤是常見的調試技巧。 5)性能優化包括使用key屬性、計算屬性和keep-alive組件。 6)最佳實踐包括清晰的組件命名、使用單文件組件和合理使用生命週期鉤子。

vue.js在前端的力量:關鍵特徵和好處vue.js在前端的力量:關鍵特徵和好處Apr 21, 2025 am 12:07 AM

Vue.js是一個漸進式的JavaScript框架,適用於構建高效、可維護的前端應用。其關鍵特性包括:1.響應式數據綁定,2.組件化開發,3.虛擬DOM。通過這些特性,Vue.js簡化了開發過程,提高了應用性能和可維護性,使其在現代Web開發中備受歡迎。

vue.js比反應好嗎?vue.js比反應好嗎?Apr 20, 2025 am 12:05 AM

Vue.js和React各有優劣,選擇取決於項目需求和團隊情況。 1)Vue.js適合小型項目和初學者,因其簡潔和易上手;2)React適用於大型項目和復雜UI,因其豐富的生態系統和組件化設計。

vue.js的功能:增強前端的用戶體驗vue.js的功能:增強前端的用戶體驗Apr 19, 2025 am 12:13 AM

Vue.js通過多種功能提升用戶體驗:1.響應式系統實現數據即時反饋;2.組件化開發提高代碼復用性;3.VueRouter提供平滑導航;4.動態數據綁定和過渡動畫增強交互效果;5.錯誤處理機制確保用戶反饋;6.性能優化和最佳實踐提升應用性能。

vue.js:定義其在網絡開發中的作用vue.js:定義其在網絡開發中的作用Apr 18, 2025 am 12:07 AM

Vue.js在Web開發中的角色是作為一個漸進式JavaScript框架,簡化開發過程並提高效率。 1)它通過響應式數據綁定和組件化開發,使開發者能專注於業務邏輯。 2)Vue.js的工作原理依賴於響應式系統和虛擬DOM,優化性能。 3)實際項目中,使用Vuex管理全局狀態和優化數據響應性是常見實踐。

了解vue.js:主要是前端框架了解vue.js:主要是前端框架Apr 17, 2025 am 12:20 AM

Vue.js是由尤雨溪在2014年發布的漸進式JavaScript框架,用於構建用戶界面。它的核心優勢包括:1.響應式數據綁定,數據變化自動更新視圖;2.組件化開發,UI可拆分為獨立、可複用的組件。

Netflix的前端:React(或VUE)的示例和應用Netflix的前端:React(或VUE)的示例和應用Apr 16, 2025 am 12:08 AM

Netflix使用React作為其前端框架。 1)React的組件化開發模式和強大生態系統是Netflix選擇它的主要原因。 2)通過組件化,Netflix將復雜界面拆分成可管理的小塊,如視頻播放器、推薦列表和用戶評論。 3)React的虛擬DOM和組件生命週期優化了渲染效率和用戶交互管理。

前端景觀:Netflix如何處理其選擇前端景觀:Netflix如何處理其選擇Apr 15, 2025 am 12:13 AM

Netflix在前端技術上的選擇主要集中在性能優化、可擴展性和用戶體驗三個方面。 1.性能優化:Netflix選擇React作為主要框架,並開發了SpeedCurve和Boomerang等工具來監控和優化用戶體驗。 2.可擴展性:他們採用微前端架構,將應用拆分為獨立模塊,提高開發效率和系統擴展性。 3.用戶體驗:Netflix使用Material-UI組件庫,通過A/B測試和用戶反饋不斷優化界面,確保一致性和美觀性。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。