溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue2/Vue3的響應式原理是什么

發(fā)布時間:2023-01-31 11:33:10 來源:億速云 閱讀:102 作者:iii 欄目:編程語言

本篇內容主要講解“Vue2/Vue3的響應式原理是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Vue2/Vue3的響應式原理是什么”吧!

在講解之前,我們先了解一下數(shù)據響應式是什么?所謂數(shù)據響應式就是建立響應式數(shù)據依賴(調用了響應式數(shù)據的操作)之間的關系,當響應式數(shù)據發(fā)生變化時,可以通知那些使用了這些響應式數(shù)據的依賴操作進行相關更新操作,可以是DOM更新,也可以是執(zhí)行一些回調函數(shù)。從Vue2到Vue3都使用了響應式,那么它們之間有什么區(qū)別?

  • Vue2響應式:基于Object.defineProperty()實現(xiàn)的。

  • Vue3響應式:基于Proxy實現(xiàn)的。

那么它們之間有什么區(qū)別?為什么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添加數(shù)組屬性
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屬性(數(shù)組)、添加student屬性并分別訪問,都沒有觸發(fā)obj對象中的get、set方法。也就是說defineProperty定義對象不能監(jiān)聽添加額外屬性或修改額外添加的屬性的變化,我們再看看這樣一個例子:

let obj = {};
// 初始化就添加hobby
this.defineReactive(obj,'hobby',['游戲', '原神']);
// 改變數(shù)組下標0的值
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

Vue2/Vue3的響應式原理是什么

假如我們一開始就為obj添加hobby屬性,我們發(fā)現(xiàn)修改數(shù)組下標0的值,并沒有觸發(fā)obj里的set方法,也就是說defineProperty定義對象不能監(jiān)聽根據自身數(shù)組下標修改數(shù)組元素的變化。那么我們繼續(xù)看一下Proxy代理的對象例子:

// proxy實現(xiàn)
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的響應式原理是什么從上圖是不是發(fā)現(xiàn)了Proxy與defineProperty的明顯區(qū)別之處了,Proxy能支持對象添加或修改觸發(fā)get、set方法,不管對象內部有什么屬性。所以

  • Object.defineProperty():defineProperty定義對象不能監(jiān)聽添加額外屬性修改額外添加的屬性的變化;defineProperty定義對象不能監(jiān)聽根據自身數(shù)組下標修改數(shù)組元素的變化。我們看看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數(shù)組


    Vue2/Vue3的響應式原理是什么

  • Proxy:解決了上面兩個弊端,proxy可以實現(xiàn):

  • 可以直接監(jiān)聽對象而非對象屬性,可以監(jiān)聽對象添加額外屬性的變化;


    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); // 監(jiān)聽對象添加額外屬性打印set run!


  • 可以直接監(jiān)聽數(shù)組的變化。


    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); // 監(jiān)聽到了數(shù)組元素的變化打印set run!


  • Proxy 返回的是一個新對象,而 Object.defineProperty 只能遍歷對象屬性直接修改。

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

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

1 Vue2響應式原理

這里基于Vue2.6.14版本進行分析

Vue2響應式:通過Object.defineProperty()對每個屬性進行監(jiān)聽,當對屬性進行讀取的時候就會觸發(fā)getter,對屬性修改的時候就會觸發(fā)setter。首先我們都知道Vue實例中有data屬性定義響應式數(shù)據,它是一個對象。我們看看下面例子的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又是怎么數(shù)據變化同時實時渲染頁面?先看看下面的圖:

Vue2/Vue3的響應式原理是什么? 給data屬性創(chuàng)建Observer實例:通過初注冊響應式函數(shù)initState中調用了initData函數(shù)實現(xiàn)為data創(chuàng)建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屬性創(chuàng)建Observer實例
  ob && ob.vmCount++
}

? 通過Observer實例把data中所有屬性轉換成getter/setter形式來實現(xiàn)響應性:對data屬性分為兩種情況處理:對象屬性處理(defineReactive實現(xiàn))和數(shù)組屬性處理。數(shù)組怎么處理(后面再詳細說明)

注意地,由于Vue實例的data永遠都是一個對象,所以data里面包含的數(shù)組類型只有對象屬性、數(shù)組屬性。

Vue2/Vue3的響應式原理是什么? 在getter收集依賴,在setter中觸發(fā)依賴:當讀取data中的數(shù)據時,會在get方法中收集依賴,當修改data中的數(shù)據時,會在set方法中通知依賴更新。defineReactive方法中主要是做四件事情:創(chuàng)建Dep實例、給對象屬性添加get/set方法、收集依賴、通知依賴更新。

Vue2/Vue3的響應式原理是什么

從上面我們知道了dep.depend()實現(xiàn)了依賴收集,dep.notify()實現(xiàn)了通知依賴更新,那么Dep類究竟做了什么?我們先看看下面的圖:

Vue2/Vue3的響應式原理是什么從圖中我們得明確一點,誰使用了變化的數(shù)據,也就是說哪個依賴使用了變化的數(shù)據,其實就是Dep.taget,它就是我們需要收集的依賴,是一個Watcher實例對象,其實Watcher對象有點類似watch監(jiān)聽器,我們先看一個例子:

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

怎么監(jiān)聽多層嵌套的對象,其實就是通過.分割為對象,循環(huán)數(shù)組一層層去讀數(shù)據,最后一后拿到的就是想要對的數(shù)據。

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屬性發(fā)生變化時,就會觸發(fā)第二個參數(shù)中的函數(shù)。也就是說a.b.c就是變化的數(shù)據,當它的值發(fā)生變化時,通知Watcher,接著Watcher觸發(fā)第二個參數(shù)執(zhí)行回調函數(shù)。我們看看Watcher類源碼,是不是發(fā)現(xiàn)了cb其實就與watch的第二參數(shù)有異曲同工之妙。

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() { // 讀取數(shù)據
    ...
    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()
    }
  }
  ...
}

? 實現(xiàn)對數(shù)組的監(jiān)聽:從最開始的例子,我們了解對象以及嵌套對象的監(jiān)聽,但是Object.defineProperty是用來監(jiān)聽對象指定屬性的變化,不支持數(shù)組監(jiān)聽,那么數(shù)組又是怎么監(jiān)聽?我們上面說了data中的數(shù)據被賦予響應性都是在Observer中實現(xiàn)的,那么監(jiān)聽的實現(xiàn)也是在Observer對象中實現(xiàn)的,先對數(shù)組的特定方法做自定義處理,為了攔截數(shù)組元素通知依賴更新,然后才通過observeArray函數(shù)遍歷創(chuàng)建Observer實例,主要分為兩種情況:

// 源碼Observer類中對數(shù)組處理的部分代碼
if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
}

  • 當瀏覽器支持__ proto __ 對象:強制賦值當前arrayMethods給target的__ proto __ 對象,直接給當前target數(shù)組帶上自定義封裝的數(shù)組方法,從而實現(xiàn)監(jiān)聽數(shù)組變化。其實arrayMethods處理后就是下面這樣一個對象:

    Vue2/Vue3的響應式原理是什么

    const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
    console.log(arrayKeys);// ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    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])// 遍歷數(shù)組元素通過為元素帶上
     }
    }


  • 當瀏覽器不支持__ proto __ 對象:遍歷數(shù)組元素通過defineProperty定義為元素帶上自定義封裝的原生數(shù)組方法,由于自定義數(shù)組方法中做了攔截通知依賴更新,從而實現(xiàn)監(jiān)聽數(shù)組的變化。

    const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
    console.log(arrayKeys);// ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    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])// 遍歷數(shù)組元素通過為元素帶上
     }
    }


    對數(shù)組的Array原生方法做了自定義封裝的源碼如下,在自定義方法中攔截通知依賴更新。Vue2/Vue3的響應式原理是什么


    // 遍歷target實現(xiàn)創(chuàng)建Observer實例
    observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
     observe(items[i])
    }
    }


Vue2響應式原理小結:

  • 給data創(chuàng)建Observer實例。

  • Observer類實現(xiàn)對數(shù)據封裝getter、setter的響應性。

  • 針對數(shù)組類型數(shù)據,自定義封裝Array原生方法,在封裝過程中攔截執(zhí)行通知依賴更新。

  • 真正通過Watcher通知依賴更新,通過run方法中的cb回調函數(shù),實現(xiàn)類似watch偵聽器第二參數(shù)中監(jiān)聽變化后的操作。

Vue3響應式原理

這里基于Vue3.2.41版本進行分析

其實Vue3的響應原理與Vue2的響應原理都差不多,唯一不同的就是它們的實現(xiàn)方式,Vue3通過創(chuàng)建Proxy的實例對象而實現(xiàn)的,它們都是收集依賴、通知依賴更新。而Vue3中把依賴命名為副作用函數(shù)effect,也就是數(shù)據改變發(fā)生的副作用,我們先來看一下例子:

const house = {status:'未出租',price:1200,type:'一房一廳'};
const obj = new Proxy(house, {
  get (target, key) {
    return target[key];
  },
  set (target, key, newVal) {
    target[key] = newVal;
    return true;
  }
})
function effect () {
  console.log('房子狀態(tài):'+obj.status);
}

effect () // 觸發(fā)了proxy對象的get方法
obj.status = '已出租!';
effect ()

通過Proxy創(chuàng)建一個代理對象,把house代理給obj,obj是代理對象,house是被代理對象。house對象中數(shù)據改變,由于effect函數(shù)讀取了對象屬性,所以當數(shù)據改變,也需要及時更新副作用函數(shù)effect。但是問題來了,假如對象中多個屬性的,依賴于數(shù)據變化的多個副作用函數(shù),數(shù)據變化一次都需要執(zhí)行一次,代碼寫起來就會很冗余,所以我們需要這樣處理:

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;
  }
})

把副作用函數(shù)都存到Set實例中,Set可以過濾重復數(shù)據,然后在獲取數(shù)據中收集副作用函數(shù),在修改數(shù)據中遍歷執(zhí)行副作用函數(shù),這樣就簡化了代碼,不需要每次改變都要執(zhí)行一次了,也就是修改一次數(shù)據及時更新effect。雖然上面已經實現(xiàn)了響應式的雛形了,但是還需要解決很多問題:

? 假如這個副作用函數(shù)是一個匿名函數(shù),這時候需要怎么處理? 添加一個全局變量臨時存儲。

effect (()=>console.log('房子狀態(tài):'+obj.status)) // 上面的例子會直接報not define
// 添加一個全局變量activeEffect存儲依賴函數(shù),這樣effect就不會依賴函數(shù)的名字了
let activeEffect;
function effect (fn) {
 activeEffect = fn;
 // 執(zhí)行副作用函數(shù)
 fn()
}

? 假如讀取不存在的屬性的時候,副作用函數(shù)發(fā)生什么? 副作用函數(shù)會被重新執(zhí)行,由于目標字段與副作用函數(shù)沒有建立明確的函數(shù)聯(lián)系。所以這就需要引入唯一key辨識每一個數(shù)據的副作用函數(shù),以target(目標數(shù)據)、key(字段名)、effectFn(依賴)??聪聢D:

setTimeout(() => {
  obj.notExit = '不存在的屬性';
}, 1000)

分三種情況分析副作用函數(shù)存儲數(shù)據唯一標識

  • 兩個副作用函數(shù)同時讀取同一個對象的屬性值

Vue2/Vue3的響應式原理是什么

  • 一個副作用函數(shù)中讀取了同一個對象不同屬性

Vue2/Vue3的響應式原理是什么

  • 不同副作用函數(shù)中讀取兩個不同對象的相同屬性

Vue2/Vue3的響應式原理是什么所以為了解決這些不同情況的副作用保存問題,所以Vue3引入了Weak、Map、Set三個集合方法來保存對象屬性的相關副作用函數(shù):

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){
      // 副作用函數(shù)存儲
      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對象屬性的副作用函數(shù)集合
    const effects = depsMap.get(key);
    effects&&effects.forEach(fn=>fn())
})

? WeakMap與Map的區(qū)別是? 區(qū)別就是垃圾回收器是否回收的問題,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);
})() // 函數(shù)執(zhí)行完,weakMap內的所有屬性都被垃圾回收器回收了
setTimeout(() => {
 console.log(weakMap);// 刷新頁面發(fā)現(xiàn)weakMap里面沒有屬性了
}, 2000)

? 假如在一個副作用函數(shù)中調用了對象的兩個屬性,但是有布爾值控制,按正常來說,副作用函數(shù)只能執(zhí)行一次get獲取值的,但是我們現(xiàn)有的實現(xiàn)方法還實現(xiàn)不了,我們看看下面例子。

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

通過這個例子,我們是不是需要解決這個問題,也就是當每次副作用函數(shù)執(zhí)行時,我們可以先把它從所有與之關聯(lián)的依賴集合中刪除。我們看看源碼例子:

// 清空副作用函數(shù)依賴的集合
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
  }
}

? 嵌套副作用函數(shù)處理:由于副作用函數(shù)可能是嵌套,比如副作用函數(shù)中effectFn1中有還有一個副作用函數(shù)effectFn2,以上面的方法對于嵌套函數(shù)的處理用全局變量 activeEffect 來存儲通過 effect 函數(shù)注冊的副作用函數(shù),這意味著同一時刻 activeEffect 所存儲的副作用函數(shù)只能有一個。當副作用函數(shù)發(fā)生嵌套時,內層副作用函數(shù)的執(zhí)行會覆蓋 activeEffect 的值,并且永遠不會恢復到原來的值??戳撕芏噘Y料舉例用effect棧存儲,是的沒錯,當執(zhí)行副作用函數(shù)的時候把它入棧,執(zhí)行完畢后把它出?!,F(xiàn)在我們一起看一下源碼怎么處理的:

  • 按位跟蹤標記遞歸深度方式(優(yōu)化方案):通過用二進制位標記當前嵌套深度的副作用函數(shù)是否記錄過,如果記錄過就,如果已經超過最大深度,因為采用降級方案,是全部刪除然后重新收集副作用函數(shù)的。


    let effectTrackDepth = 0 // 當前副作用函數(shù)遞歸深度
    export let trackOpBit = 1 // 在track函數(shù)中執(zhí)行當前的嵌套副作用函數(shù)的標志位
    const maxMarkerBits = 30 // 最大遞歸深度支持30位,


    為什么需要設置30位,因為31位會溢出。


    // 每次執(zhí)行 effect 副作用函數(shù)前,全局變量嵌套深度會自增1
    trackOpBit = 1 << ++effectTrackDepth

    // 執(zhí)行完副作用函數(shù)后會自減
    trackOpBit = 1 << --effectTrackDepth;


    為什么是左移一位,是因為第一位也就是說當前深度只是1,所以保持不變,不用管,從第二位開始。


      if (effectTrackDepth <= maxMarkerBits) {
       // 執(zhí)行副作用函數(shù)之前,使用 `deps[i].w |= trackOpBit`對依賴dep[i]進行標記,追蹤依賴
       initDepMarkers(this)
     } else {
       // 降級方案:完全清理
       cleanupEffect(this)
     }


    如何判斷當前依賴是否已記錄過,通過按位與判斷是否有位已經標識,有就大于0:


    //代表副作用函數(shù)執(zhí)行前被 track 過
    export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
    //代表副作用函數(shù)執(zhí)行后被 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 動作觸發(fā)副作用函數(shù)重新執(zhí)行時,有能力決定副作用函數(shù)執(zhí)行的時機、次數(shù)以及方式。

Vue3響應式的6個細節(jié)我們都了解了,我們可以對副作用工作流做一個全面總結如圖:

Vue2/Vue3的響應式原理是什么Vue3響應式的關鍵在于兩個函數(shù):track(收集依賴)和trigger(觸發(fā)依賴)。

// 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) // 緩存依賴到當前正在運行的副作用依賴數(shù)組
    ...
  }
}
// 根據不同的type從depsMap取出,放入effects,隨后通過run方法將當前的`effect`執(zhí)行
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)[] = []
  // 如果是清除操作,那就要執(zhí)行依賴原始數(shù)據的所有監(jiān)聽方法。因為所有項都被清除了。
  if (type === TriggerOpTypes.CLEAR) { // clear
    // 如果是調用了集合的clear方法, 則要對其所有的副作用進行處理
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newLength) {
        deps.push(dep)
      }
    })
  } else { // set add delete
    // key不為void 0,則說明肯定是SET | ADD | DELETE這三種操作 
    // 然后將依賴這個key的所有監(jiān)聽函數(shù)推到相應隊列中
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    switch (type) { // 根據不同type取出并存入deps
      case TriggerOpTypes.ADD:
         // 如果原始數(shù)據是數(shù)組,則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('length'))
        }
        break
      case TriggerOpTypes.DELETE:
       // 如果原始數(shù)據是數(shù)組,則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元素執(zhí)行run函數(shù)
    triggerEffects(createDep(effects))
  }
}

Vue3響應式原理小結:

Vue3中的副作用函數(shù)其實就是Vue2的依賴

  • activeEffect解決匿名函數(shù)問題。

  • WeakMap、Map、Set存儲對象屬性的相關副作用函數(shù)。

  • 處理副作用函數(shù)時,假如有多個響應式屬性,控制只觸發(fā)生效的屬性或用到的屬性

  • 嵌套副作用函數(shù),使用二進制位記錄嵌套副作用,通過控制二進制位是否清理嵌套副作用實現(xiàn)層級追蹤。

  • track()實現(xiàn)依賴收集、層級依賴追蹤、依賴清理(解決嵌套副作用)

  • trigger()當某個依賴值發(fā)生變化時觸發(fā)的, 根據依賴值的變化類型, 會收集與依賴相關的不同副作用處理對象, 然后逐個觸發(fā)他們的 run 函數(shù), 通過執(zhí)行副作用函數(shù)獲得與依賴變化后對應的最新值

到此,相信大家對“Vue2/Vue3的響應式原理是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI