<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          70%的人都答錯了的面試題,vue3的ref是如何實現(xiàn)響應(yīng)式的?

          共 13803字,需瀏覽 28分鐘

           ·

          2024-07-31 09:10

          前言

          最近有位面試官分享了一道他的面試題:vue3的ref是如何實現(xiàn)響應(yīng)式的?下面有不少小伙伴回答的是Proxy其實這些小伙伴只回答對了一半

          • 當(dāng)ref接收的是一個對象時確實是依靠Proxy去實現(xiàn)響應(yīng)式的。

          • 但是ref還可以接收 stringnumber 或 boolean 這樣的原始類型,當(dāng)是原始類型時,響應(yīng)式就不是依靠Proxy去實現(xiàn)的,而是在value屬性的gettersetter方法中去實現(xiàn)的響應(yīng)式。

          本文將通過debug的方式帶你搞清楚當(dāng)ref接收的是對象和原始類型時,分別是如何實現(xiàn)響應(yīng)式的。注:本文中使用的vue版本為3.4.19

          看個demo

          還是老套路,我們來搞個demo,index.vue文件代碼如下:

          <template>
            <div>
              <p>count的值為:{{ count }}</p>
              <p>user.count的值為:{{ user.count }}</p>
              <button @click="count++">count++</button>
              <button @click="user.count++">user.count++</button>
            </div>

          </template>

          <script setup lang="ts">
          import { ref } from "vue";

          const count = ref(0);

          const user = ref({
            count: 0,
          });
          </
          script>

          在上面的demo中我們有兩個ref變量,count變量接收的是原始類型,他的值是數(shù)字0。

          count變量渲染在template的p標(biāo)簽中,并且在button的click事件中會count++

          user變量接收的是對象,對象有個count屬性。

          同樣user.count也渲染在另外一個p標(biāo)簽上,并且在另外一個button的click事件中會user.count++

          接下來我將通過debug的方式帶你搞清楚,分別點擊count++user.count++按鈕時是如何實現(xiàn)響應(yīng)式的。

          開始打斷點

          第一步從哪里開始下手打斷點呢?

          既然是要搞清楚ref是如何實現(xiàn)響應(yīng)式的,那么當(dāng)然是給ref打斷點吖,所以我們的第一個斷點是打在const count = ref(0);代碼處。這行代碼是運行時代碼,是跑在瀏覽器中的。

          要在瀏覽器中打斷點,需要在瀏覽器的source面板中打開index.vue文件,然后才能給代碼打上斷點。

          那么第二個問題來了,如何在source面板中找到我們這里的index.vue文件呢?

          很簡單,像是在vscode中一樣使用command+p(windows中應(yīng)該是control+p)就可以喚起一個輸入框。在輸入框里面輸入index.vue,然后點擊回車就可以在source面板中打開index.vue文件。如下圖:

          然后我們就可以在瀏覽器中給const count = ref(0);處打上斷點了。

          RefImpl

          刷新頁面此時斷點將會停留在const count = ref(0);代碼處,讓斷點走進(jìn)ref函數(shù)中。在我們這個場景中簡化后的ref函數(shù)代碼如下:

          function ref(value{
            return createRef(value, false);
          }

          可以看到在ref函數(shù)中實際是直接調(diào)用了createRef函數(shù)。

          接著將斷點走進(jìn)createRef函數(shù),在我們這個場景中簡化后的createRef函數(shù)代碼如下:

          function createRef(rawValue, shallow{
            return new RefImpl(rawValue, shallow);
          }

          從上面的代碼可以看到實際是調(diào)用RefImpl類new了一個對象,傳入的第一個參數(shù)是rawValue,也就是ref綁定的變量值,這個值可以是原始類型,也可以是對象、數(shù)組等。

          接著將斷點走進(jìn)RefImpl類中,在我們這個場景中簡化后的RefImpl類代碼如下:

          class RefImpl {
            private _value: T
            private _rawValue: T

            constructor(value) {
              this._rawValue = toRaw(value);
              this._value = toReactive(value);
            }
            get value() {
              trackRefValue(this);
              return this._value;
            }
            set value(newVal) {
              newVal = toRaw(newVal);
              if (hasChanged(newVal, this._rawValue)) {
                this._rawValue = newVal;
                this._value = toReactive(newVal);
                triggerRefValue(this4, newVal);
              }
            }
          }

          從上面的代碼可以看到RefImpl類由三部分組成:constructor構(gòu)造函數(shù)、value屬性的getter方法、value屬性的setter方法。

          RefImpl類的constructor構(gòu)造函數(shù)

          constructor構(gòu)造函數(shù)中的代碼很簡單,如下:

          constructor(value) {
            this._rawValue = toRaw(value);
            this._value = toReactive(value);
          }

          在構(gòu)造函數(shù)中首先會將toRaw(value)的值賦值給_rawValue屬性中,這個toRaw函數(shù)是vue暴露出來的一個API,他的作用是根據(jù)一個 Vue 創(chuàng)建的代理返回其原始對象。因為ref函數(shù)不光能夠接受普通的對象和原始類型,而且還能接受一個ref對象,所以這里需要使用toRaw(value)拿到原始值存到_rawValue屬性中。

          接著在構(gòu)造函數(shù)中會執(zhí)行toReactive(value)函數(shù),將其執(zhí)行結(jié)果賦值給_value屬性。toReactive函數(shù)看名字你應(yīng)該也猜出來了,如果接收的value是原始類型,那么就直接返回value。如果接收的value不是原始類型(比如對象),那么就返回一個value轉(zhuǎn)換后的響應(yīng)式對象。這個toReactive函數(shù)我們在下面會講。

          _rawValue屬性和_value屬性都是RefImpl類的私有屬性,用于在RefImpl類中使用的,而暴露出去的也只有value屬性。

          經(jīng)過constructor構(gòu)造函數(shù)的處理后,分別給兩個私有屬性賦值了:

          • _rawValue中存的是ref綁定的值的原始值。

          • 如果ref綁定的是原始類型,比如數(shù)字0,那么_value屬性中存的就是數(shù)字0。

            如果ref綁定的是一個對象,那么_value屬性中存的就是綁定的對象轉(zhuǎn)換后的響應(yīng)式對象。

          RefImpl類的value屬性的getter方法

          我們接著來看value屬性的getter方法,代碼如下:

          get value() {
            trackRefValue(this);
            return this._value;
          }

          當(dāng)我們對ref的value屬性進(jìn)行讀操作時就會走到getter方法中。

          我們知道template經(jīng)過編譯后會變成render函數(shù),執(zhí)行render函數(shù)會生成虛擬DOM,然后由虛擬DOM生成真實DOM。

          在執(zhí)行render函數(shù)期間會對count變量進(jìn)行讀操作,所以此時會觸發(fā)count變量的value屬性對應(yīng)的getter方法。

          getter方法中會調(diào)用trackRefValue函數(shù)進(jìn)行依賴收集,由于此時是在執(zhí)行render函數(shù)期間,所以收集的依賴就是render函數(shù)。

          最后在getter方法中會return返回_value私有屬性。

          RefImpl類的value屬性的setter方法

          我們接著來看value屬性的setter方法,代碼如下:

          set value(newVal) {
            newVal = toRaw(newVal);
            if (hasChanged(newVal, this._rawValue)) {
              this._rawValue = newVal;
              this._value = toReactive(newVal);
              triggerRefValue(this4, newVal);
            }
          }

          當(dāng)我們對ref的value的屬性進(jìn)行寫操作時就會走到setter方法中,比如點擊count++按鈕,就會對count的值進(jìn)行+1,觸發(fā)寫操作走到setter方法中。

          setter方法打個斷點,點擊count++按鈕,此時斷點將會走到setter方法中。初始化count的值為0,此時點擊按鈕后新的count值為1,所以在setter方法中接收的newVal的值為1。如下圖:

          從上圖中可以看到新的值newVal的值為1,舊的值this._rawValue的值為0。然后使用if (hasChanged(newVal, this._rawValue))判斷新的值和舊的值是否相等,hasChanged的代碼也很簡單,如下:

          const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

          Object.is方法大家平時可能用的比較少,作用也是判斷兩個值是否相等。和==的區(qū)別為Object.is不會進(jìn)行強制轉(zhuǎn)換,其他的區(qū)別大家可以參看mdn上的文檔。

          使用hasChanged函數(shù)判斷到新的值和舊的值不相等時就會走到if語句里面,首先會執(zhí)行this._rawValue = newVal將私有屬性_rawValue的值更新為最新值。接著就是執(zhí)行this._value = toReactive(newVal)將私有屬性_value的值更新為最新值。

          最后就是執(zhí)行triggerRefValue函數(shù)觸發(fā)收集的依賴,前面我們講過了在執(zhí)行render函數(shù)期間由于對count變量進(jìn)行讀操作。觸發(fā)了getter方法,在getter方法中將render函數(shù)作為依賴進(jìn)行收集了。

          所以此時執(zhí)行triggerRefValue函數(shù)時會將收集的依賴全部取出來執(zhí)行一遍,由于render函數(shù)也是被收集的依賴,所以render函數(shù)會重新執(zhí)行。重新執(zhí)行render函數(shù)時從count變量中取出的值就是新值1,接著就是生成虛擬DOM,然后將虛擬DOM掛載到真實DOM上,最終在頁面上count變量綁定的值已經(jīng)更新為1了。

          看到這里你是不是以為關(guān)于ref實現(xiàn)響應(yīng)式已經(jīng)完啦?

          我們來看demo中的第二個例子,user對象,回顧一下在template和script中關(guān)于user對象的代碼如下:

          <template>
            <div>
              <p>user.count的值為:{{ user.count }}</p>
              <button @click="user.count++">user.count++</button>
            </div>

          </template>

          <script setup lang="ts">
          import { ref } from "vue";

          const user = ref({
            count: 0,
          });
          </
          script>

          在button按鈕的click事件中執(zhí)行的是:user.count++,前面我們講過了對ref的value屬性進(jìn)行寫操作會走到setter方法中。但是我們這里ref綁定的是一個對象,點擊按鈕時也不是對user.value屬性進(jìn)行寫操作,而是對user.value.count屬性進(jìn)行寫操作。所以在這里點擊按鈕不會走到setter方法中,當(dāng)然也不會重新執(zhí)行收集的依賴。

          那么當(dāng)ref綁定的是對象時,我們改變對象的某個屬性時又是怎么做到響應(yīng)式更新的呢?

          這種情況就要用到Proxy了,還記得我們前面講過的RefImpl類的constructor構(gòu)造函數(shù)嗎?代碼如下:

          class RefImpl {
            private _value: T
            private _rawValue: T

            constructor(value) {
              this._rawValue = toRaw(value);
              this._value = toReactive(value);
            }
          }

          其實就是這個toReactive函數(shù)在起作用。

          Proxy實現(xiàn)響應(yīng)式

          還是同樣的套路,這次我們給綁定對象的名為user的ref打個斷點,刷新頁面代碼停留在斷點中。還是和前面的流程一樣最終斷點走到RefImpl類的構(gòu)造函數(shù)中,當(dāng)代碼執(zhí)行到this._value = toReactive(value)時將斷點走進(jìn)toReactive函數(shù)。代碼如下:

          const toReactive = (value) => (isObject(value) ? reactive(value) : value);

          toReactive函數(shù)中判斷了如果當(dāng)前的value是對象,就返回reactive(value),否則就直接返回value。這個reactive函數(shù)你應(yīng)該很熟悉,他會返回一個對象的響應(yīng)式代理。因為reactive不接收number這種原始類型,所以這里才會判斷value是否是對象。

          我們接著將斷點走進(jìn)reactive函數(shù),看看他是如何返回一個響應(yīng)式對象的,在我們這個場景中簡化后的reactive函數(shù)代碼如下:

          function reactive(target{
            return createReactiveObject(
              target,
              false,
              mutableHandlers,
              mutableCollectionHandlers,
              reactiveMap
            );
          }

          從上面的代碼可以看到在reactive函數(shù)中是直接返回了createReactiveObject函數(shù)的調(diào)用,第三個參數(shù)是mutableHandlers。從名字你可能猜到了,他是一個Proxy對象的處理器對象,后面會講。

          接著將斷點走進(jìn)createReactiveObject函數(shù),在我們這個場景中簡化后的代碼如下:

          function createReactiveObject(
            target,
            isReadonly2,
            baseHandlers,
            collectionHandlers,
            proxyMap
          {
            const proxy = new Proxy(target, baseHandlers);
            return proxy;
          }

          在上面的代碼中我們終于看到了大名鼎鼎的Proxy了,這里new了一個Proxy對象。new的時候傳入的第一個參數(shù)是target,這個target就是我們一路傳進(jìn)來的ref綁定的對象。第二個參數(shù)為baseHandlers,是一個Proxy對象的處理器對象。這個baseHandlers是調(diào)用createReactiveObject時傳入的第三個參數(shù),也就是我們前面講過的mutableHandlers對象。

          在這里最終將Proxy代理的對象進(jìn)行返回,我們這個demo中ref綁定的是一個名為user的對象,經(jīng)過前面講過函數(shù)的層層return后,user.value的值就是這里return返回的proxy對象。

          當(dāng)我們對user.value響應(yīng)式對象的屬性進(jìn)行讀操作時,就會觸發(fā)這里Proxy的get攔截。

          當(dāng)我們對user.value響應(yīng)式對象的屬性進(jìn)行寫操作時,就會觸發(fā)這里Proxy的set攔截。

          getset攔截的代碼就在mutableHandlers對象中。

          Proxysetget攔截

          在源碼中使用搜一下mutableHandlers對象,看到他的代碼是這樣的,如下:

          const mutableHandlers = new MutableReactiveHandler();

          從上面的代碼可以看到mutableHandlers對象是使用MutableReactiveHandler類new出來的一個對象。

          我們接著來看MutableReactiveHandler類,在我們這個場景中簡化后的代碼如下:

          class MutableReactiveHandler extends BaseReactiveHandler {
            set(target, key, value, receiver) {
              let oldValue = target[key];

              const result = Reflect.set(target, key, value, receiver);
              if (target === toRaw(receiver)) {
                if (hasChanged(value, oldValue)) {
                  trigger(target, "set", key, value, oldValue);
                }
              }
              return result;
            }
          }

          在上面的代碼中我們看到了set攔截了,但是沒有看到get攔截。

          MutableReactiveHandler類是繼承了BaseReactiveHandler類,我們來看看BaseReactiveHandler類,在我們這個場景中簡化后的BaseReactiveHandler類代碼如下:

          class BaseReactiveHandler {
            get(target, key, receiver) {
              const res = Reflect.get(target, key, receiver);
              track(target, "get", key);
              return res;
            }
          }

          BaseReactiveHandler類中我們找到了get攔截,當(dāng)我們對Proxy代理返回的對象的屬性進(jìn)行讀操作時就會走到get攔截中。

          前面講過了經(jīng)過層層return后user.value的值就是這里的proxy響應(yīng)式對象,而我們在template中使用user.count將其渲染到p標(biāo)簽上,在template中讀取user.count,實際就是在讀取user.value.count的值。

          同樣的template經(jīng)過編譯后會變成render函數(shù),執(zhí)行render函數(shù)會生成虛擬DOM,然后將虛擬DOM轉(zhuǎn)換為真實DOM渲染到瀏覽器上。在執(zhí)行render函數(shù)期間會對user.value.count進(jìn)行讀操作,所以會觸發(fā)BaseReactiveHandler這里的get攔截。

          get攔截中會執(zhí)行track(target, "get", key)函數(shù),執(zhí)行后會將當(dāng)前render函數(shù)作為依賴進(jìn)行收集。到這里依賴收集的部分講完啦,剩下的就是依賴觸發(fā)的部分。

          我們接著來看MutableReactiveHandler,他是繼承了BaseReactiveHandler。在BaseReactiveHandler中有個get攔截,而在MutableReactiveHandler中有個set攔截。

          當(dāng)我們點擊user.count++按鈕時,會對user.value.count進(jìn)行寫操作。由于對count屬性進(jìn)行了寫操作,所以就會走到set攔截中,set攔截代碼如下:

          class MutableReactiveHandler extends BaseReactiveHandler {
            set(target, key, value, receiver) {
              let oldValue = target[key];

              const result = Reflect.set(target, key, value, receiver);
              if (target === toRaw(receiver)) {
                if (hasChanged(value, oldValue)) {
                  trigger(target, "set", key, value, oldValue);
                }
              }
              return result;
            }
          }

          我們先來看看set攔截接收的4個參數(shù),第一個參數(shù)為target,也就是我們proxy代理前的原始對象。第二個參數(shù)為key,進(jìn)行寫操作的屬性,在我們這里key的值就是字符串count。第三個參數(shù)是新的屬性值。

          第四個參數(shù)receiver一般情況下是Proxy返回的代理響應(yīng)式對象。這里為什么會說是一般是呢?看一下MDN上面的解釋你應(yīng)該就能明白了:

          假設(shè)有一段代碼執(zhí)行 obj.name = "jen", obj 不是一個 proxy,且自身不含 name 屬性,但是它的原型鏈上有一個 proxy,那么,那個 proxy 的 set() 處理器會被調(diào)用,而此時,obj 會作為 receiver 參數(shù)傳進(jìn)來。

          接著來看set攔截函數(shù)中的內(nèi)容,首先let oldValue = target[key]拿到舊的屬性值,然后使用Reflect.set(target, key, value, receiver)

          Proxy中一般都是搭配Reflect進(jìn)行使用,在Proxyget攔截中使用Reflect.get,在Proxyset攔截中使用Reflect.set

          這樣做有幾個好處,在set攔截中我們要return一個布爾值表示屬性賦值是否成功。如果使用傳統(tǒng)的obj[key] = value的形式我們是不知道賦值是否成功的,而使用Reflect.set會返回一個結(jié)果表示給對象的屬性賦值是否成功。在set攔截中直接將Reflect.set的結(jié)果進(jìn)行return即可。

          還有一個好處是如果不搭配使用可能會出現(xiàn)this指向不對的問題。

          前面我們講過了receiver可能不是Proxy返回的代理響應(yīng)式對象,所以這里需要使用if (target === toRaw(receiver))進(jìn)行判斷。

          接著就是使用if (hasChanged(value, oldValue))進(jìn)行判斷新的值和舊的值是否相等,如果不相等就執(zhí)行trigger(target, "set", key, value, oldValue)

          這個trigger函數(shù)就是用于依賴觸發(fā),會將收集的依賴全部取出來執(zhí)行一遍,由于render函數(shù)也是被收集的依賴,所以render函數(shù)會重新執(zhí)行。重新執(zhí)行render函數(shù)時從user.value.count屬性中取出的值就是新值1,接著就是生成虛擬DOM,然后將虛擬DOM掛載到真實DOM上,最終在頁面上user.value.count屬性綁定的值已經(jīng)更新為1了。

          這就是當(dāng)ref綁定的是一個對象時,是如何使用Proxy去實現(xiàn)響應(yīng)式的過程。

          看到這里有的小伙伴可能會有一個疑問,為什么ref使用RefImpl類去實現(xiàn),而不是統(tǒng)一使用Proxy去代理一個擁有value屬性的普通對象呢?比如下面這種:

          const proxy = new Proxy(
            {
              value: target,
            },
            baseHandlers
          );

          如果是上面這樣做那么就不需要使用RefImpl類了,全部統(tǒng)一成Proxy去使用響應(yīng)式了。

          但是上面的做法有個問題,就是使用者可以使用delete proxy.valueproxy對象的value屬性給刪除了。而使用RefImpl類的方式去實現(xiàn)就不能使用delete的方法去將value屬性給刪除了。

          總結(jié)

          這篇文章我們講了ref是如何實現(xiàn)響應(yīng)式的,主要分為兩種情況:ref接收的是number這種原始類型、ref接收的是對象這種非原始類型。

          • 當(dāng)ref接收的是number這種原始類型時是依靠RefImpl類的value屬性的gettersetter方法中去實現(xiàn)的響應(yīng)式。

            當(dāng)我們對ref的value屬性進(jìn)行讀操作時會觸發(fā)value的getter方法進(jìn)行依賴收集。

            當(dāng)我們對ref的value屬性進(jìn)行寫操作時會進(jìn)行依賴觸發(fā),重新執(zhí)行render函數(shù),達(dá)到響應(yīng)式的目的。

          • 當(dāng)ref接收的是對象這種非原始類型時,會調(diào)用reactive方法將ref的value屬性轉(zhuǎn)換成一個由Proxy實現(xiàn)的響應(yīng)式對象。

            當(dāng)我們對ref的value屬性對象的某個屬性進(jìn)行讀操作時會觸發(fā)Proxy的get攔截進(jìn)行依賴收集。

            當(dāng)我們對ref的value屬性對象的某個屬性進(jìn)行寫操作時會觸發(fā)Proxy的set攔截進(jìn)行依賴觸發(fā),然后重新執(zhí)行render函數(shù),達(dá)到響應(yīng)式的目的。

          最后我們講了為什么ref不統(tǒng)一使用Proxy去代理一個有value屬性的普通對象去實現(xiàn)響應(yīng)式,而是要多搞個RefImpl類。

          因為如果使用Proxy去代理的有value屬性的普通的對象,可以使用delete proxy.valueproxy對象的value屬性給刪除了。而使用RefImpl類的方式去實現(xiàn)就不能使用delete的方法去將value屬性給刪除了。

          瀏覽 499
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美性爱xxxx黑人xxxx | 亚洲三区视频 | 欧美一级黄色丝袜大片免费 | 九九九国产视频 | 最近最新中文字幕无码 |