<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>

          美團(tuán)技術(shù)面:你可以手寫Vue3的響應(yīng)式原理嗎?

          共 15651字,需瀏覽 32分鐘

           ·

          2021-03-15 06:42

          在上一篇嗶哩嗶哩面試官:你可以手寫Vue2的響應(yīng)式原理嗎?中,我們已經(jīng)了解了Vue2中的響應(yīng)式原理并且動手實(shí)現(xiàn)了其核心邏輯。但是Vue2的響應(yīng)式原理是存在一些缺點(diǎn)的:

          • 默認(rèn)會遞歸、消耗較大
          • 數(shù)組響應(yīng)化需要額外實(shí)現(xiàn)
          • 新增/刪除屬性屬性無法監(jiān)聽
          • Map、Set、Class 等無法響應(yīng)式,修改語法有限制

          Vue3使用ES6Proxy特性來解決上面這些問題,本篇文章我將帶大家深入了解Vue3的響應(yīng)式原理并在最后通過Proxy實(shí)現(xiàn)其核心邏輯。

          在開始分析之前,我們先來看一下什么是 Proxy?

          什么是 Proxy?

          ES6 中我們看到了一個讓人耳目一新的屬性——Proxy。我們先看一下概念:

          通過調(diào)用 new Proxy() ,你可以創(chuàng)建一個代理用來替代另一個對象(被稱為目標(biāo)),這個代理對目標(biāo)對象進(jìn)行了虛擬,因此該代理與該目標(biāo)對象表面上可以被當(dāng)作同一個對象來對待。代理允許你攔截在目標(biāo)對象上的底層操作,而這原本是 JS 引擎的內(nèi)部能力。

          Proxy 顧名思義,就是代理的意思,這是一個能讓我們隨意操控對象的特性。當(dāng)我們通過 Proxy 去對一個對象進(jìn)行代理之后,我們將得到一個和被代理對象幾乎完全一樣的對象,并且可以對這個對象進(jìn)行完全的監(jiān)控。

          什么叫完全監(jiān)控?Proxy 所帶來的,是對底層操作的攔截。前面我們在實(shí)現(xiàn)對對象監(jiān)聽時使用了 Object.defineProperty,這個其實(shí)是 JS 提供給我們的高級操作,也就是通過底層封裝之后暴露出來的方法。Proxy 的強(qiáng)大之處在于,我們可以直接攔截對代理對象的底層操作。這樣我們相當(dāng)于從一個對象的底層操作開始實(shí)現(xiàn)對它的監(jiān)聽。

          那么Proxy相比Object.defineProperty都有哪些優(yōu)勢呢?

          Proxy 的優(yōu)勢

          • Proxy 可以直接監(jiān)聽對象而非屬性;
          • Proxy 可以直接監(jiān)聽數(shù)組的變化;
          • Proxy 有多達(dá) 13 種攔截方法,不限于 applyownKeys、deleteProperty、has 等等是 Object.defineProperty 不具備的;
          • Proxy 返回的是一個新對象,我們可以只操作新的對象達(dá)到目的,而 Object.defineProperty 只能遍歷對象屬性直接修改;
          • Proxy 作為新標(biāo)準(zhǔn)將受到瀏覽器廠商重點(diǎn)持續(xù)的性能優(yōu)化,也就是傳說中的新標(biāo)準(zhǔn)的性能紅利。

          Proxy有了大致的了解后,下面我就來分析一下Vue3的響應(yīng)式原理

          響應(yīng)式原理

          這里放一張我之前整理的關(guān)于Vue3響應(yīng)式的流程圖:

          我們來梳理一下流程:

          1、通過state = reactive(target)來定義響應(yīng)式數(shù)據(jù)(這里基于Proxy實(shí)現(xiàn))

          2、通過 effect聲明依賴響應(yīng)式數(shù)據(jù)的函數(shù)cb ( 例如視圖渲染函數(shù)render函數(shù)),并執(zhí)行cb函數(shù),執(zhí)行過程中,會觸發(fā)響應(yīng)式數(shù)據(jù) getter

          3、在響應(yīng)式數(shù)據(jù) getter中進(jìn)行 track依賴收集:存儲響應(yīng)式數(shù)據(jù)與更新函數(shù) cb 的映射關(guān)系,存儲于targetMap

          4、當(dāng)變更響應(yīng)式數(shù)據(jù)時,觸發(fā)trigger,根據(jù)targetMap找到關(guān)聯(lián)的cb并執(zhí)行

          targetMap的結(jié)構(gòu)為:{target: {key: [fn1,fn2]}}

          手寫實(shí)現(xiàn)

          看下我們都要實(shí)現(xiàn)哪些核心函數(shù):

          • reactive:響應(yīng)式核心方法,用于建立數(shù)據(jù)響應(yīng)式
          • effect:聲明響應(yīng)函數(shù) cb,將回調(diào)函數(shù)保存起來備用,立即執(zhí)行一次回調(diào)函數(shù)觸發(fā)它里面一些響應(yīng)數(shù)據(jù)的 getter
          • track:依賴收集,存儲響應(yīng)式數(shù)據(jù)與更新函數(shù) cb 的映射關(guān)系
          • trigger:觸發(fā)更新:根據(jù)映射關(guān)系,執(zhí)行 cb

          建立數(shù)據(jù)響應(yīng)式(reactive函數(shù))

          // 判斷是不是對象
          function isObject(val{
            return typeof val === "object" && val !== null;
          }
          function hasOwn(target, key{
            return target.hasOwnProperty[key];
          }
          // WeakMap: 弱引用映射表
          // 原對象 : 代理過的對象
          let toProxy = new WeakMap();
          // 代理過的對象:原對象
          let toRaw = new WeakMap();
          // 響應(yīng)式核心方法
          function reactive(target{
            // 創(chuàng)建響應(yīng)式對象
            return createReactiveObject(target);
          }
          function createReactiveObject(target{
            // 如果當(dāng)前不是對象,直接返回即可
            if (!isObject(target)) {
              return target;
            }
            // 如果已經(jīng)代理過了,就直接返回代理過的結(jié)果
            let proxy = toProxy.get(target);
            if (proxy) {
              return proxy;
            }
            // 防止代理過的對象再次被代理
            if (toRaw.has(target)) {
              return target;
            }
            let baseHandler = {
              get(target, key, receiver) {
                // Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
                let res = Reflect.get(target, key, receiver);
                // 收集依賴/訂閱 把當(dāng)前的key和effect做映射關(guān)系
                track(target, key);
                // 在get取值的時候才去判斷該值是否是一個對象,如果是則遞歸(這里相比于Vue2中的默認(rèn)遞歸,其實(shí)是一種優(yōu)化)
                return isObject(res) ? reactive(res) : res;
              },
              set(target, key, value, receiver) {
                // 這里需要區(qū)分是新增屬性還是修改屬性
                let hasKey = hasOwn(target, key);
                let oldVal = target[key];
                let res = Reflect.set(target, key, value, receiver);
                if (!hasKey) {
                  console.log("新增屬性");
                  trigger(target, "add", key);
                } else if (oldVal !== value) {
                  console.log("修改屬性");
                  trigger(target, "set", key);
                }
                return res;
              },
              deleteProperty(target, key) {
                let res = Reflect.deleteProperty(target, key);
                return res;
              },
            };
            let observed = new Proxy(target, baseHandler);
            toProxy.set(target, observed);
            toRaw.set(observed, target);
            return observed;
          }

          依賴收集

          其實(shí)就是建立響應(yīng)數(shù)據(jù) key 和更新函數(shù)之間的對應(yīng)關(guān)系,用法如下:

          let obj = reactive({ name"cosen" });
          effect(() => {
            console.log(obj.name);
          });
          obj.name = "senlin";
          obj.name = "senlin1";

          要實(shí)現(xiàn)這部分功能,我們需要完成上面提到的三個方法:

          • effect
          • track
          • trigger

          首先,我們來梳理一下effect需要實(shí)現(xiàn)什么功能。

          經(jīng)過前面的reactive()方法,我們已經(jīng)能夠拿到一個響應(yīng)式的數(shù)據(jù)對象了,每次getset操作都能夠被攔截。

          effect()方法需要實(shí)現(xiàn)的功能就是:每當(dāng)我們修改數(shù)據(jù)的時候,都能夠觸發(fā)傳入effect的回調(diào)函數(shù)執(zhí)行

          effect()方法的回調(diào)函數(shù)要想在數(shù)據(jù)發(fā)生變化后能夠執(zhí)行,必須返回一個響應(yīng)式的effect()函數(shù),所以effect()內(nèi)部會返回一個響應(yīng)式的effect。

          來看下effect方法的實(shí)現(xiàn):

          // 響應(yīng)式 副作用
          function effect(fn{
            const rxEffect = function ({
              try {
                // 捕獲異常
                // 運(yùn)行fn并將effect保存起來
                activeEffectStacks.push(rxEffect);
                return fn();
              } finally {
                activeEffectStacks.pop();
              }
            };
            // 默認(rèn)應(yīng)該先執(zhí)行一次
            rxEffect();
            // 返回響應(yīng)函數(shù)
            return rxEffect;
          }

          此時數(shù)據(jù)發(fā)生變化還無法通知effect的回調(diào)函數(shù)執(zhí)行,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(71, 193, 168);">reactive和effect還未關(guān)聯(lián)起來,也就是說還沒有進(jìn)行依賴收集,所以接下來需要進(jìn)行依賴收集。

          到這里我們需要思考兩個問題:

          1、什么時候收集依賴?

          2、如何收集依賴,如何保存依賴?

          首先第一個問題:什么時候收集依賴?我們需要在取值的時候開始收集依賴,而這對應(yīng)于在Proxy的handlers的get中進(jìn)行取值,也就是在上面的createReactiveObject方法中的:

          get(target, key, receiver) {
            // Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
            let res = Reflect.get(target, key, receiver);
            // 收集依賴/訂閱 把當(dāng)前的key和effect做映射關(guān)系
          +  track(target, key);
            // 在get取值的時候才去判斷該值是否是一個對象,如果是則遞歸(這里相比于Vue2中的默認(rèn)遞歸,其實(shí)是一種優(yōu)化)
            return isObject(res) ? reactive(res) : res;
          },

          對應(yīng)觸發(fā)依賴的執(zhí)行是在Proxy的handlers的get中

          set(target, key, value, receiver) {
            // 這里需要區(qū)分是新增屬性還是修改屬性
            let hasKey = hasOwn(target, key);
            let oldVal = target[key];
            let res = Reflect.set(target, key, value, receiver);
            if (!hasKey) {
              console.log("新增屬性");
          +    trigger(target, "add", key);
            } else if (oldVal !== value) {
              console.log("修改屬性");
          +    trigger(target, "set", key);
            }
            return res;
          },

          然后是第二個問題:如何收集依賴,如何保存依賴?這個其實(shí)我有在上面的流程圖中標(biāo)注:

          {target: {key: [fn1,fn2]}}

          這里解釋一下:首先依賴是一個一個的effect函數(shù),我們可以通過Set集合進(jìn)行存儲,而這個 Set 集合肯定是要和對象的某個key進(jìn)行對應(yīng),即哪些effect依賴了對象中某個key對應(yīng)的值,這個對應(yīng)關(guān)系可以通過一個Map對象進(jìn)行保存。即:

          targetMap: WeakMap{
              target:Map{
                  keySet[cb1,cb2...]
              }
          }

          當(dāng)我們?nèi)≈档臅r候,首先通過該target對象從全局的WeakMap對象中取出對應(yīng)的depsMap對象,然后根據(jù)修改的key獲取到對應(yīng)的dep依賴集合對象,然后將當(dāng)前effect放入到dep依賴集合中,完成依賴的收集。其實(shí)這里對應(yīng)的就是track方法:

          function track(target, key{
            // 拿出棧頂函數(shù)
            let effect = activeEffectStacks[activeEffectStacks.length - 1];
            //
            if (effect) {
              // 獲取target對應(yīng)依賴表
              let depsMap = targetsMap.get(target);
              if (!depsMap) {
                targetsMap.set(target, (depsMap = new Map()));
              }
              // 獲取key對應(yīng)的響應(yīng)函數(shù)集
              let deps = depsMap.get(key);
              // 動態(tài)創(chuàng)建依賴關(guān)系
              if (!deps) {
                depsMap.set(key, (deps = new Set()));
              }
              if (!deps.has(effect)) {
                deps.add(effect);
              }
            }
          }

          當(dāng)我們修改值的時候會觸發(fā)依賴更新,也是通過target對象從全局的WeakMap對象中取出對應(yīng)的depMap對象,然后根據(jù)修改的key取出對應(yīng)的dep依賴集合,并遍歷該集合中的所有effect,并執(zhí)行effect。對應(yīng)就是trigger方法:

          function trigger(target, type, key{
            let depsMap = targetsMap.get(target);
            if (depsMap) {
              let deps = depsMap.get(key);
              if (deps) {
                // 將當(dāng)前key對應(yīng)的effect依次執(zhí)行
                deps.forEach((effect) => {
                  effect();
                });
              }
            }
          }

          完整代碼

          這里整合一下代碼,并在最后通過一個demo來測試一下:

          /**
           * Vue3 響應(yīng)式原理
           *
           */


          // 判斷是不是對象
          function isObject(val{
            return typeof val === "object" && val !== null;
          }
          function hasOwn(target, key{
            return target.hasOwnProperty[key];
          }
          // WeakMap: 弱引用映射表
          // 原對象 : 代理過的對象
          let toProxy = new WeakMap();
          // 代理過的對象:原對象
          let toRaw = new WeakMap();
          // 響應(yīng)式核心方法
          function reactive(target{
            // 創(chuàng)建響應(yīng)式對象
            return createReactiveObject(target);
          }
          function createReactiveObject(target{
            // 如果當(dāng)前不是對象,直接返回即可
            if (!isObject(target)) {
              return target;
            }
            // 如果已經(jīng)代理過了,就直接返回代理過的結(jié)果
            let proxy = toProxy.get(target);
            if (proxy) {
              return proxy;
            }
            // 防止代理過的對象再次被代理
            if (toRaw.has(target)) {
              return target;
            }
            let baseHandler = {
              get(target, key, receiver) {
                // Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
                let res = Reflect.get(target, key, receiver);
                // 收集依賴/訂閱 把當(dāng)前的key和effect做映射關(guān)系
                track(target, key);
                // 在get取值的時候才去判斷該值是否是一個對象,如果是則遞歸(這里相比于Vue2中的默認(rèn)遞歸,其實(shí)是一種優(yōu)化)
                return isObject(res) ? reactive(res) : res;
              },
              set(target, key, value, receiver) {
                // 這里需要區(qū)分是新增屬性還是修改屬性
                let hasKey = hasOwn(target, key);
                let oldVal = target[key];
                let res = Reflect.set(target, key, value, receiver);
                if (!hasKey) {
                  console.log("新增屬性");
                  trigger(target, "add", key);
                } else if (oldVal !== value) {
                  console.log("修改屬性");
                  trigger(target, "set", key);
                }
                return res;
              },
              deleteProperty(target, key) {
                let res = Reflect.deleteProperty(target, key);
                return res;
              },
            };
            let observed = new Proxy(target, baseHandler);
            toProxy.set(target, observed);
            toRaw.set(observed, target);
            return observed;
          }

          // 棧 先進(jìn)后出 {name:[effect]}
          let activeEffectStacks = [];
          let targetsMap = new WeakMap();
          // 如果target中的key發(fā)生變化了,就執(zhí)行數(shù)組里的方法
          function track(target, key{
            // 拿出棧頂函數(shù)
            let effect = activeEffectStacks[activeEffectStacks.length - 1];
            if (effect) {
              // 獲取target對應(yīng)依賴表
              let depsMap = targetsMap.get(target);
              if (!depsMap) {
                targetsMap.set(target, (depsMap = new Map()));
              }
              // 獲取key對應(yīng)的響應(yīng)函數(shù)集
              let deps = depsMap.get(key);
              // 動態(tài)創(chuàng)建依賴關(guān)系
              if (!deps) {
                depsMap.set(key, (deps = new Set()));
              }
              if (!deps.has(effect)) {
                deps.add(effect);
              }
            }
          }
          function trigger(target, type, key{
            let depsMap = targetsMap.get(target);
            if (depsMap) {
              let deps = depsMap.get(key);
              if (deps) {
                // 將當(dāng)前key對應(yīng)的effect依次執(zhí)行
                deps.forEach((effect) => {
                  effect();
                });
              }
            }
          }
          // 響應(yīng)式 副作用
          function effect(fn{
            const rxEffect = function ({
              try {
                // 捕獲異常
                // 運(yùn)行fn并將effect保存起來
                activeEffectStacks.push(rxEffect);
                return fn();
              } finally {
                activeEffectStacks.pop();
              }
            };
            // 默認(rèn)應(yīng)該先執(zhí)行一次
            rxEffect();
            // 返回響應(yīng)函數(shù)
            return rxEffect;
          }

          let obj = reactive({ name"cosen" });
          effect(() => {
            console.log(obj.name);
          });
          obj.name = "senlin";
          obj.name = "senlin";

          順便貼下運(yùn)行的結(jié)果:

          我們能看到雖然執(zhí)行了兩次的obj.name = "senlin"操作,但執(zhí)行結(jié)果卻只執(zhí)行了一次,這個與代碼中定義的toProxytoRaw是有關(guān)的:

          • toProxy:存儲原對象代理過的對象的映射關(guān)系,如果已經(jīng)代理過了,就直接返回代理過的結(jié)果
          • toRaw存儲代理過的對原對象的映射關(guān)系,防止代理過的對象再次被代理。

          總結(jié)

          ok,到這里,我基本把Vue3中關(guān)于響應(yīng)式以及依賴收集的相關(guān)原理和大家梳理了一遍,也自己手動實(shí)現(xiàn)了一個簡易的偽代碼

          本文只是簡單的用偽代碼的形式做了演示,關(guān)于具體實(shí)現(xiàn)細(xì)節(jié),如果你想更深入的了解,大家可以直接去查看Vue3響應(yīng)式部分的源碼[1]。

          參考資料

          [1]

          Vue3響應(yīng)式部分的源碼: https://github.com/vuejs/vue-next/tree/master/packages/reactivity


          瀏覽 38
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  国产孕妇孕交大片孕 | 语音先锋成人片 | 麻豆成人久久久 | 夜夜无码影院 | 天天日天天干天天射 |