您好,登錄后才能下訂單哦!
這篇文章主要介紹Vue3.0響應式系統的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
引子
先來段代碼,大家可以直接復制哦,注意引用的文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script src="../packages/vue/dist/vue.global.js"></script> </head> <body> <div id="app"></div> <script> const { reactive, computed, effect, watch, createApp } = Vue const App = { template: ` <div id="box"> <button @click="increment">{{ state.count }}</button> </div> `, setup() { const state = reactive({ count: 0 }) function increment(e) { state.count++ } effect(() => { console.log('count改變', state.count); }) return { state, increment } } } createApp().mount(App, '#app') </script> </body> </html>
這段代碼,想必大家都看得懂,點擊后count增加,視圖也隨之更新,effect監聽了count改變,那么為什么effect能觀察到count變化呢,還有為什么reactive可以實現響應式?
effect
為什么要先說這個函數呢,因為它和其他函數都息息相關,只有先了解它才能更好的理解其他響應式API
上源碼
export function effect( fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if ((fn as ReactiveEffect).isEffect) { fn = (fn as ReactiveEffect).raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }
if判斷,判斷如果傳入的fn函數,它已經是effect了,也就是一個標識,直接獲取該函數上的raw屬性,這個屬性后面會講到
調用createReactiveEffect
如果options中有lazy,就會立即調用effect,其實本質上調用的還是傳入的fn函數
// 了解一下options有哪些 { lazy?: boolean // 是否立即調用fn computed?: boolean // 是否是computed scheduler?: (run: Function) => void // 在調用fn之前執行 onTrack?: (event: DebuggerEvent) => void // 在依賴收集完成之后調用 onTrigger?: (event: DebuggerEvent) => void // 在調用fn之前執行,源碼上來看和scheduler調用時機一樣,只是傳入參數不同 onStop?: () => void // 清除依賴完成后調用 }
返回effect
createReactiveEffect
上面提到了createReactiveEffect函數,我們來看看它的實現
function createReactiveEffect( fn: Function, options: ReactiveEffectOptions ): ReactiveEffect { // 又包裝了一層函數 const effect = function effect(...args): any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect effect.isEffect = true // 標識effect effect.active = true // 如果active effect.raw = fn // 傳入的回調 effect.scheduler = options.scheduler effect.onTrack = options.onTrack effect.onTrigger = options.onTrigger effect.onStop = options.onStop effect.computed = options.computed effect.deps = [] // 用于收集依賴 return effect }
注意,敲黑板,這里有個run函數,很重要,因為它保存了依賴
function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } }
他把依賴存儲在了一個全局的數組中activeReactiveEffectStack, 他以棧的形式存儲,調用完依賴后,會彈出,大家要留意一下這里,后面會用到
怎么樣,是不是很簡單~
reactive
export function reactive(target: object) { // 如果target是已經被readonly對象,那么直接返回對應的proxy對象 if (readonlyToRaw.has(target)) { return target } // 如果target是已經被readonly對象,那么直接返回對應的真實對象 if (readonlyValues.has(target)) { return readonly(target) } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) }
前兩個if是用來處理這種情況的
// 情況一 const state1 = readonly({ count: 0 }) const state2 = reactive(state1) // 情況二 const obj = { count: 0 } const state1 = readonly(obj) const state2 = reactive(obj) 可以看到reactive它的參數是被readonly的對象,reactive不會對它再次創建響應式,而是通過Map映射,拿到對應的對象,即Proxy <==> Object的相互轉換。 createReactiveObject創建響應式對象,注意它的參數 createReactiveObject( target, rawToReactive, // Object ==> Proxy reactiveToRaw, // Proxy ==> Object mutableHandlers, // get set has ... mutableCollectionHandlers // 很少會用,不講了~ )
以上就是reative一開始所做的一些事情,下面繼續分析createReactiveObject
createReactiveObject
function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // 如果不是對象,在開發環境報出警告 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } let observed = toProxy.get(target) // 如果目標對象已經有proxy對象,直接返回 if (observed !== void 0) { return observed } // 如果目標對象是proxy的對象,并且有對應的真實對象,那么也直接返回 if (toRaw.has(target)) { return target } // 如果它是vnode或者vue,則不能被觀測 if (!canObserve(target)) { return target } // 判斷被觀測的對象是否是set,weakSet,map,weakMap,根據情況使用對應proxy的,配置對象 const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }
第一個if,判斷是否是對象,否則報出警告
toProxy拿到觀測對象的Proxy對象,如果存在直接返回
// 這種情況 const obj = { count: 0 } const state1 = reative(obj) const state2 = reative(obj)
toRaw拿到Proxy對象對應的真實對象,如果存在直接返回
// 這種情況 const obj = { count: 0 } const state1 = reative(obj) const state2 = reative(state1)
有些情況無法被觀測,則直接返回觀測對象本身
const canObserve = (value: any): boolean => { return ( !value._isVue && !value._isVNode && observableValueRE.test(toTypeString(value)) && !nonReactiveValues.has(value) ) }
設置handlers,即get,set等屬性訪問器, 注意:collectionHandlers是用來處理觀測對象為Set,Map等情況,很少見,這里就不講了
const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
然后創建了Proxy對象,并把觀測對象和Proxy對象,分別做映射
observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target)
然后在targetMap做了target ==> Map的映射,這又是干嘛,注意:targetMap是全局的
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap() if (!targetMap.has(target)) { targetMap.set(target, new Map()) }
在這里先給大家賣個關子,targetMap非常重要,是用來保存依賴的地方
講完了reactive,可以回到一開始的引子
依賴收集
說到依賴收集,不得不提到,依賴的創建,那么Vue3.0是在哪里創建了渲染依賴呢,大家可以找到下面這段代碼以及文件
// vue-next\packages\runtime-core\src\createRenderer.ts function setupRenderEffect( instance: ComponentInternalInstance, parentSuspense: HostSuspsenseBoundary | null, initialVNode: HostVNode, container: HostElement, anchor: HostNode | null, isSVG: boolean ) { // create reactive effect for rendering let mounted = false instance.update = effect(function componentEffect() { // ... }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }
代碼特別長,我剪掉了中間部分,大家還記得effect有個選項lazy嗎,沒錯,它默認是false,也就會立即調用傳入的componentEffect回調,在它內部調用了patch實現了組件的掛載。
敲黑板,關鍵來了,還記得effect調用,內部會調用run方法嗎
function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } }
這里進行了第一步的依賴收集,保存在全局數組中,為了方便觸發get的對象,將依賴收集到自己的deps中
然后就是調用patch,進行組件掛載
if (!mounted) { const subTree = (instance.subTree = renderComponentRoot(instance)) // beforeMount hook if (instance.bm !== null) { invokeHooks(instance.bm) } patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) initialVNode.el = subTree.el // mounted hook if (instance.m !== null) { queuePostRenderEffect(instance.m, parentSuspense) } mounted = true }
至于它內部實現,我就不講了,不是本文重點,然后我們去編譯的地方看看
//vue-next\packages\runtime-core\src\component.ts function finishComponentSetup( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null ) { const Component = instance.type as ComponentOptions if (!instance.render) { if (Component.template && !Component.render) { if (compile) { Component.render = compile(Component.template, { onError(err) {} }) } else if (__DEV__) { warn( `Component provides template but the build of Vue you are running ` + `does not support on-the-fly template compilation. Either use the ` + `full build or pre-compile the template using Vue CLI.` ) } } if (__DEV__ && !Component.render) { warn( `Component is missing render function. Either provide a template or ` + `return a render function from setup().` ) } instance.render = (Component.render || NOOP) as RenderFunction } // ...其他 }
上面的代碼是編譯部分,我們來看看例子中編譯后是什么樣
(function anonymous( ) { const _Vue = Vue const _createVNode = Vue.createVNode const _hoisted_1 = { id: "box" } return function render() { with (this) { const { toString: _toString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock("div", _hoisted_1, [ _createVNode("button", { onClick: increment }, _toString(state.count), 9 /* TEXT, PROPS */, ["onClick"]) ])) } } })
可以看到,編譯的代碼中,有使用到state.count,那么就會觸發get訪問器,從而收集依賴,至于為什么能直接訪問到屬性,原因是由于with設置了上下文,下面我們具體分析get
get
// vue-next\packages\reactivity\src\baseHandlers.ts function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (typeof key === 'symbol' && builtInSymbols.has(key)) { return res } // _isRef if (isRef(res)) { return res.value } track(target, OperationTypes.GET, key) // 如果該屬性對應的值還是對象,就繼續遞歸創建響應式 return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res } }
調用Reflect.get獲取屬性值
如果key是symbol并且是Symbol的一個屬性,就直接返回該值
// 這種情況 const key = Symbol('key') const state = reative({ [key]: 'symbol value' }) state[key]
如果值為Ref返回該值的value,看到這里如果大家有了解過ref api的話就知道了,由于ref它自己實現了自己的get,set,所以不再需要執行后面的邏輯,這個在后面會講
調用track
遞歸深度觀測,使整個對象都為響應式
下面我會詳細講解
track
在講它之前,先了解它有哪些參數
target: any, // 目標對象 type: OperationTypes, // 追蹤數據變化類型,這里是get key?: string | symbol // 需要獲取的key export const enum OperationTypes { SET = 'set', ADD = 'add', DELETE = 'delete', CLEAR = 'clear', GET = 'get', HAS = 'has', ITERATE = 'iterate' }
export function track( target: any, type: OperationTypes, key?: string | symbol ) { if (!shouldTrack) { return } // 獲取activeReactiveEffectStack中的依賴 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } // 獲取目標對象對應的依賴map let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } // 獲取對應屬性的依賴 let dep = depsMap.get(key as string | symbol) // 如果該依賴不存在 if (!dep) { // 設置屬性對應依賴 depsMap.set(key as string | symbol, (dep = new Set())) } // 如果屬性對應依賴set中不存在該依賴 if (!dep.has(effect)) { // 添加到依賴set中 dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.onTrack) { // 調用onTrack鉤子 effect.onTrack({ effect, target, type, key }) } } } }
activeReactiveEffectStack我兩次提到,從它這里拿到了依賴,注意后面執行完依賴后,會從它里面彈出
如果effect存在
從targetMap中獲取對象,對飲的Map,具體的數據結構類似這樣
const state = reative({ count: 0 }) effect(() => { console.log(state.count) }) // 依賴大致結構(隨便寫的,不太規范) { target(state):Map { count: Set (componentEffect渲染依賴, user自己添加的依賴) } }
如果該對象不存在Map,就初始化一個
如果該Map中屬性對應的Set不存在,就初始化一個Set
添加依賴到Set中
添加依賴到effect自身的deps數組中
最后調用onTrack回調
// 調用onTrack鉤子 effect.onTrack({ effect, target, type, key })
OK,Track實現大體就這樣,是不是也很簡單,有了這些基礎,后面要講的一些API就很容易理解了
set
當我們點擊按鈕后,就會觸發set屬性訪問器
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean { value = toRaw(value) const hadKey = hasOwn(target, key) const oldValue = target[key] // 如果舊的值是ref,而新的值不是ref if (isRef(oldValue) && !isRef(value)) { // 直接更改原始ref即可 oldValue.value = value return true } const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { /* istanbul ignore else */ if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key, extraInfo) } } else { if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key) } } } return result }
判斷舊值是ref,新值不是ref
// 這種情況 const val = ref(0) const state = reative({ count: val }) state.count = 1 // 其實state.count最終還是ref,還是能通過value訪問 state.count.value // 1
調用Reflect.set修改值
開發環境下,拿到新舊值組成的對象,調用trigger,為什么開發環境要這么做呢,其實是為了方便onTrigger能拿到新舊值
trigger(target, OperationTypes.ADD, key, extraInfo)
可以看到第二個參數和track是一樣的enum,有兩種情況,一種我們設置了新的屬性和值,另一種修改了原有屬性值,下面我們來看看trigger實現。
trigger
export function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } // effect set const effects: Set<ReactiveEffect> = new Set() // computed effect set const computedRunners: Set<ReactiveEffect> = new Set() if (type === OperationTypes.CLEAR) { depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else { // 添加effect到set中 if (key !== void 0) { addRunners(effects, computedRunners, depsMap.get(key as string | symbol)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } // 執行set中的effect const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } computedRunners.forEach(run) effects.forEach(run) }
看到這個函數開始的targetMap,大家應該很清楚要干嘛了吧,沒錯,拿到對象的Map,它包含了屬性的所有依賴
如果沒有Map直接返回
創建了兩個Set,要干嘛用呢
// 用來保存將要執行的依賴 const effects: Set<ReactiveEffect> = new Set() // computed依賴,因為trigger不僅是要處理effect,watch,還要處理computed惰性求值的情況 const computedRunners: Set<ReactiveEffect> = new Set()
處理三種情況CLEAR,ADD,DELETE,SET(這里沒有標識)
// effect set const effects: Set<ReactiveEffect> = new Set() // computed effect set const computedRunners: Set<ReactiveEffect> = new Set() function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } }
可以看到,三種情況實際上都差不多,唯一的區別就是,如果添加的對象是數組,就會拿到length屬性的依賴,用于修改數組長度
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) }
執行屬性對應的依賴
// 執行set中的effect const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } computedRunners.forEach(run) effects.forEach(run)
function scheduleRun( effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) { if (__DEV__ && effect.onTrigger) { effect.onTrigger( extend( { effect, target, key, type }, extraInfo // { oldValue, newValue: value } ) ) } if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } }
最后調用了scheduleRun,它內部會分別執行onTrigger,scheduler,effect
需要注意的是,只有開發環境才會執行onTrigger,這也是為什么,前面要這么判斷
if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key, extraInfo) } }
readonly
有了前面的基礎,readonly看起來會非常簡單,唯一的區別就是rawToReadonly,rawToReadonly, readonlyHandlers
export function readonly(target: object) { if (reactiveToRaw.has(target)) { target = reactiveToRaw.get(target) } return createReactiveObject( target, rawToReadonly, readonlyToRaw, readonlyHandlers, readonlyCollectionHandlers ) }
前兩個大家應該能猜出來了,關鍵是最后這個readonlyHandlers,區別就在set
set(target: any, key: string | symbol, value: any, receiver: any): boolean { if (LOCKED) { if (__DEV__) { console.warn( `Set operation on key "${key as any}" failed: target is readonly.`, target ) } return true } else { return set(target, key, value, receiver) } }
它的實現很簡單,不過LOCKED有是什么鬼,大家可以找到lock.ts
//vue-next\packages\reactivity\src\lock.ts export let LOCKED = true export function lock() { LOCKED = true } export function unlock() { LOCKED = false }
看似簡單,但是卻非常重要,它能夠控制被readonly的對象能夠暫時被更改,就比如我們常用的props,它是無法被修改的,但是Vue內部又要對他進行更新,那怎么辦,話不多說,我們再源碼中看他具體應用
// vue-next\packages\runtime-core\src\componentProps.ts export function resolveProps( instance: ComponentInternalInstance, rawProps: any, _options: ComponentPropsOptions | void ) { const hasDeclaredProps = _options != null const options = normalizePropsOptions(_options) as NormalizedPropsOptions if (!rawProps && !hasDeclaredProps) { return } const props: any = {} let attrs: any = void 0 const propsProxy = instance.propsProxy const setProp = propsProxy ? (key: string, val: any) => { props[key] = val propsProxy[key] = val } : (key: string, val: any) => { props[key] = val } unlock() // 省略一些修改props操作。。 lock() instance.props = __DEV__ ? readonly(props) : props instance.attrs = options ? __DEV__ && attrs != null ? readonly(attrs) : attrs : instance.props }
這里前后分別調用了unlock和lock,這樣就可以控制對readonly屬性的修改
那么readonly的講解就到這了
computed
export function computed<T>( getterOrOptions: (() => T) | WritableComputedOptions<T> ): any { const isReadonly = isFunction(getterOrOptions) const getter = isReadonly ? (getterOrOptions as (() => T)) : (getterOrOptions as WritableComputedOptions<T>).get const setter = isReadonly ? null : (getterOrOptions as WritableComputedOptions<T>).set let dirty: boolean = true let value: any = undefined const runner = effect(getter, { lazy: true, computed: true, scheduler: () => { dirty = true } }) return { _isRef: true, // expose effect so computed can be stopped effect: runner, get value() { if (dirty) { value = runner() dirty = false } trackChildRun(runner) return value }, set value(newValue) { if (setter) { setter(newValue) } else { // TODO warn attempting to mutate readonly computed value } } } }
首先是前面這段
const isReadonly = isFunction(getterOrOptions) const getter = isReadonly ? (getterOrOptions as (() => T)) : (getterOrOptions as WritableComputedOptions<T>).get const setter = isReadonly ? null : (getterOrOptions as WritableComputedOptions<T>).set
大家都知道computed是可以單獨寫一個函數,或者get,set訪問的,這里不多講
然后調用了effect,這里lazy設置為true, scheduler可以更改dirty為true
const runner = effect(getter, { lazy: true, computed: true, scheduler: () => { dirty = true } })
然后我們具體來看看,返回的對象
{ _isRef: true, // expose effect so computed can be stopped effect: runner, get value() { if (dirty) { value = runner() dirty = false } trackChildRun(runner) return value }, set value(newValue) { if (setter) { setter(newValue) } else { // TODO warn attempting to mutate readonly computed value } } }
先說說set吧,尤大似乎還沒寫完,只是單純能修改值
然后是get,注意dirty的變化,如果computed依賴了state中的值,初次渲染時,他會調用依賴,然后dirty = false,關鍵來了,最后執行了trackChildRun
function trackChildRun(childRunner: ReactiveEffect) { const parentRunner = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (parentRunner) { for (let i = 0; i < childRunner.deps.length; i++) { const dep = childRunner.deps[i] if (!dep.has(parentRunner)) { dep.add(parentRunner) parentRunner.deps.push(dep) } } } }
由于computed是依賴了state中的屬性的,一旦在初始時觸發了get,執行runner,就會將依賴收集到activeReactiveEffectStack中,最后才是自己的依賴,棧的頂部是state屬性的依賴
if (!dep.has(parentRunner)) { dep.add(parentRunner) parentRunner.deps.push(dep) }
所以最后這段代碼實現了state屬性變化后,才導致了computed依賴的調用,從而惰性求值
ref
const convert = (val: any): any => (isObject(val) ? reactive(val) : val) export function ref<T>(raw: T): Ref<T> { raw = convert(raw) const v = { _isRef: true, get value() { track(v, OperationTypes.GET, '') return raw }, set value(newVal) { raw = convert(newVal) trigger(v, OperationTypes.SET, '') } } return v as Ref<T> }
ref的實現真的很簡單了,前面已經學習了那么多,相信大家都能看懂了,區別就是convert(raw)對傳入的值進行了簡單判斷,如果是對象就設置為響應式,否則返回原始值。
以上是“Vue3.0響應式系統的示例分析”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。