国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

狂肝半個月!1.3 萬字深度剖析 Vue3 響應式(附腦圖)

共 35869字,需瀏覽 72分鐘

 ·

2022-09-19 21:10

寫在前面

本文的目標是實現(xiàn)一個基本的vue3的響應式,包含最基礎(chǔ)的情況的處理,本文是系列文章,如果你對vue3還不了解,那么請移步:

超詳細整理vue3基礎(chǔ)知識?? [1]

本文你將學到

  • 一個基礎(chǔ)的響應式實現(xiàn) ?
  • Proxy ?
  • Reflect ?
  • 嵌套effect的實現(xiàn) ?
  • computed ?
  • watch ?
  • 淺響應與深響應 ?
  • 淺只讀與深只讀 ?
  • 處理數(shù)組長度 ?
  • ref ?
  • toRefs ?
7f3c6867f479c887bc10081ad1f069f6.webp響應式.png

一. 實現(xiàn)一個完善的響應式

所謂的響應式數(shù)據(jù)的概念,其實最主要的目的就是為數(shù)據(jù)綁定執(zhí)行函數(shù),當數(shù)據(jù)發(fā)生變動的時候,再次觸發(fā)函數(shù)的執(zhí)行。

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

      //?定義一個對象
let?data?=?{
??name:?'pino',
??age:?18
}

let?nextVal
//?待綁定函數(shù)
function?effect()?{
??nextVal?=?data.age?+?1
}

data.age++
復制代碼

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

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

因為一個對象最好可以綁定多個函數(shù),所以有沒有可能我們?yōu)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">data這個對象定義一個空間,每當data的值進行變化的時候就會執(zhí)行這個空間里的函數(shù)?

答案是有的。

1. Object.defineProperty()

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

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

      ??Object.defineProperty(obj,?prop,?descriptor)
復制代碼

參數(shù)

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

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

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

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

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

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

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

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

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

      ?let?obj?=?{
???name:?"小花"
?}

?Object.defineProperty(obj,?'name',?{
???//?屬性讀取時進行攔截
???get()?{?return?'小明';?},
???//?屬性設(shè)置時攔截
???set(newValue)?{?obj.name?=?newValue;?},
???enumerable:?true,
???configurable:?true
?});
復制代碼

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

  1. 一次只能對一個屬性進行監(jiān)聽,需要遍歷來對所有屬性監(jiān)聽

  2. 對于對象的新增屬性,需要手動監(jiān)聽

  3. 對于數(shù)組通過pushunshift方法增加的元素,也無法監(jiān)聽

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

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

MDN Object.defineProperty [2]

2. Proxy

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

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

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

  • set(target, propKey, value, receiver) :攔截對象屬性的設(shè)置,比如 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 循環(huán),返回一個數(shù)組。該方法返回目標對象所有自身的屬性的屬性名,而 Object.keys() 的返回結(jié)果僅包括目標對象自身的可遍歷屬性。

  • 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) ,返回一個布爾值。如果目標對象是函數(shù),那么還有兩種額外操作可以攔截。

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

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

如果想要詳細了解proxy,請移步:

es6.ruanyifeng.com/#docs/proxy… [3]

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

3. 一個最簡單的響應式

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

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

      //?定義一個“倉庫”,用于存儲觸發(fā)函數(shù)
let?store?=?new?Set()
//?使用proxy進行代理
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)建了一個用于保存依賴函數(shù)的“倉庫”,它是Set類型,然后使用proxy對對象data進行代理,設(shè)置了setget攔截函數(shù),用于攔截讀取和設(shè)置操作,當讀取屬性時,將依賴函數(shù)effect存儲到“倉庫”中,當設(shè)置屬性值時,將依賴函數(shù)從“倉庫”中取出并重新執(zhí)行。

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

      // 定義一個對象
let data = {
name: 'pino',
age: 18
}

let nextVal
// 待綁定函數(shù)
function effect() {
// 依賴函數(shù)在這里被收集
// 當調(diào)用data.age時,effect函數(shù)被收集到“倉庫”中
nextVal = data.age + 1
console.log(nextVal)
}
// 執(zhí)行依賴函數(shù)
effect() // 19

setTimeout(()=>{
// 使用proxy進行代理后,使用代理后的對象名
// 觸發(fā)設(shè)置操作,此時會取出effect函數(shù)進行執(zhí)行
data_proxy.age++ // 2秒后輸出 20
}, 2000)
復制代碼

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

c4f3d6be5e86ef7db306b70f5393fbcb.webpJul-24-2022 17-31-39.gif

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

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

e0176684ebb851007fdae5841ce857e0.webpimage.png

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

b5ce386ca03c6b09e8f32039f169712a.webpimage.png

這樣就實現(xiàn)了一個最基本的響應式的功能。

4. 完善

問題一

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

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

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

      //?effect接受一個函數(shù),把這個匿名函數(shù)當作依賴函數(shù)
function?effect(fn)?{
??//?執(zhí)行依賴函數(shù)
??fn()
}

//?使用
effect(()=>{
??nextVal?=?data.age?+?1
??console.log(nextVal)
})
復制代碼

但是effect函數(shù)內(nèi)部只是執(zhí)行了,在get函數(shù)中怎么能知道用戶傳遞的依賴函數(shù)是什么呢,這兩個操作并不在一個函數(shù)內(nèi)???其實可以使用一個全局變量activeEffect來保存當前正在處理的依賴函數(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ù)傳遞一個匿名函數(shù)作為依賴函數(shù),當執(zhí)行時,首先會把匿名函數(shù)賦值給全局變量activeEffect,然后觸發(fā)屬性的讀取操作,進而觸發(fā)get攔截,將全局變量activeEffect進行收集。

問題二

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

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

      let?data?=?{
??name:?'pino',
??age:?18
}
復制代碼
aed5b56c85e9bb473c33f936d75b8381.webpimage.png

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

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

      let?data?=?{
?name:?'pino',
?age:?18
}
復制代碼

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

      data
???????->?name
???????????????->?effectFn

//?如果兩個屬性讀取了同一個依賴函數(shù)
data
???????->?name
???????????????->?effectFn
???????->?age
???????????????->?effectFn

//?如果兩個屬性讀取了不同的依賴函數(shù)
data
???????->?name
???????????????->?effectFn
???????->?age
???????????????->?effectFn1

//?如果是兩個不同的對象
data
???????->?name
???????????????->?effectFn
???????->?age
???????????????->?effectFn1
data2
???????->?addr
???????????????->?effectFn
復制代碼

接下來我們實現(xiàn)一下代碼,為了方便調(diào)用,將設(shè)置響應式數(shù)據(jù)的操作封裝為一個函數(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ù),則不需要進行收集。直接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()))
??}
??//?收集依賴函數(shù)
??deps.add(activeEffect)
}

function?trigger(target,?key)?{
??//?取出對象對應的Map
??let?depsMap?=?store.get(target)
??if(!depsMap)?return
??//?取出key所對應的Set
??let?deps?=?depsMap.get(key)
??//?執(zhí)行依賴函數(shù)
??deps?&&?deps.forEach(fn?=>?fn());
}
復制代碼

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

b9ec73484962ab7ab637fae92751db0d.webpimage.png

WeakMap

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

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

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

首先, 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 的鍵名,都會報錯。

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

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

      const?e1?=?document.getElementById('foo');
const?e2?=?document.getElementById('bar');
const?arr?=?[
????[e1,?'foo?元素'],
????[e2,?'bar?元素'],
];
復制代碼

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

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

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

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

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

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

Reflect

在vue3中的實現(xiàn)方式和我們的基本實現(xià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
})
復制代碼

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

      p.foo++
復制代碼

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

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

當通過代理對象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]就相當于obj.bar。因此當我們使用p.bar訪問bar屬性時,他的getter函數(shù)內(nèi)的this其實指向原始對象obj,這說明我們最終訪問的是obj.foo。所以在依賴函數(shù)內(nèi)部通過原始對象訪問他的某個屬性是不會建立響應聯(lián)系的:

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

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

先來看一下Reflect是啥:

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

      const?obj?=?{?foo:?1?}

//?直接讀取
console.log(obj.foo)?//1

//?使用Reflect.get讀取
console.log(Reflect.get(obj,?'foo'))?//?1
復制代碼

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

      const?obj?=?{?foo:?1?}

console.log(Reflect.get(obj,?'foo',?{?foo:?2?}))?//?輸出的是?2?而不是?1
復制代碼

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

而我們上文中的問題的解決方法就是在操作對象數(shù)據(jù)的時候通過Reflect的方法來傳遞第三個參數(shù)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,可以把它理解為函數(shù)調(diào)用中的this。

所以我們改造一下reactive函數(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)
???}
?})
}
復制代碼

擴展

Proxy -> get()

get 方法用于攔截某個屬性的讀取操作,可以接受三個參數(shù),依次為目標對象、屬性名和?proxy?(代理)?實例本身(嚴格地說,是操作行為所針對的對象),其中最后一個參數(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
復制代碼

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

      Reflect.get(1,?'foo')?//?報錯
Reflect.get(false,?'foo')?//?報錯
復制代碼

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)合使用,前者攔截賦值操作,后者完成賦值的默認行為,而且傳入了 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 ,導致觸發(fā) Proxy.defineProperty 攔截。這是因為 Proxy.setreceiver 參數(shù)總是指向當前的? Proxy 實例(即上例的 obj ),而 Reflect.set 一旦傳入 receiver ,就會將屬性賦值到 receiver 上面(即 obj ),導致觸發(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
復制代碼

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

      Reflect.set(1,?'foo',?{})?//?報錯
Reflect.set(false,?'foo',?{})?//?報錯
復制代碼

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

      //?定義倉庫
let?store?=?new?WeakMap()
//?定義當前處理的依賴函數(shù)
let?activeEffect

function?effect(fn)?{
??//?將操作包裝為一個函數(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ù),則不需要進行收集。直接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)?{
??//?取出對象對應的Map
??let?depsMap?=?store.get(target)
??if?(!depsMap)?return
??//?取出key所對應的Set
??const?effects?=?depsMap.get(key)
??//?執(zhí)行依賴函數(shù)
??//?為避免污染,創(chuàng)建一個新的Set來進行執(zhí)行依賴函數(shù)
??let?effectsToRun?=?new?Set()

??effects?&&?effects.forEach(effectFn?=>?{
??????effectsToRun.add(effectFn)
??})

??effectsToRun.forEach(effect?=>?effect())
}
復制代碼

二. 嵌套effect

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

      effect(function?effectFn1()?{
??effect(function?effectFn1()?{
????//?...
??})
})
復制代碼

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

      effect(()=>{
??Father.render()

??//嵌套子組件
??effect(()=>{
????Son.render()
??})
})
復制代碼

但是此時我們實現(xiàn)的effect并沒有這個能力,執(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,里面有兩個屬性foobar,然后定義了收集foo的依賴函數(shù),在依賴函數(shù)的內(nèi)部又定義了bar的依賴函數(shù)。在理想狀態(tài)下,我們希望依賴函數(shù)與屬性之間的關(guān)系如下:

      obj
????????->?foo
????????????????->?effect1
????????->?bar
????????????????->?effect2
復制代碼

當修改obj.foo的值的時候,會觸發(fā)effect1函數(shù)執(zhí)行,由于effect2函數(shù)在effect函數(shù)內(nèi)部,所以effect2函數(shù)也會執(zhí)行,而當修改obj.bar時,只會觸發(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é)果:

f6bb35989932a05fffbc72e98af483a8.webpimage_1659170045716_0.png

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

      function?effect(fn)?{
??//?將依賴函數(shù)進行包裝
??const?effectFn?=?()=>?{
????activeEffect?=?effectFn
????fn()
??}
??effectFn()
}
復制代碼

其實在這里就已經(jīng)很容易看出問題了,在接受用戶傳遞過來的值時,我們直接將activeEffect這個全局變量進行了覆蓋!所以在內(nèi)部執(zhí)行完后,activeEffect這個變量就已經(jīng)是effect2函數(shù)了,而且永遠不會再次變?yōu)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">effect1,此時再進行收集依賴函數(shù)時,永遠收集的都是effect2函數(shù)。

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

      //?增加effect調(diào)用棧
const?effectStack?=?[]?//?新增

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

??effectFn()
}
復制代碼
bfe832d94f4d2a4479824439a58225e7.webp未命名.drawio_1659171750374_0.png

此時兩個屬性所對應的依賴函數(shù)便不會發(fā)生錯亂了。

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

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

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

effect(()?=>?{
??obj.age++
})
復制代碼

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

e87e2e2f78fb0ff33d35a021b18e7b55.webpimage_1659163246902_0.png

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

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

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

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

      function?trigger(target,?key)?{
??//?取出對象對應的Map
??let?depsMap?=?store.get(target)
??if?(!depsMap)?return
??//?取出key所對應的Set
??const?effects?=?depsMap.get(key)
??//?//?執(zhí)行依賴函數(shù)
??//?因為刪除又添加都在同一個deps中,所以會產(chǎn)生無限執(zhí)行
??let?effectsToRun?=?new?Set()

??effects?&&?effects.forEach(effectFn?=>?{
????//?如果trigger出發(fā)執(zhí)行的副作用函數(shù)與當前正在執(zhí)行的副作用函數(shù)相同,則不觸發(fā)執(zhí)行
????if?(effectFn?!==?activeEffect)?{
????????????effectsToRun.add(effectFn)
????}
??})

??effectsToRun.forEach(effect?=>?effect())
}
復制代碼

四.computed

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

      const?plusOne?=?computed(()?=>?count.value?+?1)
復制代碼

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

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

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

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

      function?effect(fn,?options?=?{})?{?//?修改

??let?effectFn?=?function?()?{
????activeEffect?=?effectFn
????effectStack.push(effectFn)
????fn()
????effectStack.pop()
????activeEffect?=?effectStack[effectStack.length?-?1]
??}
??//?只有當非lazy的時候才直接執(zhí)行
??if(!options.lazy)?{
????effectFn()
??}
??//?將依賴函數(shù)組為返回值進行返回
??return?effectFn?//?新增
}
復制代碼

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

      const?effectFn?=?effect(()=>{
??console.log(obj.foo)
},?{?lazy:?true?})

//?手動執(zhí)行
effectFn()
復制代碼

但是如果我們想要獲取手動執(zhí)行后的值呢,這時只需要在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?//?新增
??}
??//?只有當非lazy的時候才直接執(zhí)行
??if(!options.lazy)?{
????effectFn()
??}
??//?將依賴函數(shù)組為返回值進行返回
??return?effectFn
}
復制代碼

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

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

??return?obj
}
復制代碼

但是此時還做不到對值進行緩存和對比,增加兩個變量,一個存儲執(zhí)行的值,另一個為一個開關(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時才會重新執(zhí)行
??????if(isRun)?{??//?新增
????????//?保存執(zhí)行結(jié)果
????????value?=?effectFn()?//?新增
????????//?執(zhí)行完畢后再次重置執(zhí)行開關(guān)
????????isRun?=?false?//?新增
??????}

??????return?value
????}
??}

??return?obj
}
復制代碼

但是上面的實現(xiàn)還有一個問題,就是好像isRun執(zhí)行一次后好像永遠都不會變成true了,我們的本意是在數(shù)據(jù)發(fā)生變動的時候需要再次觸發(fā)依賴函數(shù),也就是將isRun變?yōu)閠rue,實現(xiàn)這種效果,需要我們?yōu)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">options再傳遞一個函數(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這個函數(shù),那么只執(zhí)行scheduler這個函數(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ù)進行傳遞
????if(effectFn.options.scheduler)?{?//?新增
??????effectFn.options.scheduler(effect)?//?新增
????}?else?{
??????effect()
????}
??})
}
復制代碼

那么在computed中就可以實現(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時才會重新執(zhí)行
??????if(isRun)?{??//?新增
????????//?保存執(zhí)行結(jié)果
????????value?=?effectFn()?//?新增
????????//?執(zhí)行完畢后再次重置執(zhí)行開關(guān)
????????isRun?=?false?//?新增
??????}

??????return?value
????}
??}

??return?obj
}
復制代碼

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

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

      function?computed(getter)?{
??let?value,?isRun?=?true;?
??const?effectFn?=?effect(getter,?{
????lazy:?true,
????scheduler()?{
??????if(!isRun)?{
????????isRun?=?true
????????//?當計算屬性依賴的響應式數(shù)據(jù)發(fā)生變化時,手動調(diào)用trigger函數(shù)觸發(fā)響應
????????trigger(obj,?'value')?//?新增
??????}
????}
??})

??const?obj?=?{
????get?value()?{
??????if(isRun)?{?
????????value?=?effectFn()
????????isRun?=?false?
??????}
??????//?當讀取value時,手動調(diào)用track函數(shù)進行追蹤
??????????track(obj,?'value')
??????return?value
????}
??}

??return?obj
}
復制代碼

五. watch

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

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

//?修改數(shù)據(jù),觸發(fā)watch函數(shù)
obj.age++
復制代碼

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

      function?watch(source,?fn)?{
??effect(
????//?遞歸讀取對象中的每一項,變?yōu)轫憫綌?shù)據(jù),綁定依賴函數(shù)
????????()=>?bindData(source),
????{
??????scheduler()?{
????????//?當數(shù)據(jù)發(fā)生改變時,調(diào)用依賴函數(shù)
????????fn()
??????}
????}
??)
}
//?readData保存已讀取過的數(shù)據(jù),防止重復讀取
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函數(shù)還有另外一種用法,就是除了接收對象,還可以接受一個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()?{
????????//?當數(shù)據(jù)發(fā)生改變時,調(diào)用依賴函數(shù)
????????fn()
??????}
????}
??)
}
復制代碼

其實watch函數(shù)還有一個很重要的功能:就是在用戶傳遞的依賴函數(shù)中可以獲取新值和舊值,但是我們目前還做不到這一點。實現(xiàn)這個功能我們可以配置前文中的lazy屬性來實現(xiàn)。來回顧一下lazy屬性:設(shè)置了lazy之后一開始不會執(zhí)行依賴函數(shù),手動執(zhí)行時會返回執(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?//?新增
??????}
????}
??)
??//?手動調(diào)用依賴函數(shù),取得舊值
??oldVal?=?effectFn()?//?新增
}
復制代碼

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

      watch(obj,?()=>{
??console.log('改變了')
},?{
??immediate:?true
})
復制代碼

我們可以把scheduler封裝為一個函數(shù),以便在不同的時機去調(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??//?修改
????}
??)
??//?當immediate為true時,立即執(zhí)行一次依賴函數(shù)
??if(options.immediate)?{?//?新增
????run()?//?新增
??}?else?{
????oldVal?=?effectFn()?
??}
}
復制代碼

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

六. 淺響應與深響應

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

      const?obj?=?reatcive({?foo:?{?bar:?1}?})

effect(()=>{
??console.log(obj.foo.bar)
})

//?修改obj.foo.bar的值,并不能觸發(fā)響應
obj.foo.bar?=?2
復制代碼

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

      //?接收第二個參數(shù),標記為是否為淺響應
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,循環(huán)調(diào)用reatcive
??????if(typeof?res?===?'object'?&&?res?!==?null)?{
????????return?reatcive(res)
??????}
??????return?res
????},
????//?...省略其他
??})
復制代碼

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

      //?深響應
function?reactive(obj)?{
??return?createReactive(obj)
}

//?淺響應
function?shallowReactive(obj)?{
??return?createReactive(obj,?true)
}
復制代碼

七. 淺只讀與深只讀

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

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

      function?createReactive(obj,?isShallow?=?false,?isReadOnly?=?false)?{
??return?new?Proxy(obj,?{
????get(target,?key,?receiver)?{
??????//?訪問raw時,返回原對象
??????if?(key?===?'raw')?return?target

??????//只有在非只讀的時候才需要建立響應聯(lián)系
??????if(!isReadOnly)?{
????????track(target,?key)
??????}

??????const?res?=?Reflect.get(target,?key,?receiver)
??????//?如果是淺響應,直接返回值
??????if?(isShallow)?{
????????return?res
??????}
??????//?判斷res是否為對象并且不為null,循環(huán)調(diào)用creative
??????if?(typeof?res?===?'object'?&&?res?!==?null)?{
????????//?如果數(shù)據(jù)為只讀,則調(diào)用readOnly對值進行包裝
????????return?isReadOnly???readOnly(res)?:?creative(res)
??????}
??????return?res
????},
??})
}
復制代碼

八. 處理數(shù)組

數(shù)組的索引與length

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

      const?arr?=?reactive(['foo'])?//?數(shù)組原來的長度為1

effect(()=>{
??console.log(arr.length)?//1
})

//?設(shè)置索引為1的值,會導致數(shù)組長度變?yōu)?
arr[1]?=?'bar'
復制代碼

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

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

      //?定義常量,便于修改
const?triggerType?=?{
??ADD:?'add',
??SET:?'set'
}

set(target,?key,?newValue,?receiver)?{
??if(isReadOnly)?{
????console.log(`屬性${key}是只讀的`)
????return?false
??}

??const?oldVal?=?target[key]

??//?如果目標對象是數(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)
????}
??})

??//?當操作類型是ADD并且目標對象時數(shù)組時,應該取出執(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()
????}
??})

}
復制代碼

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

      const?arr?=?reactive(['foo'])

effect(()=>{
??//?訪問數(shù)組的第0個元素
??console.log(arrr[0])?//?foo
})

//?將數(shù)組的長度修改為0,導致第0個元素被刪除,因此應該觸發(fā)響應
arr.length?=?0
復制代碼

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

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

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

      set(target,?key,?newValue,?receiver)?{
??if(isReadOnly)?{
????console.log(`屬性${key}是只讀的`)
????return?false
??}

??const?oldVal?=?target[key]

??//?如果目標對象是數(shù)組,檢測被設(shè)置的索引值是否小于數(shù)組長度
??const?type?=?Array.isArray(target)?&&?(Number(key)?>?target.length???triggerType.ADD?:?triggerType.SET)
??const?res?=?Reflect.set(target,?key,?newValue,?receiver)

??//?將新的值進行傳遞,及觸發(fā)響應的新值
??trigger(target,?key,?type,?newValue)?//?新增

??return?res
}
復制代碼

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

dc35381819e5254342ec05b8d9f428b1.webp未命名繪圖.drawio_(2)_1659679803962_0.png

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

length屬性發(fā)生變動時,應當取出所有key值大于等于length值的所有依賴函數(shù)進行執(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)
????}
??})

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

??//?...省略
}
復制代碼

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

九. ref

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

      let?str?=?'hi'
//?無法攔截對值的修改
str?=?'pino'
復制代碼

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

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

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

      const?ref1?=?ref(1)
const?ref2?=?reactive({?value:?1?})
復制代碼

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

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

十. 響應丟失問題與toRefs

在使用...解構(gòu)賦值時會導致響應式丟失:

      const?obj?=?reactive({?foo:?1,?bar:?2?})

//?將響應式數(shù)據(jù)展開到一個新的對象newObj
const?newObj?=?{
??...obj
}
//?此時相當于:
const?newObj?=?{
??foo:?1,
??bar:?2
}

effect(()=>{
??//在副作用函數(shù)中通過新對象newObj讀取foo屬性值
??console.log(newObj.foo)
})

//?obj,foo并不會觸發(fā)響應
obj.foo?=?100
復制代碼

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

解決方法:

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

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

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

      {
??get?value()?{
????return?obj.foo
??}
}
復制代碼

toRefs

      function?toRefs(obj)?{
??let?res?=?{}
??//?處理整個對象時,將屬性依次進行遍歷,調(diào)用toRef進行轉(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
????}
??}
??//?標志為ref對象
??Object.defineProperty(wrapper,?'_isRef',?{
????value:?true
??})
??return?wrapper
}
復制代碼

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

      ??const?newObj?=?{?...toRefs(obj)?}
復制代碼

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

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

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

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

k-vue [4]

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

vue3-analysis [5]

實現(xiàn)一個mini-vue系列文章

超詳細整理vue3基礎(chǔ)知識?? [6]

寫在最后??

未來可能會更新實現(xiàn)mini-vue3javascript基礎(chǔ)知識系列,希望能一直堅持下去,期待多多點贊????,一起進步!????

作者:pino

https://juejin.cn/post/7129644396533776420

祝 您:2022 年暴富!萬事如意!

點贊和在看就是最大的支持,fc153087ca47818291ec62a3d27534e0.webp比心??fc153087ca47818291ec62a3d27534e0.webp

瀏覽 92
點贊
評論
收藏
分享

手機掃一掃分享

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

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 五月天av在线| 精品1区| 亚洲成人AV| 亚洲福利影院| 麻豆91在线| 亚洲无码人妻| 国产又粗又猛又爽又黄91精品| 成人资源站| 性色网站| 亚洲综合免费观看高清| 欧美三级理论片| 精品人人人| 午夜AV大片| 狠狠操综合网| 国产成人视频免费观看| 狠狠狠狠狠狠狠狠狠| 在线一区观看| A级黄色网| 黄色一级网站| 四川少扫搡BBw搡BBBB| 99精品热视频| 网站啪啪| 免费无码国产在线怀| 婷婷情色五月| 日本a片| 色色色五月| 人人做人人做人人做,人人做全句下一| 无码三级午夜久久人妻| 欧美黄片免费视频| 在线人妻| 欧美日本在线| 三级片无码视频| 日韩操逼电影| 亚洲午夜久久久久久久久| 成人才看的在线视频| 国产精品久久久一区二区三区| 一级成人片| 国产小骚逼| 国产精品s色| 亚洲青青| 老熟女搡BBBB搡BBBB视频| 青青草黄色片| 天天色天天日| 亚洲色图综合| 国产理伦| 日日夜夜拍| 各种BBwBBwBBwBBw| 午夜成人网站在线观看| 青青热久| 天天噜噜色| 五十路在线| 伊人大香蕉久久| 色欲无码| 日韩大香蕉视频| 一本加勒比HEZYO东京热无码| 亚洲福利网| 精品视频久久久久久| 69久久久久久久久久| 日本欧美国产| 国产成人精品一区二区三区在线| 免费黄片网站在线观看| 国产三级片自拍| 久草资源在线观看| 波多野59部无码喷潮| 操逼人妻| 蜜乳AV一区二区三区| 国产一级美女操逼视频免费播放| 在线小视频| 日本不卡中文字幕| 特级西西人体444.444人体聚色 | 亚洲天堂在线视频| 伊人在线| 久久xxx| 亚洲三级在线播放| 日韩精品成人无码免费| 一区二区三区精品视频| 欧美日韩一二| 911香蕉视频| 在线婷婷| 黄色动漫在线免费观看| 亚洲AV无码乱码国产| 久久久久久久久免费视频| 黄色爱爱| 日日免费视频| 最近中文字幕2022在线观看A| 91久久爽久久爽爽久久片| 欧美综合色| 成人三级AV| 七十路の高齢熟女千代子| 国产日韩欧美一区| 亚洲成人内射| 久久一区二区三区四区五区| 久久久免费观看视频| 18啪啪网站| 国产又爽又黄免费观看| 影音先锋三级资源| 99热官方网站| 五月天综合久久| 亚洲日韩AV电影| 女同一区二区三区| 欧美久久一区二区三区四区视频| 午夜亚洲AV永久无码精品蜜芽| www.国产豆花精品区| 亚洲欧美综合| 日韩人妻无码精品| 在线观看欧美日韩视频| 操骚逼视频| 91视频亚洲| 九九在线视频| 免费观看无码视频| 免费在线观看黄色视频网站| 51成人网站免费| 69视频网站| 婷婷成人电影| 麻豆av在线| 成人无码网站| 91久久综合亚洲鲁鲁五月天| 91大鸡巴| 西西888WWW大胆无码| 婷婷五月天激情小说| 欧美日在线| 中文天堂网| 欧美一级特黄A片免费看视频小说| 7799精品| 亚洲成人视屏| 一道本无吗一区| 国产亚洲欧美在线| 夜夜躁狠狠躁日日躁av| 内射毛片| 色噜噜狠狠一区二区三区Av蜜芽| 韩国高清无码60.70.80| 国产激情免费视频| 亚洲乱码一区| 在线观看中文字幕视频| 一本大道久久久久| 欧美三级视频在线| 翔田千里无码免费播放| 第一福利导航大全| 成人网站在线看。| 色天堂在线观看视频| 学生妹毛片视频| 人人妻人人插| 精品亚洲无码视频| 91视频专区| 黄色影院在线观看| 无码免费视频在线观看| 精品在线免费视频| 午夜无码福利| 欧美精产国品一二三区| 久久大香蕉视频| 操逼视频免费观看| 免费看AV大片| 蜜桃视频网站在线观看| 三级国产在线| 亚洲深夜福利| 无码视频久久| 在线国产中文字幕| 久久99久久99久久99| 天天摸夜夜操| 99精品人妻| 日韩色情视频| 免费在线成人网| 五月丁香欧美性爱| 国产精品系列视频| 性爱福利导航| 亚洲日韩精品无码| 午夜亚洲福利| 五月婷婷六月丁香综合| 超碰97资源| 九色影院| 久艹视频在线观看| 蜜挑视频一区二区三区| 日韩图片区小说视频区日| 激情中文网| 先锋资源国产| 三级毛片在线| 东京热91| 国产精品成人一区二区| 亚洲精品一级二级三级| 麻豆mdapp03.tⅴ| 国产精品一区二区视频| 伊人久久福利视频| 国产精品一级二级三级| 91无码人妻精品一区二区三区四| 免费看日逼视频| 久久久精品午夜人成欧洲亚洲韩国 | 97人妻一区| 黄色A级视频| 色五月激情网| 精品久久99| 操逼网站大全| 国产免费无码一区二区| 超碰老熟女| 人人人妻人人人操| 丁香五月天激情视频| 日本在线一区二区| 亚洲va中文字幕| 五月网站| 国产777| 亚洲啊v| 精品国产免费无码久久噜噜噜AV| 国产亲子乱婬一级A片借种| 丁香五月婷婷久久| 专业操老外| 中文字幕在线资源| 欧美日韩视频一区二区三区| 很很撸在线视频| 欧美午夜激情视频| 欧美日韩v| 加勒比无码高清| 日韩人妻一区二区| 欧美性BBwBBwBBwHD| 成人毛片在线大全免费| 一级AV在线| 亚洲午夜av| 亚洲无码成人在线观看| 国产无码激情视频| 伊人婷婷色香综合| 蜜芽成人在线视频| 超碰成人在线免费观看| 欧美黄片免费在线观看| 久久精品视频18| 色眯眯久久爱| 精品久草| 国产人妻精品一二三区| www.激情五月天| 足浴小少妇-88AX| 亚洲精品国产精品乱码视99| 欧美熟妇擦BBBB擦BBBB| 成人自拍偷拍视频| 迷奸91| 久久草视频在线播放| 日本黄色影院在线| 偷拍亚洲欧美| 黄片视频免费在线观看| 波多野结衣成人网站| 精品久久无码| av在线资源观看| 免费观看av| 三级片青青草| 青青青国产在线| 黄色成人视频免费看| www.777av| 91探花足浴店少妇在线| 日韩aaaa| 天天干天天操天天爽| 一区二区三区色| 五月天激情婷婷| 豆花网无码视频观看| 亚洲性爱小说网址| 国产亚洲欧美精品综合在线| 男人网站| 亚洲去干网| 日韩欧美亚洲一区二区三区 | 思思99热| 米奇7777狠狠狠狠| 亚洲精品成人网站| 欧美偷拍精品| 成人激情在线| 91麻豆一区二区| AV色图| 中文字幕-区二区三区四区视频中国| 久久久久久久人妻丝袜| 国产有码在线观看| 日本精品人妻| 黄片aaa| 青草成人在线视频| 蜜臀久久精品久久久久| 在线中文字幕AV| 第一页在线| 国产毛片18水真多18精品| 91看片看婬黄大片女跟女| 青青青草视频在线| 在线观看视频一区| 欧洲精品在线免费观看| 亚洲高清成人| 黄色视频网站免费在线观看| 亚洲性生活| 日韩三级在线播放| 亚洲精品一二三区| youjizzcom日本| 无码人妻AⅤ一区二区三区| 日韩爱爱爱| 免费三级网址| 亚洲a级毛片| 青青色在线视频| 3D动漫精品啪啪一区二区下载| 久久免费操| 九九色热| 91热在线| 狠狠肏| 三级成人AV| 亚洲人天堂| 日本黄色视频在线| 久久久精品999| 亚洲第一视频在线观看| 国产电影一区二区三区| 一级视频免费观看| 天天干天天日天天射| 黄片大全在线观看| 无码网站内射| 熟女天堂| 日韩人妻一区二区| 影音先锋av网| 簧片在线免费观看| 插进去综合网| 影音先锋91| 色综合天天综合网国产成人网| 一道本在线| 色婷久久| 操久久久| 国产粉嫩小泬白浆18p| 色天天干| 中文毛片| 中文字幕北条麻妃| 成人无码交配视频国产网站| 婷婷色777777| 狠狠干天天操| 中文字幕人妻无码| 午夜神马51| 操屄视频网站| 成人做爰黄A片免费看三区蜜臀 | 国产和日韩中文字幕| 精品视频日韩| 国产精品成人AV片| 最好看2019中文在线播放电影| 俺去也在线视频| 一区视频| 中文字幕免费av| AV网站在线播放| 可以免费观看的AV| 粉嫩av懂色av蜜臀av熟妇| 爱搞逼综合网| 人人摸天天| 亚洲小说区图片区| 精品九九九九九九| 欧美在线v| 肏逼网| 午夜人妻AV| 高清免费无码视频| 五月丁香成人电影| 日韩AV综合| 97男人的天堂| 99精品无码视频| 久久亚洲视频| 欧美日韩第一区| 成人黄色一级片| 二区三区在线观看| 人成视频在线免费观看| 一级片黄色电影| 国产香蕉av| 久久久大香蕉| 日韩一区二区三区精品| 免费观看无码| 日韩激情一区二区| 成人午夜无码视频| 日夜夜操| www.av91| 在线观看无码AV| 一级全黄120分钟免费| 日韩在线中文字幕亚洲| 久操婷婷| 天天操夜夜操| AAA日韩| 91站街农村熟女露脸| 视频一二三区| 五月天久久久| 国产精品自产拍| 一本色道久久综合熟妇| 东京热av一区二区| 亚洲色情在线| 无套内射在线播放| 九九九久久久| 五月天啪啪视频| chinese高潮老女人| 亚洲av大片| 黄色免费在线观看视频| 国产又爽又黄视频在线看| 久久韩国| 精品人妻中文字幕视频| 日韩人妻AV| 长腿女神打扫偷懒被主人猛操惩罚 | 91视频专区| 18+免费网站| 国产一级婬片A片免费无成人黑豆 国产真实露脸乱子伦对白高清视频 | 91探花秘在线播放| v天堂| 亚洲中文字幕在线播放| 日韩三级一区| 五月天激情午夜福利| 欧美色插| 日本黄A级A片国产免费| 在线黄色AV| 中文字幕无码毛片| 91在线无码精品秘国产| A视频免费在线观看| 成人做爰69片免费观看| 日本中文字幕乱伦| 久久综合中文| av一区在线| 欧美一级特黄A片免费看视频小说| 亚洲午夜激情电影| 日韩欧美色| 俺也来最新色视频| 青娱乐国产| 日韩不卡电影| 长泽梓黑人初解禁BDD07| 天天操天天操天天操天天操| 在线一级A片| 亚洲无码高清视频在线| 玖玖中文字幕| 一级av片| 51国产视频| 日本黄色视频在线| 亚洲ww国产a大作| 伊人网在线观看| 肏屄视频在线播放| 婷婷开心色四房播播免费| 成人亚洲A片V一区二区三区蜜月| 五月天婷婷在线播放视频免费观看| 日韩午夜成人电影| 婷婷五月伊人| 午夜天堂精品久久久久| 亚洲天堂无码AV| 日本大胆中出| 欧美男人的天堂| 伊人婷婷久久| 亚洲成人免费网站| 操逼激情视频| 久久毛片| 丁香五月婷婷啪啪| 国产三级AV在线观看| 人妻天天爽| 特黄色A级片视频| 亚洲高清无码视频在线观看| 欧美中文字幕| 亚洲丁香五月激情| 成人无码区免费| A级片免费看| 日逼一级片| 人人干天天操| av福利在线观看| 国产成人无码永久免费| 日韩一级在线免费观看| www.四虎成人网站| 国产在线免费视频| 噜噜噜网| 国产56页| 国产成人精品777777| 大奶一区二区| AV天堂无码| 日韩在线视频第一页| 午夜AV在线| 精品乱子伦一区二区三区在线播放| 麻豆三级片| 这里只有精品在线观看| 91久久精品一区二区三区| 三级在线视频| 日本韩国无码视频| 尤物A片| 蜜桃秘av一区二区三区安全| 国产在线看| 老司机永久免费91| 琪琪色在线视频| 日本黄色片在线播放| 久久久成人网| 欧美高清无码视频| 国产一级A片免费播放| 亚洲性爱一区| 粉嫩av懂色av蜜臀av熟妇| 亚洲无码一区二区三区蜜桃| 天天撸天天操| 亚州免费视频| 97人人操人人干| 爽好紧别夹喷水网站| 北条麻妃波多波多野结衣| 国产精品日韩欧美| 精品欧美| 大香蕉尹人在线观看| 欧美69| 91亚洲精品视频| 久久偷看各类wc女厕嘘嘘偷窃| 成人AV免费观看| 国产精品偷拍| 囯产精品一区二区三区AV做线| 亚洲女与黑人正在播放| 高清无码自拍| 成人在线H| 色婷婷一区二区三区四区五区精品视| 亚洲天堂2025| 久久黄色视屏| 黄色特级aaa片| 人妻无码电影推荐| 这里精品| 亚洲91黄色片| 91久久婷婷| 亚洲.无码.制服.日韩.中文字幕| 日韩小电影在线观看| 韩国成人啪啪无码高潮| 日韩一级成人片| 噜噜噜网| 天堂a√中文8| 综合中文字幕| 久久福利视频导航| 肥臀AV在线| 久久男人网| 欧美熟妇一区二区| 国产免费激情视频| 青娱乐亚洲领先| 国产无码中文| 超碰九九热| 波多野结衣无码流出| 欧美精品一二三区| 97精品欧美91久久久久久久| 综合视频一区| 毛片网页| 99久在线精品99re8热| 狠狠干2025| 青草青青视频| 男女AV| 波多野结衣91| 十八禁网站在线播放| 国产三级国产三级国产普通话| 青草精品视频| 国产熟女一区| 美女AV网站| 91在线看| 粉嫩av懂色av蜜臀av分享| 大香蕉亚洲在线| 免费看a的网站| 人人操人妻| 最新av| 久热免费视频在线观看| 丁香五月婷婷视频| 高清免费无码| 亚洲av高清无码| 免费三级毛片| 99久久人妻无码中文字幕系列| 无码三级| 色哟哟一区二区三区| 一级a免一级a做免费线看内裤的注意事项 | 五月天久久久| 蜜臀99久久精品久久久懂爱| 成人手机在线视频| 99草自拍| 久久人妻熟女中文字幕av蜜芽| 99成人免费视频| 人人干人人操人人| 香蕉av在线| 高清无码在线免费观看| 久久免费视频精品| 国产一级麻豆| 欧美老熟妇BBBBB搡BBB| 黄色视频日韩| 黄色电影免费在线观看| 亚洲精品成a人在线观看| 高清无码免费不卡| 免费福利视频网站| 日本无码区| 一夲道无码专区av无码A片| 黄片亚洲| 日韩啊v| 翔田千里一区二区三区精品播放| A级黄色网| 精品人妻无码| 欧洲一区二区三区| 欧美激情三区| 成人在线黄片| 人操人人人操| 中文字幕av在线| 午夜黄色福利| 亚洲欧洲成人| 艹逼在线观看| 91无码精品国产AⅤ| 亚洲一区免费| 久久91| 日本操逼网站| 亚洲欧美国产视频| 国产三级网址| 日韩毛片大全| 51精品国产午夜福利| 亚洲天媒在线播放| 成人免费视频18| 欧美成人午夜视频| 国产精品不卡一区二区三区| 伊人久久精品| 亚洲日韩色色| 亚洲色色视频| 毛片毛片毛片| 久久水蜜桃| 欧美超碰在线| 国产黄色视频网站在线观看| 噜噜色小说| 免费观看黄色一级片| 2026无码视频| 日韩中文字幕熟妇人妻| 91久久综合亚洲鲁鲁五月天| 羞羞视频com.入口| 国产精品porn| 亚洲视频www| 国产一级片免费视频| 无码精品人妻一区二区| 热久久亚洲中文字幕| 成人在线观看网站| 国产黄页| 日韩中文字码无砖| 337P人体美鮑高清| 蜜臀AV午夜精品| 欧美激情一区二区A片成人牛牛| 久久久久三级| 欧美国产在线观看综合| 黄色大片免费看| 第一福利视频| 免费精品视频| 无码一区二区在线观看| 日韩一级成人片| 99er在线观看| 日韩AⅤ视频| 中文字幕在线播放AV| 午夜福利啪啪啪| 69AV视频| 人妻少妇精品视频| 大香蕉三级片| 午夜男女福利| 香蕉视频亚洲| 蜜臀久久99精品久久久兰草影视| 免费操逼电影| 72成人网| 激情五月天综合网| 亚洲精品自拍视频| 97精品人妻麻豆一区二区| 91逼逼| 蜜桃91精品秘入口| 人人妻人人干| 欧美三级在线视频| 九色影院| 黑人无码AV黑人天堂无码AV| 日本一本不卡| 18AV在线观看| 国产操逼大全| 亚卅无码| 蜜桃亚洲AV无码一区二区三区 | 丁香五月AV| 亚洲无码自拍偷拍| 中文字幕一区二区三区四区五区六区| 婷婷性爱五月天| 国产操逼的视频| 日日艹夜夜艹| 丰满人妻一区二区三区四区53| 91社区成人影院| 亚洲五月激情| 狠狠操一区| 成人性爱福利视频| 婷婷亚洲精| 91欧美黑人| 高清无码一区| 51无码| 婷婷五月亚洲| 青青国产| 五月天婷婷色| 91麻豆成人| 中文字幕久久无码| 91久久国产| 一区在线播放| 国产免费小视频| 欧美人与禽乱婬A片| 亚洲艹逼| 亚洲无码在线免费观看视频| 成人第一页| 无码免费视频在线观看| 中文字幕人妻一区| 99爱在线观看| 色五婷婷| 国产精品女| 北条麻妃无码视频在线观看| aaa在线| 七十路の高齢熟妇无码| 中日韩在线视频| 日本韩国无码| 51毛片| 91精品国自产在线观看| 欧美成人大香蕉| 在线观看禁无码精品| 欧美一级婬片A片免费软件| a片网| 自拍偷拍无码| 一级电影网站| 天堂中文在线视频| 成人网址| 青青草国产| 99大香蕉| 91精品人妻| 中国熟女网站| 久久亚洲日韩天天做日日做综合亚洲 | 免费看成人片| h网站在线| 成人av免费在线观看| 天天爽天天爽| 性生活毛片| 第一色影院| 波多野结衣成人视频| 国产美女裸体网站| 欧美亚洲综合手机在线| 亚洲成人69| 超碰自拍99| 六月婷婷七月丁香| 免费观看黄片视频| 91久久久久久久久久久久18| 少妇无码在线| 一级爱爱爱| 天堂网中文| 亚洲综合影院| 北条麻妃无码播放| 精品一级| 国产久久这里只有精品视频| 77777色婷婷| 性欧美成人18| 成人国产欧美日韩在线视频| 伊人综合色| 苍井空一区二区三区四区| 中国AV网| 黄色A片免费视频| 无码欧美成人| 中文字幕你懂的| 亚洲色图一区二区三区| 操一操影院| 亚洲精品在线视频| 97在线观看视频| 黄色影视不卡| 91丨人妻丨国产| 国产一二| 大肉大捧视频免费观看| 三级片男人的天堂| 在线小黄片| 久久亚洲av| 中文字幕在线观看免费高清完整版在线观看| 伊人大香蕉在线观看| 无码高清在线| WWW.豆花视频精品| 亚洲网站在线免费观看| 六十路老熟女码视频| 精品成人AV| 天干夜天干天天天爽视频| 日本久久综合| 欧美污网站| 无码人妻一区二区三区| 韩国精品久久久| av在线资源网站| 三级片男人天堂| 天天草av| 亚洲免费观看高清完整版在va线观看 | 永久免费看片视频5355| 人人操人人爱人人摸| 屁屁影院CCYYCOM发布地| 无码高潮视频| 不卡日韩| 台湾成人综合网| 欧美黄色免费看| 中文字幕在线免费观看| 日韩高清一级免费| 亚洲一区二区视频在线观看| 国产五月天婷婷| 蜜臀久久99精品久久久兰草影视 | 野花Av| 久久91精品| 99久在线精品99re8| 国产伊人大香蕉| 狠狠色婷婷| 五月少妇| 国产有码在线观看| 五月激情六月婷婷| 大鸡巴免费视频| 五月天狠狠干| 亚洲欧美视频在线观看| 國產美女AV操逼網站| 欧美成人精品A片免费一区99| 99热99re6国产线播放| 97人妻人人澡人人爽人人| 日韩在线中文字幕视频| www.yw尤物| 搡BBBB搡BBBB搡BBB| 日韩精品一区二区三区四在线播放 | 日逼91| 国产精品99久久久久的广告情况| 91三级片| 中国黄色一级A片| 国产中文字字幕乱码无限| 亚洲欧美视频一区| 无码一道本一区二区无码| 久久免费观看视频| 3级片网站| 三级日韩视频| 中文无码观看| 亚洲日本黄色视频| 尤物视频网| 亚洲视频在线免费观看| 亚洲欧洲视频在线观看| 日韩一级性爱视频| 日本久久综合| 免费在线观看毛片| 丁月婷婷五香天日五月天| 亚洲经典免费视频| 日韩一级毛| 精品在线一区| 蜜桃视频一区| 人人干人人操人人爽| 日韩操逼AV| 日韩大香蕉网| 亚洲一区二区三| 加勒比综合网| 豆花视频成人| 免费无码又爽又黄又刺激网站| 美女视频一区二区三区| 在线观看黄网| 欧美第二页| 丁香婷婷社区| 西西www444无码免费视频| 一级无码毛片| 黄色视频网站在线免费观看| 亚洲性爱小说网址| 青草娱乐| 99re伊人| 成人午夜婬片A片| 亚洲精品无码永久| 欧美午夜三级| 爱逼综合| 精品尤物在线| 国色天香一区二区| 强伦轩农村人妻| 国产精品国产三级国产专业不 | 欧美成人视频18| 99自拍| 中国一级黄色毛片| 在线观看无码高清视频| 成人无码欧美大片免费看| 亚洲一区无码| 午夜综合| 人妻无码高清| 国产av资源网| 秋霞午夜久久| 日无码在线| 在线观看欧美日韩视频| 日日干日日| 国产日韩在线观看视频| 久久久免费观看视频| 欧美日韩色视频| 欧美作爱| 亚洲任你操超碰在线| 中文字幕第5页| HEZ-502搭讪绝品人妻系列| 亚洲精品中文字幕乱码三区91| 青青草网址| 蜜芽成人在线视频| 豆花精品视频| 阿宾MD0165麻豆沈娜娜| 影音av| 国产一区二区三区免费播放| 天天色AV| 国产操b视频| 囯产一级a一级a免费视频| 69久久成人精品| 成人毛片一区二区三区| 一级黄色蜜芽视频| 在线看A片| 日逼一级片| 国产性爱在线观看| 综合夜夜| 自拍超碰在线| 国产无码AV| 天天舔九色婷婷| 三级影片在线观看性| 伊人黄色网| 成人在线18禁| 国产Av婬乱麻豆| 色视频在线观看| 亚洲无码二区| 欧美一区二区三区四区视频| 18禁日韩| 国产精品一区二区在线| 蜜桃av色偷偷av老熟女| 久久久噜噜噜| 亚洲欧洲综合| 丁香五月天在线| 三个黑人猛躁我一晚上| 麻豆一二三区| 黄色在线观看国产| 亚洲二区视频| 黃色一級片黃色一級片尖叫声-百度-百| 精品无码一区二区三区在线| 亚洲国产成人精品激情在线| 高清色视频| 欧美性受XXXX黑人XYX性爽一 | 色五月婷婷视频| 99人人爽| 欧美日韩北条麻妃视频在线观看 | 江苏妇搡BBBB搡BBBB-百度| 成人黄色录像| 成年视频网站| 国产伊人影院| 亚洲国产成人电影|