久久久久久久视色,久久电影免费精品,中文亚洲欧美乱码在线观看,在线免费播放AV片

<center id="vfaef"><input id="vfaef"><table id="vfaef"></table></input></center>

    <p id="vfaef"><kbd id="vfaef"></kbd></p>

    
    
    <pre id="vfaef"><u id="vfaef"></u></pre>

      <thead id="vfaef"><input id="vfaef"></input></thead>

    1. 站長資訊網(wǎng)
      最全最豐富的資訊網(wǎng)站

      一文帶你深入剖析vue3的響應(yīng)式

      本篇文章帶你深度剖析vue3響應(yīng)式(附腦圖),本文的目標(biāo)是實(shí)現(xiàn)一個(gè)基本的vue3的響應(yīng)式,包含最基礎(chǔ)的情況的處理。

      一文帶你深入剖析vue3的響應(yīng)式

      本文你將學(xué)到

      • 一個(gè)基礎(chǔ)的響應(yīng)式實(shí)現(xiàn) ✅
      • Proxy ✅
      • Reflect ✅
      • 嵌套effect的實(shí)現(xiàn) ✅
      • computed ✅
      • watch ✅
      • 淺響應(yīng)與深響應(yīng) ✅
      • 淺只讀與深只讀 ✅
      • 處理數(shù)組長度 ✅
      • ref ✅
      • toRefs ✅

      一文帶你深入剖析vue3的響應(yīng)式

      一. 實(shí)現(xiàn)一個(gè)完善的響應(yīng)式

      所謂的響應(yīng)式數(shù)據(jù)的概念,其實(shí)最主要的目的就是為數(shù)據(jù)綁定執(zhí)行函數(shù),當(dāng)數(shù)據(jù)發(fā)生變動(dòng)的時(shí)候,再次觸發(fā)函數(shù)的執(zhí)行。(學(xué)習(xí)視頻分享:vue視頻教程)

      例如我們有一個(gè)對象data,我們想讓它變成一個(gè)響應(yīng)式數(shù)據(jù),當(dāng)data的數(shù)據(jù)發(fā)生變化時(shí),自動(dòng)執(zhí)行effect函數(shù),使nextVal變量的值也進(jìn)行變化:

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

      上面的例子中我們將data中的age的值進(jìn)行變化,但是effect函數(shù)并沒有執(zhí)行,因?yàn)楝F(xiàn)在effect函數(shù)與data這個(gè)對象不能說是沒啥聯(lián)系,簡直就是半毛錢的關(guān)系都沒有。

      那么怎么才能使這兩個(gè)毫不相關(guān)的函數(shù)與對象之間產(chǎn)生關(guān)聯(lián)呢?

      因?yàn)橐粋€(gè)對象最好可以綁定多個(gè)函數(shù),所以有沒有可能我們?yōu)?code>data這個(gè)對象定義一個(gè)空間,每當(dāng)data的值進(jìn)行變化的時(shí)候就會執(zhí)行這個(gè)空間里的函數(shù)?

      答案是有的。

      1. Object.defineProperty()

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

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

        Object.defineProperty(obj, prop, descriptor)

      參數(shù)

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

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

      • value——當(dāng)試圖獲取屬性時(shí)所返回的值。

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

      • enumerable——該屬性在for in循環(huán)中是否會被枚舉。

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

      • set()——該屬性的更新操作所調(diào)用的函數(shù)。

      • get()——獲取屬性值時(shí)所調(diào)用的函數(shù)。

      另外,數(shù)據(jù)描述符(其中屬性為: enumerable , configurablevalue , writable )與存取描述符(其中屬性為 enumerableconfigurable , set() , get() )之間是有互斥關(guān)系的。在定義了 set()get() 之后,描述符會認(rèn)為存取操作已被 定義了,其中再定義 valuewritable 會引起錯(cuò)誤。

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

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

      • 一次只能對一個(gè)屬性進(jìn)行監(jiān)聽,需要遍歷來對所有屬性監(jiān)聽

      • 對于對象的新增屬性,需要手動(dòng)監(jiān)聽

      • 對于數(shù)組通過push、unshift方法增加的元素,也無法監(jiān)聽

      那么vue3版本中是如何對一個(gè)對象進(jìn)行攔截的呢?答案是es6中的Proxy

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

      2. Proxy

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

      通過proxy我們可以實(shí)現(xiàn)對一個(gè)對象的讀取,設(shè)置等等操作進(jìn)行攔截,而且直接對對象進(jìn)行整體攔截,內(nèi)部提供了多達(dá)13種攔截方式。

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

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

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

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

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

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

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

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

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

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

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

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

      • construct(target, args) :攔截 Proxy (代理) 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如 new proxy(...args) 。

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

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

      3. 一個(gè)最簡單的響應(yīng)式

      有了proxy,我們就可以根據(jù)之前的思路實(shí)現(xiàn)一個(gè)基本的響應(yīng)式功能了,我們的思路是這樣的:在對象被讀取時(shí)把函數(shù)收集到一個(gè)“倉庫”,在對象的值被設(shè)置時(shí)觸發(fā)倉庫中的函數(shù)。

      由此我們可以寫出一個(gè)最基本的響應(yīng)式功能:

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

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

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

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

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

      一文帶你深入剖析vue3的響應(yīng)式

      此時(shí)整個(gè)響應(yīng)式流程的功能是這樣的:

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

      一文帶你深入剖析vue3的響應(yīng)式

      階段二,當(dāng)屬性發(fā)生改變時(shí),再次觸發(fā)依賴函數(shù)

      一文帶你深入剖析vue3的響應(yīng)式

      這樣就實(shí)現(xiàn)了一個(gè)最基本的響應(yīng)式的功能。

      4. 完善

      問題一

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

      所以先來進(jìn)行第一步的優(yōu)化:抽離出一個(gè)公共方法,依賴函數(shù)由用戶來傳遞參數(shù)。

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

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

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

      修改后的effect函數(shù)是這樣的:

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

      調(diào)用effect函數(shù)傳遞一個(gè)匿名函數(shù)作為依賴函數(shù),當(dāng)執(zhí)行時(shí),首先會把匿名函數(shù)賦值給全局變量activeEffect,然后觸發(fā)屬性的讀取操作,進(jìn)而觸發(fā)get攔截,將全局變量activeEffect進(jìn)行收集。

      問題二

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

      其實(shí)這會帶來很嚴(yán)重的問題,由于 “倉庫”并沒有與被操作的目標(biāo)屬性之間建立聯(lián)系,而上面我們的實(shí)現(xiàn)只是將整個(gè)“倉庫”遍歷了一遍,所以無論哪個(gè)屬性被觸發(fā),都會將“倉庫”中所有的依賴函數(shù)都取出來執(zhí)行一遍,因?yàn)檎麄€(gè)執(zhí)行程序中可能有很多對象及屬性都設(shè)置了響應(yīng)式聯(lián)系,這將會帶來很大的性能浪費(fèi)。所謂牽一發(fā)而動(dòng)全身,這種結(jié)果顯然不是我們想要的。

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

      一文帶你深入剖析vue3的響應(yīng)式

      所以我們要重新設(shè)計(jì)一下“倉庫”的數(shù)據(jù)結(jié)構(gòu),目的就是為了可以在屬性這個(gè)粒度下和“倉庫”建立明確的聯(lián)系。

      就拿我們上面進(jìn)行操作的對象來說,存在著兩層的結(jié)構(gòu),有兩個(gè)角色,對象data以及屬性name``age

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

      他們的關(guān)系是這樣的:

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

      接下來我們實(shí)現(xiàn)一下代碼,為了方便調(diào)用,將設(shè)置響應(yīng)式數(shù)據(jù)的操作封裝為一個(gè)函數(shù)reactive

      let newObj = new Proxy(obj, {   // 讀取攔截   get: function (target, propKey) {   },   // 設(shè)置攔截   set: function (target, propKey, value) {   } });  // 封裝為  function reactive(obj) {   return new Proxy(obj, {     // 讀取攔截     get: function (target, propKey) {     },     // 設(shè)置攔截     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       // 觸發(fā)依賴       trigger(target, key)     }   }) }  function track(target, key) {   // 如果沒有依賴函數(shù),則不需要進(jìn)行收集。直接return   if (!activeEffect) return    // 獲取target,也就是對象名,對應(yīng)上面例子中的data   let depsMap = store.get(target)   if (!depsMap) {     store.set(target, (depsMap = new Map()))   }   // 獲取對象中的key值,對應(yīng)上面例子中的name或age   let deps = depsMap.get(key)    if (!deps) {     depsMap.set(key, (deps = new Set()))   }   // 收集依賴函數(shù)   deps.add(activeEffect) }  function trigger(target, key) {   // 取出對象對應(yīng)的Map   let depsMap = store.get(target)   if(!depsMap) return   // 取出key所對應(yīng)的Set   let deps = depsMap.get(key)   // 執(zhí)行依賴函數(shù)   deps && deps.forEach(fn => fn()); }

      我們將讀取操作封裝為了函數(shù)track,觸發(fā)依賴函數(shù)的動(dòng)作封裝為了trigger方便調(diào)用,現(xiàn)在的整個(gè)“倉庫”結(jié)構(gòu)是這樣的:

      一文帶你深入剖析vue3的響應(yīng)式

      WeakMap

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

      WeakMap 結(jié)構(gòu)與 Map 結(jié)構(gòu)類似,也是用于生成鍵值對的集合。

      WeakMapMap 的區(qū)別有兩點(diǎn)。

      首先, 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

      上面代碼中,如果將數(shù)值 1Symbol 值作為 WeakMap 的鍵名,都會報(bào)錯(cuò)。

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

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

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

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

      一旦不再需要這兩個(gè)對象,我們就必須手動(dòng)刪除這個(gè)引用,否則垃圾回收機(jī)制就不會釋放 e1e2 占用的內(nèi)存。

      // 不需要 e1 和 e2 的時(shí)候 // 必須手動(dòng)刪除引用 arr [0] = null; arr [1] = null;

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

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

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

      Reflect

      在vue3中的實(shí)現(xiàn)方式和我們的基本實(shí)現(xiàn)還有一點(diǎn)不同就是在vue3中是使用Reflect來操作數(shù)據(jù)的,例如:

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

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

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

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

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

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

      p.foo++

      實(shí)際上問題就出在bar屬性中的訪問器函數(shù)getter上:

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

      當(dāng)通過代理對象p訪問p.bar,這回觸發(fā)代理對象的get攔截函數(shù)執(zhí)行:

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

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

      effect(()=>{   // obj是原始數(shù)據(jù),不是代理對象,不會建立響應(yīng)聯(lián)系   obj.foo })

      那么怎么解決這個(gè)問題呢,這時(shí)候就需要用到 Reflect出場了。

      先來看一下Reflect是啥:

      Reflect函數(shù)的功能就是提供了訪問一個(gè)對象屬性的默認(rèn)行為,例如下面兩個(gè)操作是等價(jià)的:

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

      實(shí)際上Reflect.get函數(shù)還能接受第三個(gè)函數(shù),即制定接受者receiver,可以把它理解為函數(shù)調(diào)用過程中的this

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

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

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

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

      當(dāng)使用代理對象p訪問bar屬性時(shí),那么receiver就是p,可以把它理解為函數(shù)調(diào)用中的this

      所以我們改造一下reactive函數(shù)的實(shí)現(xiàn):

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

      擴(kuò)展

      Proxy -> get()

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

      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 屬性部署了讀取函數(shù)( getter ),則讀取函數(shù)的 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

      如果第一個(gè)參數(shù)不是對象, Reflect.get 方法會報(bào)錯(cuò)。

      Reflect.get(1, 'foo') // 報(bào)錯(cuò) Reflect.get(false, 'foo') // 報(bào)錯(cuò)

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

      Reflect.set 方法設(shè)置 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 屬性設(shè)置了賦值函數(shù),則賦值函數(shù)的 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 對象聯(lián)合使用,前者攔截賦值操作,后者完成賦值的默認(rèn)行為,而且傳入了 receiver ,那么 Reflect.set 會觸發(fā) 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 ,導(dǎo)致觸發(fā) Proxy.defineProperty 攔截。這是因?yàn)?Proxy.setreceiver 參數(shù)總是指向當(dāng)前的 Proxy 實(shí)例(即上例的 obj ),而 Reflect.set 一旦傳入 receiver ,就會將屬性賦值到 receiver 上面(即 obj ),導(dǎo)致觸發(fā) defineProperty 攔截。如果 Reflect.set 沒有傳入 receiver ,那么就不會觸發(fā) 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

      如果第一個(gè)參數(shù)不是對象, Reflect.set 會報(bào)錯(cuò)。

      Reflect.set(1, 'foo', {}) // 報(bào)錯(cuò) Reflect.set(false, 'foo', {}) // 報(bào)錯(cuò)

      到這里,一個(gè)非?;镜捻憫?yīng)式的功能就完成了,整體代碼如下:

      // 定義倉庫 let store = new WeakMap() // 定義當(dāng)前處理的依賴函數(shù) let activeEffect  function effect(fn) {   // 將操作包裝為一個(gè)函數(shù)   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) {       // 觸發(fā)依賴       trigger(target, key)       Reflect.set(target, key, newVal, receiver)     }   }) }  function track(target, key) {   // 如果沒有依賴函數(shù),則不需要進(jìn)行收集。直接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()))   }   // 收集依賴函數(shù)   deps.add(activeEffect) }  function trigger(target, key) {   // 取出對象對應(yīng)的Map   let depsMap = store.get(target)   if (!depsMap) return   // 取出key所對應(yīng)的Set   const effects = depsMap.get(key)   // 執(zhí)行依賴函數(shù)   // 為避免污染,創(chuàng)建一個(gè)新的Set來進(jìn)行執(zhí)行依賴函數(shù)   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {       effectsToRun.add(effectFn)   })    effectsToRun.forEach(effect => effect()) }

      二. 嵌套effect

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

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

      當(dāng)組件中發(fā)生嵌套時(shí),此時(shí)的渲染函數(shù):

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

      但是此時(shí)我們實(shí)現(xiàn)的effect并沒有這個(gè)能力,執(zhí)行下面這段代碼,并不會出現(xiàn)意料之中的行為:

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

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

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

      當(dāng)修改obj.foo的值的時(shí)候,會觸發(fā)effect1函數(shù)執(zhí)行,由于effect2函數(shù)在effect函數(shù)內(nèi)部,所以effect2函數(shù)也會執(zhí)行,而當(dāng)修改obj.bar時(shí),只會觸發(fā)effect2函數(shù)。接下來修改一下obj.foo

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

      看一下執(zhí)行結(jié)果:

      一文帶你深入剖析vue3的響應(yīng)式

      可以看到effect2函數(shù)竟然執(zhí)行了兩次?按照之前的分析,當(dāng)obj.foo被修改后,應(yīng)當(dāng)觸發(fā)effect1這個(gè)依賴函數(shù),但是為什么會effect2會被再次執(zhí)行呢? 來看一下我們effect函數(shù)的實(shí)現(xiàn):

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

      其實(shí)在這里就已經(jīng)很容易看出問題了,在接受用戶傳遞過來的值時(shí),我們直接將activeEffect這個(gè)全局變量進(jìn)行了覆蓋!所以在內(nèi)部執(zhí)行完后,activeEffect這個(gè)變量就已經(jīng)是effect2函數(shù)了,而且永遠(yuǎn)不會再次變?yōu)?code>effect1,此時(shí)再進(jìn)行收集依賴函數(shù)時(shí),永遠(yuǎn)收集的都是effect2函數(shù)。

      那么如何解決這種問題呢,這種情況可以借鑒棧結(jié)構(gòu)來進(jìn)行處理,棧結(jié)構(gòu)是一種后進(jìn)先出的結(jié)構(gòu),在依賴函數(shù)執(zhí)行時(shí),將當(dāng)前的依賴函數(shù)壓入棧中,等待依賴函數(shù)執(zhí)行完畢后將其從棧中彈出,始終activeEffect指向棧頂?shù)囊蕾嚭瘮?shù)。

      // 增加effect調(diào)用棧 const effectStack = [] // 新增  function effect(fn) {   let effectFn = function () {     activeEffect = effectFn     // 入棧     effectStack.push(effectFn) // 新增     // 執(zhí)行函數(shù)的時(shí)候進(jìn)行g(shù)et收集     fn()     // 收集完畢后彈出     effectStack.pop() // 新增     // 始終指向棧頂     activeEffect = effectStack[effectStack.length - 1] // 新增   }    effectFn() }

      一文帶你深入剖析vue3的響應(yīng)式

      此時(shí)兩個(gè)屬性所對應(yīng)的依賴函數(shù)便不會發(fā)生錯(cuò)亂了。

      三. 避免無限循環(huán)

      如果現(xiàn)在將effect函數(shù)中傳遞的依賴函數(shù)改一下:

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

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

      一文帶你深入剖析vue3的響應(yīng)式

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

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

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

      解決這個(gè)問題,其實(shí)只需要在觸發(fā)函數(shù)執(zhí)行時(shí),判斷當(dāng)前取出的依賴函數(shù)是否等于activeEffect,就可以避免重復(fù)執(zhí)行同一個(gè)依賴函數(shù)。

      function trigger(target, key) {   // 取出對象對應(yīng)的Map   let depsMap = store.get(target)   if (!depsMap) return   // 取出key所對應(yīng)的Set   const effects = depsMap.get(key)   // // 執(zhí)行依賴函數(shù)   // 因?yàn)閯h除又添加都在同一個(gè)deps中,所以會產(chǎn)生無限執(zhí)行   let effectsToRun = new Set()    effects && effects.forEach(effectFn => {     // 如果trigger出發(fā)執(zhí)行的副作用函數(shù)與當(dāng)前正在執(zhí)行的副作用函數(shù)相同,則不觸發(fā)執(zhí)行     if (effectFn !== activeEffect) {             effectsToRun.add(effectFn)     }   })    effectsToRun.forEach(effect => effect()) }

      四.computed

      computed是vue3中的計(jì)算屬性,它可以根據(jù)傳入的參數(shù)進(jìn)行響應(yīng)式的處理:

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

      根據(jù)computed的用法,我們可以知道它的幾個(gè)特點(diǎn):

      • 懶執(zhí)行,值變化時(shí)才會觸發(fā)

      • 緩存功能,如果值沒有變化,就會返回上一次的執(zhí)行結(jié)果 在實(shí)現(xiàn)這兩個(gè)核心功能之前,我們先來改造一下之前實(shí)現(xiàn)的effect函數(shù)。

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

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

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

      這時(shí),如果傳遞了lazy屬性,那么該effect將不會立即執(zhí)行,需要手動(dòng)進(jìn)行執(zhí)行:

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

      但是如果我們想要獲取手動(dòng)執(zhí)行后的值呢,這時(shí)只需要在effect函數(shù)中將其返回即可。

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

      接下來開始實(shí)現(xiàn)computed函數(shù):

      function computed(getter) {   // 創(chuàng)建一個(gè)可手動(dòng)調(diào)用的依賴函數(shù)   const effectFn = effect(getter, {     lazy: true   })   // 當(dāng)對象被訪問的時(shí)候才調(diào)用依賴函數(shù)   const obj = {     get value() {       return effectFn()     }   }    return obj }

      但是此時(shí)還做不到對值進(jìn)行緩存和對比,增加兩個(gè)變量,一個(gè)存儲執(zhí)行的值,另一個(gè)為一個(gè)開關(guān),表示“是否可以重新執(zhí)行依賴函數(shù)”:

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

      但是上面的實(shí)現(xiàn)還有一個(gè)問題,就是好像isRun執(zhí)行一次后好像永遠(yuǎn)都不會變成true了,我們的本意是在數(shù)據(jù)發(fā)生變動(dòng)的時(shí)候需要再次觸發(fā)依賴函數(shù),也就是將isRun變?yōu)閠rue,實(shí)現(xiàn)這種效果,需要我們?yōu)?code>options再傳遞一個(gè)函數(shù),用于用戶自定義的調(diào)度執(zhí)行。

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

      接下來需要修改一下trigger如果傳遞了scheduler這個(gè)函數(shù),那么只執(zhí)行scheduler這個(gè)函數(shù)而不執(zhí)行依賴函數(shù):

      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 => {     // 如果存在調(diào)度器scheduler,那么直接調(diào)用該調(diào)度器,并將依賴函數(shù)進(jìn)行傳遞     if(effectFn.options.scheduler) { // 新增       effectFn.options.scheduler(effect) // 新增     } else {       effect()     }   }) }

      那么在computed中就可以實(shí)現(xiàn)重置執(zhí)行開關(guān)isRun的操作了:

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

      當(dāng)computed傳入的依賴函數(shù)中的值發(fā)生改變時(shí),會觸發(fā)響應(yīng)式對象的trigger函數(shù),而計(jì)算屬性創(chuàng)建響應(yīng)式對象時(shí)傳入了scheduler,所以當(dāng)數(shù)據(jù)改變時(shí),只會執(zhí)行scheduler函數(shù),在scheduler函數(shù)內(nèi)我們將執(zhí)行開關(guān)重置為true,再下次訪問數(shù)據(jù)觸發(fā)get函數(shù)時(shí),就會重新執(zhí)行依賴函數(shù)。這也就實(shí)現(xiàn)了當(dāng)數(shù)據(jù)發(fā)生改變時(shí),會再次觸發(fā)依賴函數(shù)的功能了。

      為了避免計(jì)算屬性被另外一個(gè)依賴函數(shù)調(diào)用而失去響應(yīng),我們還需要為計(jì)算屬性單獨(dú)進(jìn)行綁定響應(yīng)式的功能,形成一個(gè)effect嵌套。

      function computed(getter) {   let value, isRun = true;    const effectFn = effect(getter, {     lazy: true,     scheduler() {       if(!isRun) {         isRun = true         // 當(dāng)計(jì)算屬性依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時(shí),手動(dòng)調(diào)用trigger函數(shù)觸發(fā)響應(yīng)         trigger(obj, 'value') // 新增       }     }   })    const obj = {     get value() {       if(isRun) {          value = effectFn()         isRun = false        }       // 當(dāng)讀取value時(shí),手動(dòng)調(diào)用track函數(shù)進(jìn)行追蹤           track(obj, 'value')       return value     }   }    return obj }

      五. watch

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

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

      watch接受兩個(gè)參數(shù),第一個(gè)參數(shù)為綁定的響應(yīng)式數(shù)據(jù),第二個(gè)參數(shù)為依賴函數(shù),我們依然可以沿用之前的思路來進(jìn)行處理,利用effect以及scheduler來改變觸發(fā)執(zhí)行時(shí)機(jī)。

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

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

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

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

      function watch(source, fn) {   let getter = typeof source === 'function' ? source : (()=> bindData(source))    effect(     // 執(zhí)行g(shù)etter         ()=> getter(),     {       scheduler() {         // 當(dāng)數(shù)據(jù)發(fā)生改變時(shí),調(diào)用依賴函數(shù)         fn()       }     }   ) }

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

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

      此外,watch函數(shù)還有一個(gè)功能,就是可以自定義執(zhí)行時(shí)機(jī),比如immediate屬性,他會在創(chuàng)建時(shí)立即執(zhí)行一次:

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

      我們可以把scheduler封裝為一個(gè)函數(shù),以便在不同的時(shí)機(jī)去調(diào)用他:

      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來執(zhí)行依賴函數(shù)       scheduler: run  // 修改     }   )   // 當(dāng)immediate為true時(shí),立即執(zhí)行一次依賴函數(shù)   if(options.immediate) { // 新增     run() // 新增   } else {     oldVal = effectFn()    } }

      watch函數(shù)還支持其他的執(zhí)行調(diào)用時(shí)機(jī),這里只實(shí)現(xiàn)了immediate。

      六. 淺響應(yīng)與深響應(yīng)

      深響應(yīng)和淺響應(yīng)的區(qū)別:

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

      因?yàn)橹皩?shí)現(xiàn)的攔截,無論對于什么類型的數(shù)據(jù)都是直接進(jìn)行返回的,如果實(shí)現(xiàn)深響應(yīng),那么首先應(yīng)該判斷是否為對象類型的值,如果是對象類型的值,應(yīng)當(dāng)遞歸調(diào)用reactive方法進(jìn)行轉(zhuǎn)換。

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

      將創(chuàng)建響應(yīng)式對象的方法抽離出去,通過傳遞isShallow參數(shù)來決定是否創(chuàng)建深響應(yīng)/淺響應(yīng)對象。

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

      七. 淺只讀與深只讀

      有時(shí)候我們并不需要對值進(jìn)行修改,也就是需要值為只讀的,這個(gè)操作也分為深只讀和淺只讀,首先需要在createReactive函數(shù)中增加一個(gè)參數(shù)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 }

      如果為只讀屬性,那么也不需要為其建立響應(yīng)聯(lián)系 如果為只讀屬性,那么在進(jìn)行深層次遍歷的時(shí)候,需要調(diào)用readOnly函數(shù)對值進(jìn)行包裝

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

      八. 處理數(shù)組

      數(shù)組的索引與length

      如果操作數(shù)組時(shí),設(shè)置的索引值大于數(shù)組當(dāng)前的長度,那么要更新數(shù)組的length屬性,所以當(dāng)通過索引設(shè)置元素值時(shí),可能會隱式的修改length的屬性值,因此再j進(jìn)行觸發(fā)響應(yīng)時(shí),也應(yīng)該觸發(fā)與length屬性相關(guān)聯(lián)的副作用函數(shù)重新執(zhí)行。

      const arr = reactive(['foo']) // 數(shù)組原來的長度為1  effect(()=>{   console.log(arr.length) //1 })  // 設(shè)置索引為1的值,會導(dǎo)致數(shù)組長度變?yōu)? arr[1] = 'bar'

      在判斷操作類型時(shí),新增對數(shù)組類型的判斷,如果代理目標(biāo)是數(shù)組,那么對于操作類型的判斷作出處理:

      如果設(shè)置的索引值小于數(shù)組的長度,就視為SET操作,因?yàn)樗粫淖償?shù)組長度,如果設(shè)置的索引值大于當(dāng)前數(shù)組的長度,那么應(yīng)該被視為ADD操作。

      // 定義常量,便于修改 const triggerType = {   ADD: 'add',   SET: 'set' }  set(target, key, newValue, receiver) {   if(isReadOnly) {     console.log(`屬性${key}是只讀的`)     return false   }    const oldVal = target[key]    // 如果目標(biāo)對象是數(shù)組,檢測被設(shè)置的索引值是否小于數(shù)組長度   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)     }   })    // 當(dāng)操作類型是ADD并且目標(biāo)對象時(shí)數(shù)組時(shí),應(yīng)該取出執(zhí)行那些與 length 屬性相關(guān)的副作用函數(shù)   if(Array.isArray(target) && type === triggerType.ADD) {     // 取出與length相關(guān)的副作用函數(shù)     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()     }   })  }

      還有一點(diǎn):其實(shí)修改數(shù)組的length屬性也會隱式的影響數(shù)組元素:

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

      如上所示,在副作用函數(shù)內(nèi)部訪問了第0個(gè)元素,然后將數(shù)組的length屬性修改為0,這回隱式的影響數(shù)組元素,及所有的元素都會被刪除,所以應(yīng)該觸發(fā)副作用函數(shù)重新執(zhí)行。

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

      調(diào)用trigger函數(shù)時(shí)傳入新值:

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

      判斷新的下標(biāo)值與需要操作的新的下標(biāo)值進(jìn)行判斷,因?yàn)閿?shù)組的key為下標(biāo),所以副作用函數(shù)搜集器是以下標(biāo)作為key值的,當(dāng)length發(fā)生變動(dòng)時(shí),只需要將新值與每個(gè)下標(biāo)的key判斷,大于等于新的length值的需要重新執(zhí)行副作用函數(shù)。

      一文帶你深入剖析vue3的響應(yīng)式

      如上圖所示,Map為根據(jù)數(shù)組的key,也就是id組成的Map結(jié)構(gòu),他們的每一個(gè)key都對應(yīng)一個(gè)Set,用于保存這個(gè)key下面的所有的依賴函數(shù)。

      當(dāng)length屬性發(fā)生變動(dòng)時(shí),應(yīng)當(dāng)取出所有key值大于等于length值的所有依賴函數(shù)進(jìn)行執(zhí)行。

      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)     }   })    // 如果操作目標(biāo)是數(shù)組,并且修改了數(shù)組的length屬性   if(Array.isArray(target) && key === 'length') {     // 對于索引值大于或等于新的length元素     // 需要把所有相關(guān)聯(lián)的副作用函數(shù)取出并添加到effectToRun中待執(zhí)行     depsMap.forEach((effects, key)=>{       // key 與 newValue均為數(shù)組下標(biāo),因?yàn)閿?shù)組中key為index       if(key >= newValue) {         effects.forEach(effectFn=>{           if (effectFn !== activeEffect) {             effectsToRun.add(effectFn)           }         })       }     })   }    // ...省略 }

      本文的實(shí)現(xiàn)數(shù)組這種數(shù)據(jù)結(jié)構(gòu)只考慮了針對長度發(fā)生變化的情況。

      九. ref

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

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

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

      function ref(val) {   // 創(chuàng)建一個(gè)對象對原始值進(jìn)行包裹   const wrapper = {     value: val   }   // 使用reactive函數(shù)將包裹對象編程響應(yīng)式數(shù)據(jù)并返回   return reactive(wrapper) }

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

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

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

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

      十. 響應(yīng)丟失問題與toRefs

      在使用…解構(gòu)賦值時(shí)會導(dǎo)致響應(yīng)式丟失:

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

      首先創(chuàng)建一個(gè)響應(yīng)式對象obj,然后使用展開運(yùn)算符得到一個(gè)新對象newObj,他是一個(gè)普通對象,不具有響應(yīng)式的能力,所以修改obj.foo的值不會觸發(fā)副作用函數(shù)重新更新。

      解決方法:

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

      將單個(gè)值包裝為一個(gè)對象,相當(dāng)于訪問該屬性的時(shí)候會得到該屬性的getter,在getter中返回原始的響應(yīng)式對象。

      相當(dāng)于解構(gòu)訪問newObj.foo === obj.foo

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

      toRefs

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

      使用toRefs處理整個(gè)對象,在toRefs這個(gè)函數(shù)中循環(huán)處理了對象所包含的所有屬性。

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

      當(dāng)設(shè)置value屬性值的時(shí)候,最終設(shè)置的是響應(yīng)式數(shù)據(jù)的同名屬性值。

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

      最后當(dāng)然是需要取讀一讀源碼,不過在讀源碼之前能夠先了解一下實(shí)現(xiàn)的核心原理,再去看源碼是事半功倍的。希望大家都能早日學(xué)透源碼,面試的時(shí)候能夠?qū)Υ鹑缌?,工作中遇到的問題也能從原理層面去理解和更好地解決!

      目前我也在實(shí)現(xiàn)一個(gè)mini-vue,截止到目前只實(shí)現(xiàn)了響應(yīng)式部分,而且與本文的實(shí)現(xiàn)方式有所不同,后續(xù)還會繼續(xù)實(shí)現(xiàn)編譯和虛擬DOM部分,歡迎star!

      k-vue:

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

      如果想學(xué)習(xí)《vue.js的設(shè)計(jì)與實(shí)現(xiàn)》這本書這本書,那么請關(guān)注下面這個(gè)鏈接作為參考,里面包含了根據(jù)具體的問題的功能進(jìn)行拆分實(shí)現(xiàn),同樣也只實(shí)現(xiàn)了響應(yīng)式的部分!

      vue3-analysis:

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

      (學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)一文帶你深入剖析vue3的響應(yīng)式

    2. 微信
    3. 分享
    4. 聲明:本文轉(zhuǎn)載于:掘金社區(qū),如有侵犯,請聯(lián)系admin@php.cn刪除

    5. 相關(guān)標(biāo)簽:響應(yīng)式 vue3
    6. 推薦:PHP從基礎(chǔ)到實(shí)戰(zhàn)教程視頻

      • 上一篇:2022七夕情人節(jié)表白特效代碼+網(wǎng)站素材【免費(fèi)下載】
      • 下一篇:沒有了

      贊(0)
      分享到: 更多 (0)
      網(wǎng)站地圖   滬ICP備18035694號-2    滬公網(wǎng)安備31011702889846號