【vue3源碼】三、effectScope源碼解析

前言

參考代碼版本:vue 3.2.37

官方文檔:https://vuejs.org/

關于為什么要有effectScope可以參考RFC

使用示例

effectScope可以對內部的響應式對象的副作用effect進行統(tǒng)一管理。

const counter = ref(1)
const scope = effectScope()
scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 處理掉當前作用域內的所有 effect
scope.stop()

effectScope接收一個boolean值,如果傳true代表游離模式萝勤,那么創(chuàng)建的scope不會被父scope收集,通俗來講呐伞,如果是游離模式敌卓,那么scope之間是不存在父子關系的,每一個scope都是獨立的伶氢。

export function effectScope(detached?: boolean) {
  return new EffectScope(detached)
}

effectScope返回一個EffectScope實例趟径。

EffectScope

export class EffectScope {
  active = true
  effects: ReactiveEffect[] = []
  cleanups: (() => void)[] = []

  parent: EffectScope | undefined
  scopes: EffectScope[] | undefined
  /**
   * track a child scope's index in its parent's scopes array for optimized
   * removal
   */
  private index: number | undefined

  constructor(detached = false) {
    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }

  run<T>(fn: () => T): T | undefined {
    if (this.active) {
      try {
        activeEffectScope = this
        return fn()
      } finally {
        activeEffectScope = this.parent
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }

  on() {
    activeEffectScope = this
  }

  off() {
    activeEffectScope = this.parent
  }

  stop(fromParent?: boolean) {
    if (this.active) {
      let i, l
      for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].stop()
      }
      for (i = 0, l = this.cleanups.length; i < l; i++) {
        this.cleanups[i]()
      }
      if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i++) {
          this.scopes[i].stop(true)
        }
      }
      // nested scope, dereference from parent to avoid memory leaks
      if (this.parent && !fromParent) {
        // optimized O(1) removal
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.active = false
    }
  }
}

constructor

EffectScope構造器接收一個參數(shù):detached,默認值為false癣防,代表EffectScope是否是游離狀態(tài)蜗巧。

constructor(detached = false) {
  if (!detached && activeEffectScope) {
    this.parent = activeEffectScope
    this.index =
      (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
        this
      ) - 1
  }
}

如果detachedfalse,并且存在activeEffectScopeactiveEffectScope是個全局變量)的情況蕾盯,會將activeEffectScope賦值給this.parent幕屹,同時會將當前EffectScope實例放入activeEffectScope.scopes中,并將activeEffectScope.scopes最后一個索引賦值給當前EffectScope實例的index屬性。這樣就可以通過this.index來獲取EffectScope實例在父scope中的索引位置望拖。

run

run方法可以接收一個函數(shù)參數(shù)渺尘。

run<T>(fn: () => T): T | undefined {
  if (this.active) {
    try {
      activeEffectScope = this
      return fn()
    } finally {
      activeEffectScope = this.parent
    }
  } else if (__DEV__) {
    warn(`cannot run an inactive effect scope.`)
  }
}

run方法會首先對this.active進行判斷,如果this.activetrue说敏,也就是EffectScope處于激活狀態(tài)鸥跟,那么會將this賦給activeEffectScope,然后執(zhí)行fn盔沫,并返回其執(zhí)行結果锌雀。當fn執(zhí)行完畢后,將activeEffectScope改為this.parent迅诬。

on


on() {
  activeEffectScope = this
}

on方法會將activeEffectScope指向當前EffectScope實例腋逆。

off

off() {
  activeEffectScope = this.parent
}

off方法會將activeEffectScope指向當前EffectScope實例的父scope

stop

stop函數(shù)的作用是清除scope內的所有的響應式效果侈贷,包括子scope惩歉。stop接收一個boolean類型的fromParent參數(shù),如果fromParenttrue俏蛮,stop將不會刪除在父scope中的引用撑蚌。

stop(fromParent?: boolean) {
  if (this.active) {
    let i, l
    // 調用ReactiveEffect.prototype.stop,清除scope內所有響應式效果
    for (i = 0, l = this.effects.length; i < l; i++) {
      this.effects[i].stop()
    }
    // 觸發(fā)scope銷毀時的監(jiān)聽函數(shù)
    for (i = 0, l = this.cleanups.length; i < l; i++) {
      this.cleanups[i]()
    }
    // 銷毀子scope
    if (this.scopes) {
      for (i = 0, l = this.scopes.length; i < l; i++) {
        this.scopes[i].stop(true)
      }
    }
    // 嵌套范圍搏屑,從父級取消引用以避免內存泄漏
    if (this.parent && !fromParent) {
      // 獲取父scope的中最后一個scope
      const last = this.parent.scopes!.pop()
      // last不是當前的scope
      if (last && last !== this) {
        // 將last放在當前scope在parent.scopes中的索引位置
        this.parent.scopes![this.index!] = last
        // last.index改為this.index
        last.index = this.index!
      }
    }
    // 修改scope的激活狀態(tài)
    this.active = false
  }
}

stop中的所有操作都要建立在scope處于激活狀態(tài)的基礎上争涌。首先遍歷this.effects執(zhí)行元素的stop方法。

for (i = 0, l = this.effects.length; i < l; i++) {
  this.effects[i].stop()
}

scope.effects存儲的是在run過程中獲取到的ReactiveEffect實例辣恋,這些ReactiveEffect實例會通過一個recordEffectScope方法被添加到scope.effects中亮垫。

export function recordEffectScope(
  effect: ReactiveEffect,
  scope: EffectScope | undefined = activeEffectScope
) {
  if (scope && scope.active) {
    scope.effects.push(effect)
  }
}

當遍歷完scope.effects或,會遍歷scope.cleanups屬性伟骨。

for (i = 0, l = this.cleanups.length; i < l; i++) {
      this.cleanups[i]()
    }

scope.cleanups中保存的是通過onScopeDispose添加的scope銷毀監(jiān)聽函數(shù)饮潦。

export function onScopeDispose(fn: () => void) {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn)
  } else if (__DEV__) {
    warn(
      `onScopeDispose() is called when there is no active effect scope` +
        ` to be associated with.`
    )
  }
}

如果當前scope存在scopes屬性,意味著當前scope存在子scope携狭,所以需要將所有子scope也進行銷毀继蜡。

if (this.scopes) {
  for (i = 0, l = this.scopes.length; i < l; i++) {
    this.scopes[i].stop(true)
  }
}

如果當前scope存在parent的話,需要將scope從其parent中移除逛腿。

if (this.parent && !fromParent) {
  // 獲取父scope的中最后一個scope
  const last = this.parent.scopes!.pop()
  // last不是當前的scope
  if (last && last !== this) {
    // 將last放在當前scope在parent.scopes中的索引位置
    this.parent.scopes![this.index!] = last
    // last.index改為this.index
    last.index = this.index!
  }
}

這里的移除過邏輯是稀并,先獲取當前scope的父scope中的所有子scope,然后取出最后一個scope单默,這里用last代表(注意last不一定和當前scope相同)碘举,如果last和當前scope不同的話,需要讓last替換當前scope雕凹,這樣我們就把當前scope從其父scope中移除了殴俱。這里僅僅替換是不夠的,因為last.index此時還是之前父scope的最后一個索引枚抵,所以還需要把last.index改為當前scope在其父scope.scopes中的位置线欲。這樣就完全移除了scope

最后汽摹,需要把scope的激活狀態(tài)改為false李丰。

this.active = false

getCurrentScope

getCurrentScope可以獲取當前處于活躍狀態(tài)的EffectScope。這里處于活躍狀態(tài)的EffectScope指得是當前執(zhí)行環(huán)境在所處的那個EffectScope逼泣。

export function getCurrentScope() {
  return activeEffectScope
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末趴泌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拉庶,更是在濱河造成了極大的恐慌嗜憔,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氏仗,死亡現(xiàn)場離奇詭異吉捶,居然都是意外死亡,警方通過查閱死者的電腦和手機皆尔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門呐舔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人慷蠕,你說我怎么就攤上這事珊拼。” “怎么了流炕?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵澎现,是天一觀的道長。 經(jīng)常有香客問我每辟,道長昔头,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任影兽,我火速辦了婚禮揭斧,結果婚禮上,老公的妹妹穿的比我還像新娘峻堰。我一直安慰自己讹开,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布捐名。 她就那樣靜靜地躺著旦万,像睡著了一般。 火紅的嫁衣襯著肌膚如雪镶蹋。 梳的紋絲不亂的頭發(fā)上成艘,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天赏半,我揣著相機與錄音,去河邊找鬼淆两。 笑死断箫,一個胖子當著我的面吹牛,可吹牛的內容都是我干的秋冰。 我是一名探鬼主播仲义,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼剑勾!你這毒婦竟也來了埃撵?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤虽另,失蹤者是張志新(化名)和其女友劉穎暂刘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捂刺,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鸳惯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了叠萍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芝发。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖苛谷,靈堂內的尸體忽然破棺而出辅鲸,到底是詐尸還是另有隱情,我是刑警寧澤腹殿,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布独悴,位于F島的核電站,受9級特大地震影響锣尉,放射性物質發(fā)生泄漏刻炒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一自沧、第九天 我趴在偏房一處隱蔽的房頂上張望坟奥。 院中可真熱鬧,春花似錦拇厢、人聲如沸锣笨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牡属。三九已至献雅,卻和暖如春侨舆,著一層夾襖步出監(jiān)牢的瞬間拾酝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工爷抓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阻塑。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓蓝撇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叮姑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內容