站長資訊網
        最全最豐富的資訊網站

        一文帶你深入剖析vue3的響應式

        本篇文章帶你深度剖析vue3響應式(附腦圖),本文的目標是實現一個基本的vue3的響應式,包含最基礎的情況的處理。

        一文帶你深入剖析vue3的響應式

        本文你將學到

        • 一個基礎的響應式實現 ✅
        • Proxy ✅
        • Reflect ✅
        • 嵌套effect的實現 ✅
        • computed ✅
        • watch ✅
        • 淺響應與深響應 ✅
        • 淺只讀與深只讀 ✅
        • 處理數組長度 ✅
        • ref ✅
        • toRefs ✅

        一文帶你深入剖析vue3的響應式

        一. 實現一個完善的響應式

        所謂的響應式數據的概念,其實最主要的目的就是為數據綁定執行函數,當數據發生變動的時候,再次觸發函數的執行。(學習視頻分享:vue視頻教程)

        例如我們有一個對象data,我們想讓它變成一個響應式數據,當data的數據發生變化時,自動執行effect函數,使nextVal變量的值也進行變化:

        // 定義一個對象 let data = {   name: 'pino',   age: 18 }  let nextVal // 待綁定函數 function effect() {   nextVal = data.age + 1 }  data.age++

        上面的例子中我們將data中的age的值進行變化,但是effect函數并沒有執行,因為現在effect函數與data這個對象不能說是沒啥聯系,簡直就是半毛錢的關系都沒有。

        那么怎么才能使這兩個毫不相關的函數與對象之間產生關聯呢?

        因為一個對象最好可以綁定多個函數,所以有沒有可能我們為data這個對象定義一個空間,每當data的值進行變化的時候就會執行這個空間里的函數?

        答案是有的。

        1. Object.defineProperty()

        js在原生提供了一個用于操作對象的比較底層的api:Object.defineProperty(),它賦予了我們對一個對象的讀取和攔截的操作。

        Object.defineProperty()方法直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 并返回這個對象。

          Object.defineProperty(obj, prop, descriptor)

        參數

        obj 需要定義屬性的對象。 prop 需被定義或修改的屬性名。 descriptor (描述符) 需被定義或修改的屬性的描述符。

        其中descriptor接受一個對象,對象中可以定義以下的屬性描述符,使用屬性描述符對一個對象進行攔截和控制:

        • value——當試圖獲取屬性時所返回的值。

        • writable——該屬性是否可寫。

        • enumerable——該屬性在for in循環中是否會被枚舉。

        • configurable——該屬性是否可被刪除。

        • set()——該屬性的更新操作所調用的函數。

        • get()——獲取屬性值時所調用的函數。

        另外,數據描述符(其中屬性為: enumerable , configurablevaluewritable )與存取描述符(其中屬性為 enumerable , configurableset()get() )之間是有互斥關系的。在定義了 set()get() 之后,描述符會認為存取操作已被 定義了,其中再定義 valuewritable 會引起錯誤。

         let obj = {    name: "小花"  }   Object.defineProperty(obj, 'name', {    // 屬性讀取時進行攔截    get() { return '小明'; },    // 屬性設置時攔截    set(newValue) { obj.name = newValue; },    enumerable: true,    configurable: true  });

        上面的例子中就已經完成對一個對象的最基本的攔截,這也是vue2.x中對對象監聽的方式,但是由于Object.defineProperty()中存在一些問題,例如:

        • 一次只能對一個屬性進行監聽,需要遍歷來對所有屬性監聽

        • 對于對象的新增屬性,需要手動監聽

        • 對于數組通過pushunshift方法增加的元素,也無法監聽

        那么vue3版本中是如何對一個對象進行攔截的呢?答案是es6中的Proxy。

        由于本文主要是vue3版本的響應式的實現,如果想要深入了解Object.defineProperty(),請移步:MDN Object.defineProperty

        2. Proxy

        proxyes6版本出現的一種對對象的操作方式,Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。

        通過proxy我們可以實現對一個對象的讀取,設置等等操作進行攔截,而且直接對對象進行整體攔截,內部提供了多達13種攔截方式。

        • get(target, propKey, receiver) :攔截對象屬性的讀取,比如 proxy.fooproxy['foo'] 。

        • set(target, propKey, value, receiver) :攔截對象屬性的設置,比如 proxy.foo = vproxy['foo'] = v ,返回一個布爾值。

        • has(target, propKey) :攔截 propKey in proxy 的操作,返回一個布爾值。

        • deleteProperty(target, propKey) :攔截 delete proxy[propKey] 的操作,返回一個布爾值。

        • ownKeys(target) :攔截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy)Object.keys(proxy) 、 for...in 循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而 Object.keys() 的返回結果僅包括目標對象自身的可遍歷屬性。

        • getOwnPropertyDescriptor(target, propKey) :攔截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回屬性的描述對象。

        • defineProperty(target, propKey, propDesc) :攔截 Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs) ,返回一個布爾值。

        • preventExtensions(target) :攔截 Object.preventExtensions(proxy) ,返回一個布爾值。

        • getPrototypeOf(target) :攔截 Object.getPrototypeOf(proxy) ,返回一個對象。

        • isExtensible(target) :攔截 Object.isExtensible(proxy) ,返回一個布爾值。

        • setPrototypeOf(target, proto) :攔截 Object.setPrototypeOf(proxy, proto) ,返回一個布爾值。如果目標對象是函數,那么還有兩種額外操作可以攔截。

        • apply(target, object, args) :攔截 Proxy (代理) 實例作為函數調用的操作,比如 proxy(...args) 、 proxy.call(object, ...args) 、 proxy.apply(...) 。

        • construct(target, args) :攔截 Proxy (代理) 實例作為構造函數調用的操作,比如 new proxy(...args)

        如果想要詳細了解proxy,請移步:es6.ruanyifeng.com/#docs/proxy…

         let obj = {    name: "小花"  }  // 只使用get和set進行演示  let obj2 = new Proxy(obj, {    // 讀取攔截    get: function (target, propKey) {      return target[propKey]    },    // 設置攔截    set: function (target, propKey, value) {      // 此處的value為用戶設置的新值      target[propKey] = value    }  });

        3. 一個最簡單的響應式

        有了proxy,我們就可以根據之前的思路實現一個基本的響應式功能了,我們的思路是這樣的:在對象被讀取時把函數收集到一個“倉庫”,在對象的值被設置時觸發倉庫中的函數。

        由此我們可以寫出一個最基本的響應式功能:

        // 定義一個“倉庫”,用于存儲觸發函數 let store = new Set() // 使用proxy進行代理 let data_proxy = new Proxy(data, {   // 攔截讀取操作   get(target, key) {     // 收集依賴函數     store.add(effect)     return target[key]   },   // 攔截設置操作   set(target, key, newVal) {     target[key] = newVal     // 取出所有的依賴函數,執行     store.forEach(fn => fn())   } })

        我們創建了一個用于保存依賴函數的“倉庫”,它是Set類型,然后使用proxy對對象data進行代理,設置了setget攔截函數,用于攔截讀取和設置操作,當讀取屬性時,將依賴函數effect存儲到“倉庫”中,當設置屬性值時,將依賴函數從“倉庫”中取出并重新執行。

        還有一個小問題,怎么觸發對象的讀取操作呢?我們可以直接調用一次effect函數,如果在effect函數中存在需要收集的屬性,那么執行一次effect函數也是比較符合常理的。

        // 定義一個對象 let data = {   name: 'pino',   age: 18 }  let nextVal // 待綁定函數 function effect() {   // 依賴函數在這里被收集   // 當調用data.age時,effect函數被收集到“倉庫”中   nextVal = data.age + 1   console.log(nextVal) } // 執行依賴函數 effect() // 19  setTimeout(()=>{   // 使用proxy進行代理后,使用代理后的對象名   // 觸發設置操作,此時會取出effect函數進行執行   data_proxy.age++ // 2秒后輸出 20 }, 2000)

        一開始會執行一次effect,然后函數兩秒鐘后會執行代理對象設置操作,再次執行effect函數,輸出20。

        一文帶你深入剖析vue3的響應式

        此時整個響應式流程的功能是這樣的:

        階段一,在屬性被讀取時,為對象屬性收集依賴函數:

        一文帶你深入剖析vue3的響應式

        階段二,當屬性發生改變時,再次觸發依賴函數

        一文帶你深入剖析vue3的響應式

        這樣就實現了一個最基本的響應式的功能。

        4. 完善

        問題一

        其實上面實現的功能還有很大的缺陷,首先最明顯的問題是,我們把effect函數給固定了,如果用戶使用的依賴函數不叫effect怎么辦,顯然我們的功能就不能正常運行了。

        所以先來進行第一步的優化:抽離出一個公共方法,依賴函數由用戶來傳遞參數

        我們使用effect函數來接受用戶傳遞的依賴函數:

        // effect接受一個函數,把這個匿名函數當作依賴函數 function effect(fn) {   // 執行依賴函數   fn() }  // 使用 effect(()=>{   nextVal = data.age + 1   console.log(nextVal) })

        但是effect函數內部只是執行了,在get函數中怎么能知道用戶傳遞的依賴函數是什么呢,這兩個操作并不在一個函數內???其實可以使用一個全局變量activeEffect來保存當前正在處理的依賴函數。

        修改后的effect函數是這樣的:

        let activeEffect // 新增  function effect(fn) {   // 保存到全局變量activeEffect   activeEffect = fn // 新增   // 執行依賴函數   fn() }  // 而在get內部只需要?收集activeEffect即可 get(target, key) {   store.add(activeEffect)   return target[key] },

        調用effect函數傳遞一個匿名函數作為依賴函數,當執行時,首先會把匿名函數賦值給全局變量activeEffect,然后觸發屬性的讀取操作,進而觸發get攔截,將全局變量activeEffect進行收集。

        問題二

        從上面我們定義的對象可以看到,我們的對象data中有兩個屬性,上面的例子中我們只給age建立了響應式連接,那么如果我現在也想給name建立響應式連接怎么辦呢?那好說,那我們直接向“倉庫”中繼續添加依賴函數不就行了嗎。

        其實這會帶來很嚴重的問題,由于 “倉庫”并沒有與被操作的目標屬性之間建立聯系,而上面我們的實現只是將整個“倉庫”遍歷了一遍,所以無論哪個屬性被觸發,都會將“倉庫”中所有的依賴函數都取出來執行一遍,因為整個執行程序中可能有很多對象及屬性都設置了響應式聯系,這將會帶來很大的性能浪費。所謂牽一發而動全身,這種結果顯然不是我們想要的。

        let data = {   name: 'pino',   age: 18 }

        一文帶你深入剖析vue3的響應式

        所以我們要重新設計一下“倉庫”的數據結構,目的就是為了可以在屬性這個粒度下和“倉庫”建立明確的聯系。

        就拿我們上面進行操作的對象來說,存在著兩層的結構,有兩個角色,對象data以及屬性name``age

        let data = {  name: 'pino',  age: 18 }

        他們的關系是這樣的:

        data        -> name                -> effectFn  // 如果兩個屬性讀取了同一個依賴函數 data        -> name                -> effectFn        -> age                -> effectFn  // 如果兩個屬性讀取了不同的依賴函數 data        -> name                -> effectFn        -> age                -> effectFn1  // 如果是兩個不同的對象 data        -> name                -> effectFn        -> age                -> effectFn1 data2        -> addr                -> effectFn

        接下來我們實現一下代碼,為了方便調用,將設置響應式數據的操作封裝為一個函數reactive

        let newObj = new Proxy(obj, {   // 讀取攔截   get: function (target, propKey) {   },   // 設置攔截   set: function (target, propKey, value) {   } });  // 封裝為  function reactive(obj) {   return new Proxy(obj, {     // 讀取攔截     get: function (target, propKey) {     },     // 設置攔截     set: function (target, propKey, value) {     }   }); }
        function reactive(obj) {   return new Proxy(obj, {     get(target, key) {       // 收集依賴       track(target, key)       return target[key]     },     set(target, key, newVal) {       target[key] = newVal       // 觸發依賴       trigger(target, key)     }   }) }  function track(target, key) {   // 如果沒有依賴函數,則不需要進行收集。直接return   if (!activeEffect) return    // 獲取target,也就是對象名,對應上面例子中的data   let depsMap = store.get(target)   if (!depsMap) {     store.set(target, (depsMap = new Map()))   }   // 獲取對象中的key值,對應上面例子中的name或age   let deps = depsMap.get(key)    if (!deps) {     depsMap.set(key, (deps = new Set()))   }   // 收集依賴函數   deps.add(activeEffect) }  function trigger(target, key) {   // 取出對象對應的Map   let depsMap = store.get(target)   if(!depsMap) return   // 取出key所對應的Set   let deps = depsMap.get(key)   // 執行依賴函數   deps && deps.forEach(fn => fn()); }

        我們將讀取操作封裝為了函數track,觸發依賴函數的動作封裝為了trigger方便調用,現在的整個“倉庫”結構是這樣的:

        一文帶你深入剖析vue3的響應式

        WeakMap

        可能有人會問了,為什么設置“倉庫”要使用WeakMap呢,我使用一個普通對象來創建不行嗎? –

        WeakMap 結構與 Map 結構類似,也是用于生成鍵值對的集合。

        WeakMapMap 的區別有兩點。

        首先, WeakMap 只接受對象作為鍵名( null 除外),不接受其他類型的值作為鍵名。

        const map = new WeakMap(); map.set(1, 2) // TypeError: 1 is not an object! map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key map.set(null, 2) // TypeError: Invalid value used as weak map key

        上面代碼中,如果將數值 1Symbol 值作為 WeakMap 的鍵名,都會報錯。

        其次, WeakMap 的鍵名所指向的對象,不計入垃圾回收機制。

        WeakMap 的設計目的在于,有時我們想在某個對象上面存放一些數據,但是這會形成對于這個對象的引用。請看下面的例子。

        const e1 = document.getElementById('foo'); const e2 = document.getElementById('bar'); const arr = [     [e1, 'foo 元素'],     [e2, 'bar 元素'], ];

        上面代碼中, e1e2 是兩個對象,我們通過 arr 數組對這兩個對象添加一些文字說明。這就形成了 arre1e2 的引用。

        一旦不再需要這兩個對象,我們就必須手動刪除這個引用,否則垃圾回收機制就不會釋放 e1e2 占用的內存。

        // 不需要 e1 和 e2 的時候 // 必須手動刪除引用 arr [0] = null; arr [1] = null;

        上面這樣的寫法顯然很不方便。一旦忘了寫,就會造成內存泄露。

        它的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內。因此,只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的內存。也就是說,一旦不再需要,WeakMap 里面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。

        如果我們上文中target對象沒有任何引用了,那么說明用戶已經不需要用到它了,這時垃圾回收器會自動執行回收,而如果使用Map來進行收集,那么即使其他地方的代碼已經對target沒有任何引用,這個target也不會被回收。

        Reflect

        在vue3中的實現方式和我們的基本實現還有一點不同就是在vue3中是使用Reflect來操作數據的,例如:

        function reactive(obj) {  return new Proxy(obj, {    get(target, key, receiver) {      track(target, key)      // 使用Reflect.get操作讀取數據      return Reflect.get(target, key, receiver)    },    set(target, key, value, receiver) {      trigger(target, key)      // 使用Reflect.set來操作觸發數據      Reflect.set(target, key, value, receiver)    }  }) }

        那么為什么要使用Reflect來操作數據呢,像之前一樣直接操作原對象不行嗎,我們先來看一下一種特殊的情況:

        const obj = {   foo: 1,   get bar() {     return this.foo   } }

        effect依賴函數中通過代理對象p訪問bar屬性:

        effect(()=>{   console.log(p.bar) // 1 })

        可以分析一下這個過程發生了什么,當effect函數被調用時,會讀取p.bar屬性,他發現p.bar屬性是一個訪問器屬性,因此會執行getter函數,由于在getter函數中通過this.foo讀取了foo屬性的值,因此我們會認為副作用函數與屬性foo之間也會建立聯系,當修改p.foo的值的時候因該也能夠觸發響應,使依賴函數重新執行才對,然而當修改p.foo的時候,并沒有觸發依賴函數:

        p.foo++

        實際上問題就出在bar屬性中的訪問器函數getter上:

        get bar() {   // 這個this究竟指向誰?   return this.foo }

        當通過代理對象p訪問p.bar,這回觸發代理對象的get攔截函數執行:

        const p = new Proxt(obj, {   get(target, key) {     track(target, key)     return target[key]   } })

        可以看到在get的攔截函數中,通過target[key]返回屬性值,其中target是原始對象obj,而key就是字符串'bar',所以target[key]就相當于obj.bar。因此當我們使用p.bar訪問bar屬性時,他的getter函數內的this其實指向原始對象obj,這說明我們最終訪問的是obj.foo。所以在依賴函數內部通過原始對象訪問他的某個屬性是不會建立響應聯系的:

        effect(()=>{   // obj是原始數據,不是代理對象,不會建立響應聯系   obj.foo })

        那么怎么解決這個問題呢,這時候就需要用到 Reflect出場了。

        先來看一下Reflect是啥:

        Reflect函數的功能就是提供了訪問一個對象屬性的默認行為,例如下面兩個操作是等價的:

        const obj = { foo: 1 }  // 直接讀取 console.log(obj.foo) //1  // 使用Reflect.get讀取 console.log(Reflect.get(obj, 'foo')) // 1

        實際上Reflect.get函數還能接受第三個函數,即制定接受者receiver,可以把它理解為函數調用過程中的this

        const obj = { foo: 1 }  console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 輸出的是 2 而不是 1

        在這段代碼中,指定了第三個參數receiver為一個對象{ foo: 2 },這是讀取到的值時receiver對象的foo屬性。

        而我們上文中的問題的解決方法就是在操作對象數據的時候通過Reflect的方法來傳遞第三個參數receiver,它代表誰在讀取屬性:

        const p = new Proxt(obj, {   // 讀取屬性接收receiver   get(target, key, receiver) {     track(target, key)     // 使用Reflect.get返回讀取到的屬性值     return Reflect.get(target, key, receiver)   } })

        當使用代理對象p訪問bar屬性時,那么receiver就是p,可以把它理解為函數調用中的this。

        所以我們改造一下reactive函數的實現:

        function reactive(obj) {  return new Proxy(obj, {    get(target, key, receiver) {      track(target, key)      return Reflect.get(target, key, receiver)    },    set(target, key, value, receiver) {      trigger(target, key)      Reflect.set(target, key, value, receiver)    }  }) }

        擴展

        Proxy -> get()

        get 方法用于攔截某個屬性的讀取操作,可以接受三個參數,依次為目標對象、屬性名和 proxy (代理) 實例本身(嚴格地說,是操作行為所針對的對象),其中最后一個參數可選。

        Reflect.get(target, name, receiver)

        Reflect.get 方法查找并返回 target 對象的 name 屬性,如果沒有該屬性,則返回 undefined

        var myObject = { foo: 1, bar: 2, get baz() {   return this.foo + this.bar; }, }  Reflect.get(myObject, 'foo') // 1 Reflect.get(myObject, 'bar') // 2 Reflect.get(myObject, 'baz') // 3

        如果 name 屬性部署了讀取函數( getter ),則讀取函數的 this 綁定 receiver 。

        var myObject = { foo: 1, bar: 2, get baz() {   return this.foo + this.bar; }, };  var myReceiverObject = { foo: 4, bar: 4, };  Reflect.get(myObject, 'baz', myReceiverObject) // 8

        如果第一個參數不是對象, Reflect.get 方法會報錯。

        Reflect.get(1, 'foo') // 報錯 Reflect.get(false, 'foo') // 報錯

        Reflect.set(target, name, value, receiver)

        Reflect.set 方法設置 target 對象的 name 屬性等于 value

        var myObject = { foo: 1, set bar(value) {   return this.foo = value; }, }  myObject.foo // 1  Reflect.set(myObject, 'foo', 2); myObject.foo // 2  Reflect.set(myObject, 'bar', 3) myObject.foo // 3

        如果 name 屬性設置了賦值函數,則賦值函數的 this 綁定 receiver

        var myObject = { foo: 4, set bar(value) {   return this.foo = value; }, };  var myReceiverObject = { foo: 0, };  Reflect.set(myObject, 'bar', 1, myReceiverObject); myObject.foo // 4 myReceiverObject.foo // 1

        注意,如果 Proxy 對象和 Reflect 對象聯合使用,前者攔截賦值操作,后者完成賦值的默認行為,而且傳入了 receiver ,那么 Reflect.set 會觸發 Proxy.defineProperty 攔截。

        let p = { a: 'a' };  let handler = { set(target, key, value, receiver) {   console.log('set');   Reflect.set(target, key, value, receiver) }, defineProperty(target, key, attribute) {   console.log('defineProperty');   Reflect.defineProperty(target, key, attribute); } };  let obj = new Proxy(p, handler); obj.a = 'A'; // set // defineProperty

        上面代碼中, Proxy.set 攔截里面使用了 Reflect.set ,而且傳入了 receiver ,導致觸發 Proxy.defineProperty 攔截。這是因為 Proxy.setreceiver 參數總是指向當前的 Proxy 實例(即上例的 obj ),而 Reflect.set 一旦傳入 receiver ,就會將屬性賦值到 receiver 上面(即 obj ),導致觸發 defineProperty 攔截。如果 Reflect.set 沒有傳入 receiver ,那么就不會觸發 defineProperty 攔截。

        let p = { a: 'a' };  let handler = { set(target, key, value, receiver) {   console.log('set');   Reflect.set(target, key, value) }, defineProperty(target, key, attribute) {   console.log('defineProperty');   Reflect.defineProperty(target, key, attribute); } };  let obj = new Proxy(p, handler); obj.a = 'A'; // set

        如果第一個參數不是對象, Reflect.set 會報錯。

        Reflect.set(1, 'foo', {}) // 報錯 Reflect.set(false, 'foo', {}) // 報錯

        到這里,一個非?;镜捻憫降墓δ芫屯瓿闪耍w代碼如下:

        // 定義倉庫 let store = new WeakMap() // 定義當前處理的依賴函數 let activeEffect  function effect(fn) {   // 將操作包裝為一個函數   const effectFn = ()=> {     activeEffect = effectFn     fn()   }   effectFn() }  function reactive(obj) {   return new Proxy(obj, {     get(target, key, receiver) {       // 收集依賴       track(target, key)       return Reflect.get(target, key, receiver)      },     set(target, key, newVal, receiver) {       // 觸發依賴       trigger(target, key)       Reflect.set(target, key, newVal, receiver)     }   }) }  function track(target, key) {   // 如果沒有依賴函數,則不需要進行收集。直接return   if (!activeEffect) return    // 獲取target,也就是對象名   let depsMap = store.get(target)   if (!depsMap) {     store.set(target, (depsMap = new Map()))   }   // 獲取對象中的key值   let deps = depsMap.get(key)    if (!deps) {     depsMap.set(key, (deps = new Set()))   }   // 收集依賴函數   deps.add(activeEffect) }  function trigger(target, key) {   // 取出對象對應的Map   let depsMap = store.get(target)   if (!depsMap) return   // 取出key所對應的Set   const effects = depsMap.get(key)   // 執行依賴函數   // 為避免污染,創建一個新的Set來進行執行依賴函數   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {       effectsToRun.add(effectFn)   })    effectsToRun.forEach(effect => effect()) }

        二. 嵌套effect

        在日常的工作中,effect函數并不是單獨存在的,比如在vue的渲染函數中,各個組件之間互相嵌套,那么他們在組件中所使用的effect是必然會發生嵌套的:

        effect(function effectFn1() {   effect(function effectFn1() {     // ...   }) })

        當組件中發生嵌套時,此時的渲染函數:

        effect(()=>{   Father.render()    //嵌套子組件   effect(()=>{     Son.render()   }) })

        但是此時我們實現的effect并沒有這個能力,執行下面這段代碼,并不會出現意料之中的行為:

        const data = { foo: 'pino', bar: '在干啥' } // 創建代理對象 const obj = reactive(data)  let p1, p2; // 設置obj.foo的依賴函數 effect(function effect1(){   console.log('effect1執行');   // 嵌套,obj.bar的依賴函數   effect(function effect2(){     p2 = obj.bar      console.log('effect2執行')   })   p1 = obj.foo })

        在這段代碼中,定義了代理對象obj,里面有兩個屬性foobar,然后定義了收集foo的依賴函數,在依賴函數的內部又定義了bar的依賴函數。 在理想狀態下,我們希望依賴函數與屬性之間的關系如下:

        obj         -> foo                 -> effect1         -> bar                 -> effect2

        當修改obj.foo的值的時候,會觸發effect1函數執行,由于effect2函數在effect函數內部,所以effect2函數也會執行,而當修改obj.bar時,只會觸發effect2函數。接下來修改一下obj.foo

        const data = { foo: 'pino', bar: '在干啥' } // 創建代理對象 const obj = reactive(data)  let p1, p2; // 設置obj.foo的依賴函數 effect(function effect1(){   console.log('effect1執行');   // 嵌套,obj.bar的依賴函數   effect(function effect2(){     p2 = obj.bar      console.log('effect2執行')   })   p1 = obj.foo })  // 修改obj.foo的值 obj.foo = '前來買瓜'

        看一下執行結果:

        一文帶你深入剖析vue3的響應式

        可以看到effect2函數竟然執行了兩次?按照之前的分析,當obj.foo被修改后,應當觸發effect1這個依賴函數,但是為什么會effect2會被再次執行呢? 來看一下我們effect函數的實現:

        function effect(fn) {   // 將依賴函數進行包裝   const effectFn = ()=> {     activeEffect = effectFn     fn()   }   effectFn() }

        其實在這里就已經很容易看出問題了,在接受用戶傳遞過來的值時,我們直接將activeEffect這個全局變量進行了覆蓋!所以在內部執行完后,activeEffect這個變量就已經是effect2函數了,而且永遠不會再次變為effect1,此時再進行收集依賴函數時,永遠收集的都是effect2函數。

        那么如何解決這種問題呢,這種情況可以借鑒棧結構來進行處理,棧結構是一種后進先出的結構,在依賴函數執行時,將當前的依賴函數壓入棧中,等待依賴函數執行完畢后將其從棧中彈出,始終activeEffect指向棧頂的依賴函數。

        // 增加effect調用棧 const effectStack = [] // 新增  function effect(fn) {   let effectFn = function () {     activeEffect = effectFn     // 入棧     effectStack.push(effectFn) // 新增     // 執行函數的時候進行get收集     fn()     // 收集完畢后彈出     effectStack.pop() // 新增     // 始終指向棧頂     activeEffect = effectStack[effectStack.length - 1] // 新增   }    effectFn() }

        一文帶你深入剖析vue3的響應式

        此時兩個屬性所對應的依賴函數便不會發生錯亂了。

        三. 避免無限循環

        如果現在將effect函數中傳遞的依賴函數改一下:

        // 定義一個對象 let data = {   name: 'pino',   age: 18 } // 將data更改為響應式對象 let obj = reactive(data)  effect(() => {   obj.age++ })

        在這段代碼中,我們將代理對象objage屬性執行自增操作,但是執行這段代碼,卻發現竟然棧溢出了?這是怎么回事呢?

        一文帶你深入剖析vue3的響應式

        其實在effect中處理依賴函數時,obj.age++的操作其實可以看做是這樣的:

        effect(()=>{   // 等式右邊的操作是先執行了一次讀取操作   obj.age = obj.age + 1 })

        這段代碼的執行流程是這樣的:首先讀取obj.foo的值,這會觸發track函數進行收集操作,也就是將當前的依賴函數收集到“倉庫”中,接著將其加1后再賦值給obj.foo,此時會觸發trigger操作,即把“倉庫”中的依賴函數取出并執行。但是此時該依賴函數正在執行中,還沒有執行完就要再次開始下一次的執行。就會導致無限的遞歸調用自己。

        解決這個問題,其實只需要在觸發函數執行時,判斷當前取出的依賴函數是否等于activeEffect,就可以避免重復執行同一個依賴函數。

        function trigger(target, key) {   // 取出對象對應的Map   let depsMap = store.get(target)   if (!depsMap) return   // 取出key所對應的Set   const effects = depsMap.get(key)   // // 執行依賴函數   // 因為刪除又添加都在同一個deps中,所以會產生無限執行   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {     // 如果trigger出發執行的副作用函數與當前正在執行的副作用函數相同,則不觸發執行     if (effectFn !== activeEffect) {             effectsToRun.add(effectFn)     }   })    effectsToRun.forEach(effect => effect()) }

        四.computed

        computed是vue3中的計算屬性,它可以根據傳入的參數進行響應式的處理:

        const plusOne = computed(() => count.value + 1)

        根據computed的用法,我們可以知道它的幾個特點:

        • 懶執行,值變化時才會觸發

        • 緩存功能,如果值沒有變化,就會返回上一次的執行結果 在實現這兩個核心功能之前,我們先來改造一下之前實現的effect函數。

        怎么能使effect函數變成懶執行呢,比如計算屬性的這種功能,我們不想要他立即執行,而是希望在它需要的時候才執行。

        這時候我們可以在effect函數中傳遞第二個參數,一個對象,用來設置一些額外的功能。

        function effect(fn, options = {}) { // 修改    let effectFn = function () {     activeEffect = effectFn     effectStack.push(effectFn)     fn()     effectStack.pop()     activeEffect = effectStack[effectStack.length - 1]   }   // 只有當非lazy的時候才直接執行   if(!options.lazy) {     effectFn()   }   // 將依賴函數組為返回值進行返回   return effectFn // 新增 }

        這時,如果傳遞了lazy屬性,那么該effect將不會立即執行,需要手動進行執行:

        const effectFn = effect(()=>{   console.log(obj.foo) }, { lazy: true })  // 手動執行 effectFn()

        但是如果我們想要獲取手動執行后的值呢,這時只需要在effect函數中將其返回即可。

        function effect(fn, options = {}) {    let effectFn = function () {     activeEffect = effectFn     effectStack.push(effectFn)     // 保存返回值     const res = fn() // 新增     effectStack.pop()     activeEffect = effectStack[effectStack.length - 1]      return res // 新增   }   // 只有當非lazy的時候才直接執行   if(!options.lazy) {     effectFn()   }   // 將依賴函數組為返回值進行返回   return effectFn }

        接下來開始實現computed函數:

        function computed(getter) {   // 創建一個可手動調用的依賴函數   const effectFn = effect(getter, {     lazy: true   })   // 當對象被訪問的時候才調用依賴函數   const obj = {     get value() {       return effectFn()     }   }    return obj }

        但是此時還做不到對值進行緩存和對比,增加兩個變量,一個存儲執行的值,另一個為一個開關,表示“是否可以重新執行依賴函數”:

        function computed(getter) {   // 定義value保存執行結果   // isRun表示是否需要執行依賴函數   let value, isRun = true; // 新增   const effectFn = effect(getter, {     lazy: true   })    const obj = {     get value() {       // 增加判斷,isRun為true時才會重新執行       if(isRun) {  // 新增         // 保存執行結果         value = effectFn() // 新增         // 執行完畢后再次重置執行開關         isRun = false // 新增       }        return value     }   }    return obj }

        但是上面的實現還有一個問題,就是好像isRun執行一次后好像永遠都不會變成true了,我們的本意是在數據發生變動的時候需要再次觸發依賴函數,也就是將isRun變為true,實現這種效果,需要我們為options再傳遞一個函數,用于用戶自定義的調度執行。

        function effect(fn, options = {}) {    let effectFn = function () {     activeEffect = effectFn     effectStack.push(effectFn)     const res = fn()      effectStack.pop()     activeEffect = effectStack[effectStack.length - 1]      return res    }   // 掛載用戶自定義的調度執行器   effectFn.options = options // 新增    if(!options.lazy) {     effectFn()   }   return effectFn }

        接下來需要修改一下trigger如果傳遞了scheduler這個函數,那么只執行scheduler這個函數而不執行依賴函數:

        function trigger(target, key) {   let depsMap = store.get(target)   if (!depsMap) return   const effects = depsMap.get(key)   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {     if (effectFn !== activeEffect) {         effectsToRun.add(effectFn)     }   })    effectsToRun.forEach(effect => {     // 如果存在調度器scheduler,那么直接調用該調度器,并將依賴函數進行傳遞     if(effectFn.options.scheduler) { // 新增       effectFn.options.scheduler(effect) // 新增     } else {       effect()     }   }) }

        那么在computed中就可以實現重置執行開關isRun的操作了:

        function computed(getter) {   // 定義value保存執行結果   // isRun表示是否需要執行依賴函數   let value, isRun = true; // 新增   const effectFn = effect(getter, {     lazy: true,     scheduler() {       if(!isRun) {         isRun = true       }     }   })    const obj = {     get value() {       // 增加判斷,isRun為true時才會重新執行       if(isRun) {  // 新增         // 保存執行結果         value = effectFn() // 新增         // 執行完畢后再次重置執行開關         isRun = false // 新增       }        return value     }   }    return obj }

        computed傳入的依賴函數中的值發生改變時,會觸發響應式對象的trigger函數,而計算屬性創建響應式對象時傳入了scheduler,所以當數據改變時,只會執行scheduler函數,在scheduler函數內我們將執行開關重置為true,再下次訪問數據觸發get函數時,就會重新執行依賴函數。這也就實現了當數據發生改變時,會再次觸發依賴函數的功能了。

        為了避免計算屬性被另外一個依賴函數調用而失去響應,我們還需要為計算屬性單獨進行綁定響應式的功能,形成一個effect嵌套。

        function computed(getter) {   let value, isRun = true;    const effectFn = effect(getter, {     lazy: true,     scheduler() {       if(!isRun) {         isRun = true         // 當計算屬性依賴的響應式數據發生變化時,手動調用trigger函數觸發響應         trigger(obj, 'value') // 新增       }     }   })    const obj = {     get value() {       if(isRun) {          value = effectFn()         isRun = false        }       // 當讀取value時,手動調用track函數進行追蹤           track(obj, 'value')       return value     }   }    return obj }

        五. watch

        先來看一下watch函數的用法,它的用法也非常簡單:

        watch(obj, ()=>{   console.log(改變了) })  // 修改數據,觸發watch函數 obj.age++

        watch接受兩個參數,第一個參數為綁定的響應式數據,第二個參數為依賴函數,我們依然可以沿用之前的思路來進行處理,利用effect以及scheduler來改變觸發執行時機。

        function watch(source, fn) {   effect(     // 遞歸讀取對象中的每一項,變為響應式數據,綁定依賴函數         ()=> bindData(source),     {       scheduler() {         // 當數據發生改變時,調用依賴函數         fn()       }     }   ) } // readData保存已讀取過的數據,防止重復讀取 function bindData(value, readData = new Set()) {   // 此處只考慮對象的情況,如果值已被讀取/值不存在/值不為對象,那么直接返回   if(typeof value !== 'object' || value == null || readData.has(value)) return   // 保存已讀取對象   readData.add(value)   // 遍歷對象   for(const key in value) {     // 遞歸進行讀取     bindData(value[key], readData)   }   return value }

        watch函數還有另外一種用法,就是除了接收對象,還可以接受一個getter函數,例如:

        watch(     ()=> obj.age,     ()=> {       console.log('改變了')     }  )

        這種情況下只需要將用戶傳入的getter將我們自定義的bindData替代即可:

        function watch(source, fn) {   let getter = typeof source === 'function' ? source : (()=> bindData(source))    effect(     // 執行getter         ()=> getter(),     {       scheduler() {         // 當數據發生改變時,調用依賴函數         fn()       }     }   ) }

        其實watch函數還有一個很重要的功能:就是在用戶傳遞的依賴函數中可以獲取新值和舊值,但是我們目前還做不到這一點。實現這個功能我們可以配置前文中的lazy屬性來實現。 來回顧一下lazy屬性:設置了lazy之后一開始不會執行依賴函數,手動執行時會返回執行結果:

        function watch(source, fn) {   let getter = typeof source === 'function' ? source : (()=> bindData(source))   // 定義新值與舊值   let newVal, oldVal; // 新增   const effectFn = effect(     // 執行getter         ()=> getter(),     {       lazy: true,       scheduler() {         // 在scheduler重新執行依賴函數,得到新值         newVal = effectFn() // 新增         fn(newVal, oldVal) // 新增         // 執行完畢后更新舊值         oldVal = newVal // 新增       }     }   )   // 手動調用依賴函數,取得舊值   oldVal = effectFn() // 新增 }

        此外,watch函數還有一個功能,就是可以自定義執行時機,比如immediate屬性,他會在創建時立即執行一次:

        watch(obj, ()=>{   console.log('改變了') }, {   immediate: true })

        我們可以把scheduler封裝為一個函數,以便在不同的時機去調用他:

        function watch(source, fn, options = {}) {   let getter = typeof source === 'function' ? source : (()=> bindData(source))    let newVal, oldVal;    const run = () => { // 新增     newVal = effectFn()     fn(newVal, oldVal)     oldVal = newVal   }   const effectFn = effect(         ()=> getter(),     {       lazy: true,       // 使用run來執行依賴函數       scheduler: run  // 修改     }   )   // 當immediate為true時,立即執行一次依賴函數   if(options.immediate) { // 新增     run() // 新增   } else {     oldVal = effectFn()    } }

        watch函數還支持其他的執行調用時機,這里只實現了immediate。

        六. 淺響應與深響應

        深響應和淺響應的區別:

        const obj = reatcive({ foo: { bar: 1} })  effect(()=>{   console.log(obj.foo.bar) })  // 修改obj.foo.bar的值,并不能觸發響應 obj.foo.bar = 2

        因為之前實現的攔截,無論對于什么類型的數據都是直接進行返回的,如果實現深響應,那么首先應該判斷是否為對象類型的值,如果是對象類型的值,應當遞歸調用reactive方法進行轉換。

        // 接收第二個參數,標記為是否為淺響應 function createReactive(obj, isShallow = false) {   return new Proxy(obj, {     get(target, key, receiver) {       // 訪問raw時,返回原對象       if(key === 'raw') return target       track(target, key)        const res = Reflect.get(target, key, receiver)       // 如果是淺響應,直接返回值       if(isShallow) {         return res       }       // 判斷res是否為對象并且不為null,循環調用reatcive       if(typeof res === 'object' && res !== null) {         return reatcive(res)       }       return res     },     // ...省略其他   })

        將創建響應式對象的方法抽離出去,通過傳遞isShallow參數來決定是否創建深響應/淺響應對象。

        // 深響應 function reactive(obj) {   return createReactive(obj) }  // 淺響應 function shallowReactive(obj) {   return createReactive(obj, true) }

        七. 淺只讀與深只讀

        有時候我們并不需要對值進行修改,也就是需要值為只讀的,這個操作也分為深只讀和淺只讀,首先需要在createReactive函數中增加一個參數isReadOnly,代表是否為只讀屬性。

        // 淺只讀 function shallowReadOnly(obj) {   return createReactive(obj, true, true) }  // 深只讀 function readOnly(obj) {   return createReactive(obj, false, true) }
        set(target, key, newValue, receiver) {   // 是否為只讀屬性,如果是則打印警告信息并直接返回   if(isReadOnly) {     console.log(`屬性${key}是只讀的`)     return false   }    const oldVal = target[key]   const type = Object.prototype.hasOwnProperty.call(target, key) ? triggerType.SET : triggerType.ADD   const res = Reflect.set(target, key, newValue, receiver)   if (target === receiver.raw) {     if (oldVal !== newValue && (oldVal === oldVal || newValue === newValue)) {       trigger(target, key, type)     }   }    return res }

        如果為只讀屬性,那么也不需要為其建立響應聯系 如果為只讀屬性,那么在進行深層次遍歷的時候,需要調用readOnly函數對值進行包裝

        function createReactive(obj, isShallow = false, isReadOnly = false) {   return new Proxy(obj, {     get(target, key, receiver) {       // 訪問raw時,返回原對象       if (key === 'raw') return target        //只有在非只讀的時候才需要建立響應聯系       if(!isReadOnly) {         track(target, key)       }        const res = Reflect.get(target, key, receiver)       // 如果是淺響應,直接返回值       if (isShallow) {         return res       }       // 判斷res是否為對象并且不為null,循環調用creative       if (typeof res === 'object' && res !== null) {         // 如果數據為只讀,則調用readOnly對值進行包裝         return isReadOnly ? readOnly(res) : creative(res)       }       return res     },   }) }

        八. 處理數組

        數組的索引與length

        如果操作數組時,設置的索引值大于數組當前的長度,那么要更新數組的length屬性,所以當通過索引設置元素值時,可能會隱式的修改length的屬性值,因此再j進行觸發響應時,也應該觸發與length屬性相關聯的副作用函數重新執行。

        const arr = reactive(['foo']) // 數組原來的長度為1  effect(()=>{   console.log(arr.length) //1 })  // 設置索引為1的值,會導致數組長度變為2 arr[1] = 'bar'

        在判斷操作類型時,新增對數組類型的判斷,如果代理目標是數組,那么對于操作類型的判斷作出處理:

        如果設置的索引值小于數組的長度,就視為SET操作,因為他不會改變數組長度,如果設置的索引值大于當前數組的長度,那么應該被視為ADD操作。

        // 定義常量,便于修改 const triggerType = {   ADD: 'add',   SET: 'set' }  set(target, key, newValue, receiver) {   if(isReadOnly) {     console.log(`屬性${key}是只讀的`)     return false   }    const oldVal = target[key]    // 如果目標對象是數組,檢測被設置的索引值是否小于數組長度   const type = Array.isArray(target) && (Number(key) > target.length ? triggerType.ADD : triggerType.SET)   const res = Reflect.set(target, key, newValue, receiver)    trigger(target, key, type)    return res },
        function trigger(target, key, type) {   const depsMap = store.get(target)   if (!depsMap) return   const effects = depsMap.get(key)   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {     if (effectFn !== activeEffect) {       effectsToRun.add(effectFn)     }   })    // 當操作類型是ADD并且目標對象時數組時,應該取出執行那些與 length 屬性相關的副作用函數   if(Array.isArray(target) && type === triggerType.ADD) {     // 取出與length相關的副作用函數     const lengthEffects = deps.get('length')      lengthEffects && lengthEffects.forEach(effectFn => {       if (effectFn !== activeEffect) {         effectsToRun.add(effectFn)       }     })   }    effectsToRun.forEach(effect => {     if (effectFn.options.scheduler) {       effectFn.options.scheduler(effect)     } else {       effect()     }   })  }

        還有一點:其實修改數組的length屬性也會隱式的影響數組元素:

        const arr = reactive(['foo'])  effect(()=>{   // 訪問數組的第0個元素   console.log(arrr[0]) // foo })  // 將數組的長度修改為0,導致第0個元素被刪除,因此應該觸發響應 arr.length = 0

        如上所示,在副作用函數內部訪問了第0個元素,然后將數組的length屬性修改為0,這回隱式的影響數組元素,及所有的元素都會被刪除,所以應該觸發副作用函數重新執行。

        然而并非所有的對length屬性值的修改都會影響數組中的已有元素,如果設置的length屬性為100,這并不會影響第0個元素,當修改屬性值時,只有那些索引值大于等于新的length屬性值的元素才需要觸發響應。

        調用trigger函數時傳入新值:

        set(target, key, newValue, receiver) {   if(isReadOnly) {     console.log(`屬性${key}是只讀的`)     return false   }    const oldVal = target[key]    // 如果目標對象是數組,檢測被設置的索引值是否小于數組長度   const type = Array.isArray(target) && (Number(key) > target.length ? triggerType.ADD : triggerType.SET)   const res = Reflect.set(target, key, newValue, receiver)    // 將新的值進行傳遞,及觸發響應的新值   trigger(target, key, type, newValue) // 新增    return res }

        判斷新的下標值與需要操作的新的下標值進行判斷,因為數組的key為下標,所以副作用函數搜集器是以下標作為key值的,當length發生變動時,只需要將新值與每個下標的key判斷,大于等于新的length值的需要重新執行副作用函數。

        一文帶你深入剖析vue3的響應式

        如上圖所示,Map為根據數組的key,也就是id組成的Map結構,他們的每一個key都對應一個Set,用于保存這個key下面的所有的依賴函數。

        length屬性發生變動時,應當取出所有key值大于等于length值的所有依賴函數進行執行。

        function trigger(target, key, type, newValue) {   const depsMap = store.get(target)   if (!depsMap) return   const effects = depsMap.get(key)   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {     if (effectFn !== activeEffect) {       effectsToRun.add(effectFn)     }   })    // 如果操作目標是數組,并且修改了數組的length屬性   if(Array.isArray(target) && key === 'length') {     // 對于索引值大于或等于新的length元素     // 需要把所有相關聯的副作用函數取出并添加到effectToRun中待執行     depsMap.forEach((effects, key)=>{       // key 與 newValue均為數組下標,因為數組中key為index       if(key >= newValue) {         effects.forEach(effectFn=>{           if (effectFn !== activeEffect) {             effectsToRun.add(effectFn)           }         })       }     })   }    // ...省略 }

        本文的實現數組這種數據結構只考慮了針對長度發生變化的情況。

        九. ref

        由于Proxy的代理目標是非原始值,所以沒有任何手段去攔截對原始值的操作:

        let str = 'hi' // 無法攔截對值的修改 str = 'pino'

        解決方法是:使用一個非原始值去包裹原始值:

        function ref(val) {   // 創建一個對象對原始值進行包裹   const wrapper = {     value: val   }   // 使用reactive函數將包裹對象編程響應式數據并返回   return reactive(wrapper) }

        如何判斷是用戶傳入的對象還是包裹對象呢?

        const ref1 = ref(1) const ref2 = reactive({ value: 1 })

        只需要在包裹對象內部定義一個不可枚舉且不可寫的屬性:

        function ref(val) {   // 創建一個對象對原始值進行包裹   const wrapper = {     value: val   }   // 定義一個屬性值__v_isRef,值為true,代表是包裹對象   Object.defineProperty(wrapper, '_isRef', {     value: true   })   // 使用reactive函數將包裹對象編程響應式數據并返回   return reactive(wrapper) }

        十. 響應丟失問題與toRefs

        在使用…解構賦值時會導致響應式丟失:

        const obj = reactive({ foo: 1, bar: 2 })  // 將響應式數據展開到一個新的對象newObj const newObj = {   ...obj } // 此時相當于: const newObj = {   foo: 1,   bar: 2 }  effect(()=>{   //在副作用函數中通過新對象newObj讀取foo屬性值   console.log(newObj.foo) })  // obj,foo并不會觸發響應 obj.foo = 100

        首先創建一個響應式對象obj,然后使用展開運算符得到一個新對象newObj,他是一個普通對象,不具有響應式的能力,所以修改obj.foo的值不會觸發副作用函數重新更新。

        解決方法:

        const newObj = {   foo: {     // 用于返回其原始的響應式對象     get value() {       return obj.foo     }   },   bar: {     get value() {       return obj.bar     }   } }

        將單個值包裝為一個對象,相當于訪問該屬性的時候會得到該屬性的getter,在getter中返回原始的響應式對象。

        相當于解構訪問newObj.foo === obj.foo。

        {   get value() {     return obj.foo   } }

        toRefs

        function toRefs(obj) {   let res = {}   // 處理整個對象時,將屬性依次進行遍歷,調用toRef進行轉化   for(let key in obj) {     res[key] = toRef(obj, key)   }   return res }   function toRef(obj, key) {   const wrapper = {     // 允許讀取值     get value() {       return obj[key]     },     // 允許設置值     set value(val) {       obj[key] = val     }   }   // 標志為ref對象   Object.defineProperty(wrapper, '_isRef', {     value: true   })   return wrapper }

        使用toRefs處理整個對象,在toRefs這個函數中循環處理了對象所包含的所有屬性。

          const newObj = { ...toRefs(obj) }

        當設置value屬性值的時候,最終設置的是響應式數據的同名屬性值。

        一個基本的vue3響應式就完成了,但是本文所實現的依然是閹割版本,有很多情況都沒有進行考慮,還有好多功能沒有實現,比如:攔截 Map,Set,數組的其他問題,對象的其他問題,其他api的實現,但是上面的實現已經足夠讓你理解vue3響應式原理實現的核心了,這里還有很多其他的資料需要推薦,比如阮一峰老師的es6教程,對于vue3底層原理的實現,許多知識依然是需要回顧和復習,查看原始底層的實現,再比如霍春陽老師的《vue.js的設計與實現》這本書,這本書目前我也只看完了一半,但是截止到目前我認為這本書對于學習vue3的原理是非常深入淺出,鞭辟入里的,本文的許多例子也是借鑒了這本書。

        最后當然是需要取讀一讀源碼,不過在讀源碼之前能夠先了解一下實現的核心原理,再去看源碼是事半功倍的。希望大家都能早日學透源碼,面試的時候能夠對答如流,工作中遇到的問題也能從原理層面去理解和更好地解決!

        目前我也在實現一個mini-vue,截止到目前只實現了響應式部分,而且與本文的實現方式有所不同,后續還會繼續實現編譯和虛擬DOM部分,歡迎star!

        k-vue:

        https://github.com/konvyi/k-vue

        如果想學習《vue.js的設計與實現》這本書這本書,那么請關注下面這個鏈接作為參考,里面包含了根據具體的問題的功能進行拆分實現,同樣也只實現了響應式的部分!

        vue3-analysis:

        https://github.com/konvyi/vue3-analysis

        (學習視頻分享:web前端開發、編程基礎視頻)一文帶你深入剖析vue3的響應式

      1. 微信
      2. 分享
      3. 聲明:本文轉載于:掘金社區,如有侵犯,請聯系admin@php.cn刪除

      4. 相關標簽:響應式 vue3
      5. 推薦:PHP從基礎到實戰教程視頻

        • 上一篇:2022七夕情人節表白特效代碼+網站素材【免費下載】
        • 下一篇:沒有了

        贊(0)
        分享到: 更多 (0)
        網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
        主站蜘蛛池模板: 99精品免费视频| 精品国产污污免费网站| 久久久无码人妻精品无码| 亚洲国产精品久久久久久| 四虎成人精品无码| 久久久精品视频免费观看| 欧美精品人人做人人爱视频| 99精品国产高清一区二区麻豆| 日韩在线精品一二三区| 午夜在线视频91精品 | 99精品国产成人一区二区| 久久精品国产网红主播| 国产精品高清一区二区三区不卡| 少妇亚洲免费精品| 精品国产高清在线拍| 欧美精品在线一区| 国产精品久久久久久久久免费 | 国产精品天干天干综合网| 中国精品18videosex性中国| 欧美午夜精品久久久久久浪潮| 国产小呦泬泬99精品| 99热亚洲精品6码| 中文字幕精品一区二区日本| 国内精品免费视频精选在线观看| 国精品无码一区二区三区左线| 久久亚洲精品国产精品| 亚洲精品成人片在线观看精品字幕 | 色播精品免费小视频| 久久久国产精品福利免费| 国产成人精品日本亚洲11| 国产日韩欧美精品| 久久夜色精品国产亚洲| 四虎国产精品永久地址99| 亚洲日本精品一区二区| 国产成人久久久精品二区三区| mm1313亚洲国产精品无码试看| 亚洲国产精品久久| 国产韩国精品一区二区三区| 中文字幕久久精品无码| 亚洲午夜精品一级在线播放放 | 久久棈精品久久久久久噜噜|