Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。javascript
proxy是es6新特性,爲了對目標的做用主要是經過handler對象中的攔截方法攔截目標對象target的某些行爲(如屬性查找、賦值、枚舉、函數調用等)。html
/* target: 目標對象,待要使用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。 */
/* handler: 一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 proxy 的行爲。 */
const proxy = new Proxy(target, handler);
複製代碼
** 3.0 將帶來一個基於 Proxy 的 observer 實現,它能夠提供覆蓋語言 (JavaScript——譯註) 全範圍的響應式能力,消除了當前 Vue 2 系列中基於 Object.defineProperty 所存在的一些侷限,這些侷限包括:1 對屬性的添加、刪除動做的監測; 2 對數組基於下標的修改、對於 .length 修改的監測; 3 對 Map、Set、WeakMap 和 WeakSet 的支持;;vue
vue2.0 用 Object.defineProperty 做爲響應式原理的實現,可是會有它的侷限性,好比 沒法監聽數組基於下標的修改,不支持 Map、Set、WeakMap 和 WeakSet等缺陷 ,因此改用了proxy解決了這些問題,這也意味着vue3.0將放棄對低版本瀏覽器的兼容(兼容版本ie11以上)。java
vue3.0 響應式用到的捕獲器(接下來會重點介紹)node
handler.has() -> in 操做符 的捕捉器。 (vue3.0 用到) handler.get() -> 屬性讀取 操做的捕捉器。 (vue3.0 用到) handler.set() -> 屬性設置* 操做的捕捉器。 (vue3.0 用到) handler.deleteProperty() -> delete 操做符 的捕捉器。(vue3.0 用到) handler.ownKeys() -> Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。(vue3.0 用到)react
vue3.0 響應式沒用到的捕獲器(有興趣的同窗能夠研究一下)es6
handler.getPrototypeOf() -> Object.getPrototypeOf 方法的捕捉器。 handler.setPrototypeOf() -> Object.setPrototypeOf 方法的捕捉器。 handler.isExtensible() -> Object.isExtensible 方法的捕捉器。 handler.preventExtensions() -> Object.preventExtensions 方法的捕捉器。 handler.getOwnPropertyDescriptor() -> Object.getOwnPropertyDescriptor 方法的捕捉器。 handler.defineProperty() -> Object.defineProperty 方法的捕捉器。 handler.apply() -> 函數調用操做 的捕捉器。 handler.construct() -> new 操做符 的捕捉器。api
has(target, propKey)數組
target:目標對象瀏覽器
propKey:待攔截屬性名
做用: 攔截判斷target對象是否含有屬性propKey的操做
攔截操做: propKey in proxy; 不包含for...in循環
對應Reflect: Reflect.has(target, propKey)
🌰例子:
const handler = {
has(target, propKey){
/* * 作你的操做 */
return propKey in target
}
}
const proxy = new Proxy(target, handler)
複製代碼
get(target, propKey, receiver)
target:目標對象
propKey:待攔截屬性名
receiver: proxy實例
返回: 返回讀取的屬性
做用:攔截對象屬性的讀取
攔截操做:proxy[propKey]或者點運算符
對應Reflect: Reflect.get(target, propertyKey[, receiver])
🌰例子:
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : '沒有此水果';
}
}
const foot = new Proxy({}, handler)
foot.apple = '蘋果'
foot.banana = '香蕉';
console.log(foot.apple, foot.banana); /* 蘋果 香蕉 */
console.log('pig' in foot, foot.pig); /* false 沒有此水果 */
複製代碼
特殊狀況
const person = {};
Object.defineProperty(person, 'age', {
value: 18,
writable: false,
configurable: false
})
const proxPerson = new Proxy(person, {
get(target,propKey) {
return 20
//應該return 18;不能返回其餘值,不然報錯
}
})
console.log( proxPerson.age ) /* 會報錯 */
複製代碼
set(target,propKey, value,receiver)
target:目標對象
propKey:待攔截屬性名
value:新設置的屬性值
receiver: proxy實例
返回:嚴格模式下返回true操做成功;不然失敗,報錯
做用: 攔截對象的屬性賦值操做
攔截操做: proxy[propkey] = value
對應Reflect: Reflect.set(obj, prop, value, receiver)
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) { /* 若是年齡不是整數 */
throw new TypeError('The age is not an integer')
}
if (value > 200) { /* 超出正常的年齡範圍 */
throw new RangeError('The age seems invalid')
}
}
obj[prop] = value
// 表示成功
return true
}
}
let person = new Proxy({}, validator)
person.age = 100
console.log(person.age) // 100
person.age = 'young' // 拋出異常: Uncaught TypeError: The age is not an integer
person.age = 300 // 拋出異常: Uncaught RangeError: The age seems invalid
複製代碼
當對象的屬性writable爲false時,該屬性不能在攔截器中被修改
const person = {};
Object.defineProperty(person, 'age', {
value: 18,
writable: false,
configurable: true,
});
const handler = {
set: function(obj, prop, value, receiver) {
return Reflect.set(...arguments);
},
};
const proxy = new Proxy(person, handler);
proxy.age = 20;
console.log(person) // {age: 18} 說明修改失敗
複製代碼
deleteProperty(target, propKey)
target:目標對象
propKey:待攔截屬性名
返回:嚴格模式下只有返回true, 不然報錯
做用: 攔截刪除target對象的propKey屬性的操做
攔截操做: delete proxy[propKey]
對應Reflect: Reflect.delete(obj, prop)
var foot = { apple: '蘋果' , banana:'香蕉' }
var proxy = new Proxy(foot, {
deleteProperty(target, prop) {
console.log('當前刪除水果 :',target[prop])
return delete target[prop]
}
});
delete proxy.apple
console.log(foot)
/* 運行結果: '當前刪除水果 : 蘋果' { banana:'香蕉' } */
複製代碼
特殊狀況: 屬性是不可配置屬性時,不能刪除
var foot = { apple: '蘋果' }
Object.defineProperty(foot, 'banana', {
value: '香蕉',
configurable: false
})
var proxy = new Proxy(foot, {
deleteProperty(target, prop) {
return delete target[prop];
}
})
delete proxy.banana /* 沒有效果 */
console.log(foot)
複製代碼
ownKeys(target)
target:目標對象
返回: 數組(數組元素必須是字符或者Symbol,其餘類型報錯)
做用: 攔截獲取鍵值的操做
攔截操做:
1 Object.getOwnPropertyNames(proxy)
2 Object.getOwnPropertySymbols(proxy)
3 Object.keys(proxy)
4 for...in...循環
對應Reflect:Reflect.ownKeys()
var obj = { a: 10, [Symbol.for('foo')]: 2 };
Object.defineProperty(obj, 'c', {
value: 3,
enumerable: false
})
var p = new Proxy(obj, {
ownKeys(target) {
return [...Reflect.ownKeys(target), 'b', Symbol.for('bar')]
}
})
const keys = Object.keys(p) // ['a']
// 自動過濾掉Symbol/非自身/不可遍歷的屬性
/* 和Object.keys()過濾性質同樣,只返回target自己的可遍歷屬性 */
for(let prop in p) {
console.log('prop-',prop) /* prop-a */
}
/* 只返回攔截器返回的非Symbol的屬性,無論是否是target上的屬性 */
const ownNames = Object.getOwnPropertyNames(p) /* ['a', 'c', 'b'] */
/* 只返回攔截器返回的Symbol的屬性,無論是否是target上的屬性*/
const ownSymbols = Object.getOwnPropertySymbols(p)// [Symbol(foo), Symbol(bar)]
/*返回攔截器返回的全部值*/
const ownKeys = Reflect.ownKeys(p)
// ['a','c',Symbol(foo),'b',Symbol(bar)]
複製代碼
vue3.0 創建響應式的方法有兩種: 第一個就是運用composition-api中的reactive直接構建響應式,composition-api的出現咱們能夠在.vue文件中,直接用setup()函數來處理以前的大部分邏輯,也就是說咱們沒有必要在 export default{ } 中在聲明生命週期 , data(){} 函數,watch{} , computed{} 等 ,取而代之的是咱們在setup函數中,用vue3.0 reactive watch 生命週期api來到達一樣的效果,這樣就像react-hooks同樣提高代碼的複用率,邏輯性更強。
第二個就是用傳統的 data(){ return{} } 形式 ,vue3.0沒有放棄對vue2.0寫法的支持,而是對vue2.0的寫法是徹底兼容的,提供了applyOptions 來處理options形式的vue組件。可是options裏面的data , watch , computed等處理邏輯,仍是用了composition-api中的API對應處理。
Reactive 至關於當前的 Vue.observable () API,通過reactive處理後的函數能變成響應式的數據,相似於option api裏面的vue處理data函數的返回值。
咱們用一個todoList的demo試着嚐嚐鮮。
const { reactive , onMounted } = Vue
setup(){
const state = reactive({
count:0,
todoList:[]
})
/* 生命週期mounted */
onMounted(() => {
console.log('mounted')
})
/* 增長count數量 */
function add(){
state.count++
}
/* 減小count數量 */
function del(){
state.count--
}
/* 添加代辦事項 */
function addTodo(id,title,content){
state.todoList.push({
id,
title,
content,
done:false
})
}
/* 完成代辦事項 */
function complete(id){
for(let i = 0; i< state.todoList.length; i++){
const currentTodo = state.todoList[i]
if(id === currentTodo.id){
state.todoList[i] = {
...currentTodo,
done:true
}
break
}
}
}
return {
state,
add,
del,
addTodo,
complete
}
}
複製代碼
options形式的和vue2.0並無什麼區別
export default {
data(){
return{
count:0,
todoList:[]
}
},
mounted(){
console.log('mounted')
}
methods:{
add(){
this.count++
},
del(){
this.count--
},
addTodo(id,title,content){
this.todoList.push({
id,
title,
content,
done:false
})
},
complete(id){
for(let i = 0; i< this.todoList.length; i++){
const currentTodo = this.todoList[i]
if(id === currentTodo.id){
this.todoList[i] = {
...currentTodo,
done:true
}
break
}
}
}
}
}
複製代碼
vue3.0能夠根據業務需求引進不一樣的API方法。這裏須要
創建響應式reactive,返回proxy對象,這個reactive能夠深層次遞歸,也就是若是發現展開的屬性值是引用類型的並且被引用,還會用reactive遞歸處理。並且屬性是能夠被修改的。
創建響應式shallowReactive,返回proxy對象。和reactive的區別是隻創建一層的響應式,也就是說若是發現展開屬性是引用類型也不會遞歸。
返回的proxy處理的對象,能夠展開遞歸處理,可是屬性是隻讀的,不能修改。能夠作props傳遞給子組件使用。
返回通過處理的proxy對象,可是創建響應式屬性是隻讀的,不展開引用也不遞歸轉換,能夠這用於爲有狀態組件建立props代理對象。
上文中咱們說起到。用Reactive處理過並返回的對象是一個proxy對象,假設存在不少組件,或者在一個組件中被屢次reactive,就會有不少對proxy對象和它代理的原對象。爲了能把proxy對象和原對象創建關係,vue3.0採用了WeakMap去儲存這些對象關係。WeakMaps 保持了對鍵名所引用的對象的弱引用,即垃圾回收機制不將該引用考慮在內。只要所引用的對象的其餘引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦再也不須要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>() /* 只讀的 */
const readonlyToRaw = new WeakMap<any, any>() /* 只讀的 */
複製代碼
vue3.0 用readonly來設置被攔截器攔截的對象可否被修改,能夠知足以前的props不能被修改的單向數據流場景。 咱們接下來重點講一下接下來的四個weakMap的儲存關係。
rawToReactive
鍵值對 : { [targetObject] : obseved }
target(鍵):目標對象值(這裏能夠理解爲reactive的第一個參數。) obsered(值):通過proxy代理以後的proxy對象。
reactiveToRaw reactiveToRaw 儲存的恰好與 rawToReactive的鍵值對是相反的。 鍵值對 { [obseved] : targetObject }
rawToReadonly
鍵值對 : { [target] : obseved }
target(鍵):目標對象。 obsered(值):通過proxy代理以後的只讀屬性的proxy對象。
readonlyToRaw 儲存狀態與rawToReadonly恰好相反。
接下來咱們重點從reactive開始講。
/* TODO: */
export function reactive(target: object) {
if (readonlyToRaw.has(target)) {
return target
}
return createReactiveObject(
target, /* 目標對象 */
rawToReactive, /* { [targetObject] : obseved } */
reactiveToRaw, /* { [obseved] : targetObject } */
mutableHandlers, /* 處理 基本數據類型 和 引用數據類型 */
mutableCollectionHandlers /* 用於處理 Set, Map, WeakMap, WeakSet 類型 */
)
}
複製代碼
reactive函數的做用就是經過createReactiveObject方法產生一個proxy,並且針對不一樣的數據類型給定了不一樣的處理方法。
以前說到的createReactiveObject,咱們接下來看看createReactiveObject發生了什麼。
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) {
/* 判斷目標對象是否被effect */
/* observed 爲通過 new Proxy代理的函數 */
let observed = toProxy.get(target) /* { [target] : obseved } */
if (observed !== void 0) { /* 若是目標對象已經被響應式處理,那麼直接返回proxy的observed對象 */
return observed
}
if (toRaw.has(target)) { /* { [observed] : target } */
return target
}
/* 若是目標對象是 Set, Map, WeakMap, WeakSet 類型,那麼 hander函數是 collectionHandlers 否側目標函數是baseHandlers */
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
/* TODO: 建立響應式對象 */
observed = new Proxy(target, handlers)
/* target 和 observed 創建關聯 */
toProxy.set(target, observed)
toRaw.set(observed, target)
/* 返回observed對象 */
return observed
}
複製代碼
經過上面源碼建立proxy對象的大體流程是這樣的: ①首先判斷目標對象有沒有被proxy響應式代理過,若是是那麼直接返回對象。 ②而後經過判斷目標對象是不是[ Set, Map, WeakMap, WeakSet ]數據類型來選擇是用collectionHandlers , 仍是baseHandlers->就是reactive傳進來的mutableHandlers做爲proxy的hander對象。 ③最後經過真正使用new proxy來建立一個observed ,而後經過rawToReactive reactiveToRaw 保存 target和observed鍵值對。
大體流程圖:
以前咱們介紹過baseHandlers就是調用reactive方法createReactiveObject傳進來的mutableHandlers對象。 咱們先來看一下mutableHandlers對象
mutableHandlers
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
複製代碼
vue3.0 用到了以上幾個攔截器,咱們在上節已經介紹了這幾個攔截器的基本用法,首先咱們對幾個基本用到的攔截器在作一下回顧。
①get,對數據的讀取屬性進行攔截,包括 target.點語法 和 target[]
②set,對數據的存入屬性進行攔截 。
③deleteProperty delete操做符進行攔截。
vue2.0不能對對象的delete操做符進行屬性攔截。
例子🌰:
delete object.a
複製代碼
是沒法監測到的。
vue3.0proxy中deleteProperty 能夠攔截 delete 操做符,這就表述vue3.0響應式能夠監聽到屬性的刪除操做。
④has,對 in 操做符進行屬性攔截。
vue2.0不能對對象的in操做符進行屬性攔截。
例子
a in object
複製代碼
has 是爲了解決如上問題。這就表示了vue3.0能夠對 in 操做符 進行攔截。
⑤ownKeys Object.keys(proxy) ,for...in...循環 Object.getOwnPropertySymbols(proxy) , Object.getOwnPropertyNames(proxy) 攔截器
例子
Object.keys(object)
複製代碼
說明vue3.0能夠對以上這些方法進行攔截。
若是咱們想要弄明白整個響應式原理。那麼組件初始化,到初始化過程當中composition-api的reactive處理data,以及編譯階段對data屬性進行依賴收集是分不開的。vue3.0提供了一套從初始化,到render過程當中依賴收集,到組件更新,到組件銷燬完整響應式體系,咱們很難從一個角度把東西講明白,因此在正式講攔截器對象如何收集依賴,派發更新以前,咱們看看effect作了些什麼操做。
vue3.0用effect反作用鉤子來代替vue2.0watcher。咱們都知道在vue2.0中,有渲染watcher專門負責數據變化後的重新渲染視圖。vue3.0改用effect來代替watcher達到一樣的效果。
咱們先簡單介紹一下mountComponent流程,後面的文章會詳細介紹mount階段的
// 初始化組件
const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => {
/* 第一步: 建立component 實例 */
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
/* 第二步 : TODO:初始化 初始化組件,創建proxy , 根據字符竄模版獲得 */
setupComponent(instance)
/* 第三步:創建一個渲染effect,執行effect */
setupRenderEffect(
instance, // 組件實例
initialVNode, //vnode
container, // 容器元素
anchor,
parentSuspense,
isSVG,
optimized
)
}
複製代碼
上面是整個mountComponent的主要分爲了三步,咱們這裏分別介紹一下每一個步驟幹了什麼: ① 第一步: 建立component 實例 。 ② 第二步:初始化組件,創建proxy ,根據字符竄模版獲得render函數。生命週期鉤子函數處理等等 ③ 第三步:創建一個渲染effect,執行effect。
從如上方法中咱們能夠看到,在setupComponent已經構建了響應式對象,可是尚未初始化收集依賴。
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => {
/* 建立一個渲染 effect */
instance.update = effect(function componentEffect() {
//...省去的內容後面會講到
},{ scheduler: queueJob })
}
複製代碼
爲了讓你們更清楚的明白響應式原理,我這隻保留了和響應式原理有關係的部分代碼。
setupRenderEffect的做用
① 建立一個effect,並把它賦值給組件實例的update方法,做爲渲染更新視圖用。 ② componentEffect做爲回調函數形式傳遞給effect做爲第一個參數
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> {
const effect = createReactiveEffect(fn, options)
/* 若是不是懶加載 當即執行 effect函數 */
if (!options.lazy) {
effect()
}
return effect
}
複製代碼
effect做用以下
① 首先調用。createReactiveEffect ② 若是不是懶加載 當即執行 由createReactiveEffect建立出來的ReactiveEffect函數
function createReactiveEffect<T = any>( fn: (...args: any[]) => T, /**回調函數 */ options: ReactiveEffectOptions ): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
try {
enableTracking()
effectStack.push(effect) //往effect數組中裏放入當前 effect
activeEffect = effect //TODO: effect 賦值給當前的 activeEffect
return fn(...args) //TODO: fn 爲effect傳進來 componentEffect
} finally {
effectStack.pop() //完成依賴收集後從effect數組刪掉這個 effect
resetTracking()
/* 將activeEffect還原到以前的effect */
activeEffect = effectStack[effectStack.length - 1]
}
} as ReactiveEffect
/* 配置一下初始化參數 */
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = [] /* TODO:用於收集相關依賴 */
effect.options = options
return effect
}
複製代碼
createReactiveEffect
createReactiveEffect的做用主要是配置了一些初始化的參數,而後包裝了以前傳進來的fn,重要的一點是把當前的effect賦值給了activeEffect,這一點很是重要,和收集依賴有着直接的關係
在這裏留下了一個疑點,
①爲何要用effectStack數組來存放這裏effect
咱們這裏個響應式初始化階段進行總結
① setupComponent建立組件,調用composition-api,處理options(構建響應式)獲得Observer對象。
② 建立一個渲染effect,裏面包裝了真正的渲染方法componentEffect,添加一些effect初始化屬性。
③ 而後當即執行effect,而後將當前渲染effect賦值給activeEffect
最後咱們用一張圖來解釋一下整個流程。
/* 深度get */
const get = /*#__PURE__*/ createGetter()
/* 淺get */
const shallowGet = /*#__PURE__*/ createGetter(false, true)
/* 只讀的get */
const readonlyGet = /*#__PURE__*/ createGetter(true)
/* 只讀的淺get */
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
複製代碼
上面咱們能夠知道,對於以前講的四種不一樣的創建響應式方法,對應了四種不一樣的get,下面是一一對應關係。
reactive ---------> get
shallowReactive --------> shallowGet
readonly ----------> readonlyGet
shallowReadonly ---------------> shallowReadonlyGet
四種方法都是調用了createGetter方法,只不過是參數的配置不一樣,咱們這裏那第一個get方法作參考,接下來探索一下createGetter。
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
/* 淺邏輯 */
if (shallow) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
}
/* 數據綁定 */
!isReadonly && track(target, TrackOpTypes.GET, key)
return isObject(res)
? isReadonly
?
/* 只讀屬性 */
readonly(res)
/* */
: reactive(res)
: res
}
}
複製代碼
這就是createGetter主要流程,特殊的數據類型和ref咱們暫時先不考慮。 這裏用了一些流程判斷,咱們用流程圖來講明一下這個函數主要作了什麼?
咱們能夠得出結論: 在vue2.0的時候。響應式是在初始化的時候就深層次遞歸處理了 可是
與vue2.0不一樣的是,即使是深度響應式咱們也只能在獲取上一級get以後才能觸發下一級的深度響應式。 好比
setup(){
const state = reactive({ a:{ b:{} } })
return {
state
}
}
複製代碼
在初始化的時候,只有a的一層級創建了響應式,b並無創建響應式,而當咱們用state.a的時候,纔會真正的將b也作響應式處理,也就是說咱們訪問了上一級屬性後,下一代屬性纔會真正意義上創建響應式
這樣作好處是, 1 初始化的時候不用遞歸去處理對象,形成了沒必要要的性能開銷。 *2 有一些沒有用上的state,這裏就不須要在深層次響應式處理。
咱們先來看看track源碼:
/* target 對象自己 ,key屬性值 type 爲 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
/* 當打印或者獲取屬性的時候 console.log(this.a) 是沒有activeEffect的 當前返回值爲0 */
let depsMap = targetMap.get(target)
if (!depsMap) {
/* target -map-> depsMap */
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
/* key : dep dep觀察者 */
depsMap.set(key, (dep = new Set()))
}
/* 當前activeEffect */
if (!dep.has(activeEffect)) {
/* dep添加 activeEffect */
dep.add(activeEffect)
/* 每一個 activeEffect的deps 存放當前的dep */
activeEffect.deps.push(dep)
}
}
複製代碼
裏面主要引入了兩個概念 targetMap 和 depsMap
targetMap 鍵值對 proxy : depsMap proxy : 爲reactive代理後的 Observer對象 。 depsMap :爲存放依賴dep的 map 映射。
depsMap 鍵值對:key : deps key 爲當前get訪問的屬性名, deps 存放effect的set數據類型。
咱們知道track做用大體是,首先根據 proxy對象,獲取存放deps的depsMap,而後經過訪問的屬性名key獲取對應的dep,而後將當前激活的effect存入當前dep收集依賴。
主要做用 ①找到與當前proxy 和 key對應的dep。 ②dep與當前activeEffect創建聯繫,收集依賴。
爲了方便理解,targetMap 和 depsMap的關係,下面咱們用一個例子來講明: 例子: 父組件A
<div id="app" >
<span>{{ state.a }}</span>
<span>{{ state.b }}</span>
<div>
<script> const { createApp, reactive } = Vue /* 子組件 */ const Children ={ template="<div> <span>{{ state.c }}</span> </div>", setup(){ const state = reactive({ c:1 }) return { state } } } /* 父組件 */ createApp({ component:{ Children } setup(){ const state = reactive({ a:1, b:2 }) return { state } } })mount('#app') </script>
複製代碼
咱們用一幅圖表示如上關係:
咱們在前面說過,建立一個渲染renderEffect,而後把賦值給activeEffect,最後執行renderEffect ,在這個期間是怎麼作依賴收集的呢,讓咱們一塊兒來看看,update函數中作了什麼,咱們回到以前講的componentEffect邏輯上來
function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, a, parent } = instance
/* TODO: 觸發instance.render函數,造成樹結構 */
const subTree = (instance.subTree = renderComponentRoot(instance))
if (bm) {
//觸發 beforeMount聲明週期鉤子
invokeArrayFns(bm)
}
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
/* 觸發聲明週期 mounted鉤子 */
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
} else {
// 更新組件邏輯
// ......
}
}
複製代碼
這邊代碼大體首先會經過renderComponentRoot方法造成樹結構,這裏要注意的是,咱們在最初mountComponent的setupComponent方法中,已經經過編譯方法compile編譯了template模版的內容,state.a state.b等抽象語法樹,最終返回的render函數在這個階段會被觸發,在render函數中在模版中的表達式 state.a state.b 點語法會被替換成data中真實的屬性,這時候就進行了真正的依賴收集,觸發了get方法。接下來就是觸發生命週期 beforeMount ,而後對整個樹結構從新patch,patch完畢後,調用mounted鉤子
① 首先執行renderEffect ,賦值給activeEffect ,調用renderComponentRoot方法,而後觸發render函數。
② 根據render函數,解析通過compile,語法樹處理事後的模版表達式,訪問真實的data屬性,觸發get。
③ get方法首先通過以前不一樣的reactive,經過track方法進行依賴收集。
④ track方法經過當前proxy對象target,和訪問的屬性名key來找到對應的dep。
⑤ 將dep與當前的activeEffect創建起聯繫。將activeEffect壓入dep數組中,(此時的dep中已經含有當前組件的渲染effect,這就是響應式的根本緣由)若是咱們觸發set,就能在數組中找到對應的effect,依次執行。
最後咱們用一個流程圖來表達一下依賴收集的流程。
接下來咱們set部分邏輯。
const set = /*#__PURE__*/ createSetter()
/* 淺邏輯 */
const shallowSet = /*#__PURE__*/ createSetter(true)
複製代碼
set也是分兩個邏輯,set和shallowSet,兩種方法都是由createSetter產生,咱們這裏主要以set進行剖析。
function createSetter(shallow = false) {
return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {
const oldValue = (target as any)[key]
/* shallowSet邏輯 */
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
/* 判斷當前對象,和存在reactiveToRaw 裏面是否相等 */
if (target === toRaw(receiver)) {
if (!hadKey) { /* 新建屬性 */
/* TriggerOpTypes.ADD -> add */
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
/* 改變原有屬性 */
/* TriggerOpTypes.SET -> set */
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
複製代碼
createSetter的流程大體是這樣的
① 首先經過toRaw判斷當前的proxy對象和創建響應式存入reactiveToRaw的proxy對象是否相等。 ② 判斷target有沒有當前key,若是存在的話,改變屬性,執行trigger(target, TriggerOpTypes.SET, key, value, oldValue)。 ③ 若是當前key不存在,說明是賦值新屬性,執行trigger(target, TriggerOpTypes.ADD, key, value)。
/* 根據value值的改變,從effect和computer拿出對應的callback ,而後依次執行 */
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) {
/* 獲取depssMap */
const depsMap = targetMap.get(target)
/* 沒有通過依賴收集的 ,直接返回 */
if (!depsMap) {
return
}
const effects = new Set<ReactiveEffect>() /* effect鉤子隊列 */
const computedRunners = new Set<ReactiveEffect>() /* 計算屬性隊列 */
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) { /* 處理computed邏輯 */
computedRunners.add(effect) /* 儲存對應的dep */
} else {
effects.add(effect) /* 儲存對應的dep */
}
}
})
}
}
add(depsMap.get(key))
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) { /* 放進 scheduler 調度*/
effect.options.scheduler(effect)
} else {
effect() /* 不存在調度狀況,直接執行effect */
}
}
//TODO: 必須首先運行計算屬性的更新,以便計算的getter
//在任何依賴於它們的正常更新effect運行以前,均可能失效。
computedRunners.forEach(run) /* 依次執行computedRunners 回調*/
effects.forEach(run) /* 依次執行 effect 回調( TODO: 裏面包括渲染effect )*/
}
複製代碼
咱們這裏保留了trigger的核心邏輯
① 首先從targetMap中,根據當前proxy找到與之對應的depsMap。 ② 根據key找到depsMap中對應的deps,而後經過add方法分離出對應的effect回調函數和computed回調函數。 ③ 依次執行computedRunners 和 effects 隊列裏面的回調函數,若是發現須要調度處理,放進scheduler事件調度
值得注意的的是:
此時的effect隊列中有咱們上述負責渲染的renderEffect,還有經過effectAPI創建的effect,以及經過watch造成的effect。咱們這裏只考慮到渲染effect。至於後面的狀況會在接下來的文章中和你們一塊兒分享。
咱們用一幅流程圖說明一下set過程。
咱們總結一下整個數據綁定創建響應式大體分爲三個階段
1 初始化階段: 初始化階段經過組件初始化方法造成對應的proxy對象,而後造成一個負責渲染的effect。
2 get依賴收集階段:經過解析template,替換真實data屬性,來觸發get,而後經過stack方法,經過proxy對象和key造成對應的deps,將負責渲染的effect存入deps。(這個過程還有其餘的effect,好比watchEffect存入deps中 )。
3 set派發更新階段:當咱們 this[key] = value 改變屬性的時候,首先經過trigger方法,經過proxy對象和key找到對應的deps,而後給deps分類分紅computedRunners和effect,而後依次執行,若是須要調度的,直接放入調度。
微信掃碼關注公衆號,按期分享技術文章