上節咱們講了數據綁定proxy原理,vue3.0用到的基本的攔截器,以及reactive入口等等。調用reactive創建響應式,首先經過判斷數據類型來肯定使用的hander,而後建立proxy代理對象observed。這裏的疑惑點就是hander對象具體作了什麼?本文咱們將已baseHandlers爲着手點,繼續分析響應式原理。html
連載文章是大體是這樣的,可能會根據變化隨時更改:
1 數據綁定原理(上)
2 數據綁定原理(下)
3 computed和watch原理
4 事件系統
5 ceateApp
6 初始化mounted和patch流程。
7 diff算法與2.0區別
8 編譯compiler系列
...vue
以前咱們介紹過baseHandlers就是調用reactive方法createReactiveObject傳進來的mutableHandlers對象。
咱們先來看一下mutableHandlers對象node
mutableHandlersreact
export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys }
vue3.0 用到了以上幾個攔截器,咱們在上節已經介紹了這幾個攔截器的基本用法,首先咱們對幾個基本用到的攔截器在作一下回顧。算法
①get,對數據的讀取屬性進行攔截,包括 target.點語法 和 target[]api
②set,對數據的存入屬性進行攔截 。數組
③deleteProperty delete操做符進行攔截。微信
vue2.0不能對對象的delete操做符進行屬性攔截。app
例子🌰:函數
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,而後依次執行,若是須要調度的,直接放入調度。
還有一些問題沒有解決,好比:
① 爲何要用effectStack數組來存放這裏effect。
② 何時向deps存入其餘的effect。
等等...
帶着這些問題,但願咱們在接下來的文章中,一塊兒探討。
微信掃碼關注公衆號,按期分享技術文章