Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。html
proxy是es6新特性,爲了對目標的做用主要是經過handler對象中的攔截方法攔截目標對象target的某些行爲(如屬性查找、賦值、枚舉、函數調用等)。vue
/* 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 的支持;;
vue2.0 用 Object.defineProperty 做爲響應式原理的實現,可是會有它的侷限性,好比 沒法監聽數組基於下標的修改,不支持 Map、Set、WeakMap 和 WeakSet等缺陷 ,因此改用了proxy解決了這些問題,這也意味着vue3.0將放棄對低版本瀏覽器的兼容(兼容版本ie11以上)。node
vue3.0 響應式用到的捕獲器(接下來會重點介紹)react
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 用到)es6
vue3.0 響應式沒用到的捕獲器(有興趣的同窗能夠研究一下)api
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 操做符 的捕捉器。數組
has(target, propKey)瀏覽器
target:目標對象微信
propKey:待攔截屬性名app
做用: 攔截判斷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,而後依次執行,若是須要調度的,直接放入調度。
微信掃碼關注公衆號,按期分享技術文章