vue3.x原始碼剖析之資料響應式的深入講解
目錄
- 前言
- 什麼是資料響應式
- 資料響應式的大體流程
- 2.x資料響應式和3.x響應式對比
- 大致流程圖
- 實現依賴收集
- 程式碼倉庫
- 結尾
前言
如果錯過了秋楓和冬雪,那麼春天的櫻花一定會盛開吧。最近一直在準備自己的考試,考完試了,終於可以繼續研究原始碼和寫文章了,哈哈哈。學過vue的都知道,資料響應式在vue框架中極其重要,寫程式碼也好,面試也罷,資料響應式都是核心的內容。在vue3的官網文件中,作者說如果想讓資料更加響應式的話,可以把資料放在reactive裡面,官方文件在講述這裡的時候一筆帶過,筆者剛開始也不是很理解。後來看了原始碼才知道,在vue3裡面響應式已經變成了一個單獨的模組,而處理響應式的模組就是reactive;
什麼是資料響應式
從一開始使用 Vue 時,對於之前的 jq 開發而言,一個很大的區別就是基本不用手動操作 dom,data 中宣告的資料狀態改變後會自動重新渲染相關的 dom。
換句話說就是 Vue 自己知道哪個資料狀態發生了變化及哪裡有用到這個資料需要隨之修改。
因此實現資料響應式有兩個重點問題:
- 如何知道資料發生了變化?
- 如何知道資料變化後哪裡需要修改?
對於第一個問題,如何知道資料發生了變化,Vue3 之前使用了 ES5 的一個 API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是對需要偵測的資料進行 變化偵測 ,新增 getter 和 setter ,這樣就可以知道資料何時被讀取和修改。
第二個問題,如何知道資料變化後哪裡需要修改,Vue 對於每個資料都收集了與之相關的 依賴 ,這裡的依賴其實就是一個物件,儲存有該資料的舊值及資料變化後需要執行的函式。每個響應式的資料變化時會遍歷通知其對應的每個依賴,依賴收到通知後會判斷一下新舊值有沒有發生變化,如果變化則執行回撥函式響應資料變化(比如修改 dom)。
資料響應式的大體流程
在vue3.0的響應式的部分,我們需要找的核心檔案是vue3.0原始碼的packages裡面的runtime-core下面的src裡面的;我們今天研究的這條線,就是沿著render這條線走下去的;
return { render,hydrate,createApp: createAppAPI(render,hydrate) }
在該檔案下找到render函式,如下所示;該函式的作用是渲染傳入vnode,到指定容器中;
const render: RootRenderFunction = (vnode,container) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode,null,true) } } else { patch(container._vnode || null,vnode,container) } flushPostFlushCbs() container._vnode = vnode }
檢視patch方法,初始化的話會走else if (shapeFlag & ShapeFlags.COMPONENT)
const patch: PatchFn = ( n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,isSVG = false,optimized = false ) => { // patching & not same type,unmount old tree if (n1 && !isSameVNodeType(n1,n2)) { anchor = getNextHostNode(n1) unmount(n1,parentComponent,parentSuspense,true) n1 = null } if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } const { type,ref,shapeFlag } = n2 switch (type) { case Text: processText(n1,anchor) break case Comment: processCommentNode(n1,anchor) break case Static: if (n1 == null) { mountStaticNode(n2,anchor,isSVG) } else if (__DEV__) { patchStaticNode(n1,isSVG) } break case Fragment: processFragment( n1,isSVG,optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1,optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { // 初始化走這個 processComponent( n1,optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process( n1 as TeleportVNode,n2 as TeleportVNode,optimized,internals ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process( n1,internals ) } else if (__DEV__) { warn('Invalid VNode type:',type,`(${typeof type})`) } } // set ref if (ref != null && parentComponent) { setRef(ref,n1 && n1.ref,n2) } }
接下來檢視processComponent方法,接下來走我們熟悉的mountComponent
const processComponent = ( n1: VNode | null,n2: VNode,container: RendererElement,anchor: RendererNode | null,parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null,isSVG: boolean,optimized: boolean ) => { if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( n2,optimized ) } else { // 初始化走掛載流程 mountComponent( n2,optimized ) } } else { updateComponent(n1,optimized) } }
進入mountComponent方法,其中比較重要的instance為建立元件例項,setupComponent為安裝元件準備的;做選項處理用的;setupRenderEffec用於建立渲染函式副作用,在依賴收集的時候使用。
const mountComponent: MountComponentFn = ( initialVNode,optimized ) => { // 建立元件例項 const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode,parentSuspense )) if (__DEV__ && instance.type.__hmrId) { registerHMR(instance) } if (__DEV__) { pushWarningContext(initialVNode) startMeasure(instance,`mount`) } // inject renderer internals for keepAlive if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } // resolve props and slots for setup context if (__DEV__) { startMeasure(instance,`init`) } // 安裝元件:選項處理 setupComponent(instance) if (__DEV__) { endMeasure(instance,`init`) } www.cppcns.com// setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { parentSuspense && parentSuspense.registerDep(instance,setupRenderEffect) // Give it a placeholder if this is not hydration // TODO handle self-defined fallback if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null,placeholder,container!,anchor) } return } // 建立渲染函式副作用:依賴收集 setupRenderEffect( instance,initialVNode,optimized ) if (__DEV__) { popWarningContext() endMeasure(instance,`mount`) } }
進入到setupComponent函式裡面,觀看setupComponent函式的內部邏輯,在這裡面有屬性插槽的初始化; 在這裡面可以看到setupStatefulComponent方法,它就是用來處理響應式的。
export function setupComponent( instance: ComponentInternalInstance,isSSR = false ) { isInSSRComponentSetup = isSSR const { props,children,shapeFlag } = instance.vnode const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT initProps(instance,props,isStateful,isSSR) initSlots(instance,children) const setupResult = isStateful ? setupStatefulComponent(instance,isSSR) : undefined isInSSRComponentSetup = false return setupResult }
進入方法setupStatefulComponent,其中const Component = instance.type as ComponentOptions用於元件配置。其中instance.proxy = new Proxy(instance.ctx,PublicInstanceProxyHandlers)用於代理,data,$等都是在這裡處理的。
function setupStatefulComponent( instance: ComponentInternalInstance,isSSR: boolean ) { // 元件配置 const Component = instance.type as ComponentOptions if (__DEV__) { if (Component.name) { validateComponentName(Component.name,instance.appContext.config) } if (Component.components) { const names = Object.keys(Component.components) for (let i = 0; i < names.length; i++) { validateComponentName(names[i],instance.appContext.config) } } if (Component.directives) { const names = Object.keys(Component.directives) for (let i = 0; i < names.length; i++) { validateDirectiveName(names[i]) } } } // 0. create render proxy property access cache instance.accessCache = {} // 1. create public instance / render proxy // also mark it raw so it's never observed instance.proxy = new Proxy(instance.ctx,PublicInstanceProxyHandlers) if (__DEV__) { exposePropsOnRenderContext(instance) } // 2. call setup() const { setup } = Component if (setup) { const setupCoFHwKnbPwHntext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null) currentInstance = instance pauseTracking() const setupResult = callWithErrorHandling( setup,instance,ErrorCodes.SETUP_FUNCTION,[__DEV__ ? shallowReadonly(instance.props) : instance.props,setupContext] ) resetTracking() currentInstance = null if (isPromise(setupResult)) { if (isSSR) { // return the promise so server-renderer can wait on it return setupResult.then((resolvedResult: unknown) => { handleSetupResult(instance,resolvedResult,isSSR) }) } else if (__FEATURE_SUSPENSE__) { // async setup returned Promise. // bail here and wait for re-entry. instance.asyncDep = setupResult } else if (__DEV__) { warn( `setup() returned a Promise,but the version of Vue you are using ` + `does not support it yet.` ) } } else { handleSetupResult(instance,setupResult,isSSR) } } else { // 處理選項等事務 finishComponentSetup(instance,isSSR) } }
由於咱們的案例裡面沒有setup,所以會執行 finishComponentSetup(instance,isSSR)來處理選項式api相關的東西。進入該函式裡面檢視程式碼邏輯,會看到如下的程式碼,該部分的程式碼用於處理選項式API相關的東西,用於支援vue2.x的版本。
// support for 2.x options // 支援選項API if (__FEATURE_OPTIONS_API__) { currentInstance = instance applyOptions(instance,Component) currentInstance = null }
進入applyOptions方法裡面;往下翻,會看到這幾行註釋,這幾行註釋清晰地解釋了vue2.x裡面各個選項的優先順序,其中包括props、inject、methods、data等。
// options initialization order (to be consistent with Vue 2): // - props (already done outside of this function) // - inject // - methods // - data (deferred since it relies on `this` access) // - computed // - watch (deferred since it relies on `this` access)
繼續往下看,會看到這幾行程式碼,我們這裡面用的不是混入的形式,所以這行這一系列的程式碼,,其中涉及到資料相應式的程式碼都在resolveData方法裡面。
if (!asMixin) { if (deferredData.length) { deferredData.forEach(dataFn => resolveData(instance,dataFn,publicThis)) } if (dataOptions) { // 資料響應式 resolveData(instance,dataOptions,publicThis) }
進入resolveData裡面,可以看到const data = dataFn.call(publicThis,publicThis),這一行程式碼用於獲取資料物件。instance.data = reactive(data)這一行程式碼用於對data做響應式處理。其中核心的就是reactive,該方法用於做響應式的處FHwKnbPwH理。選項式api也好,setup也罷,最終走的都是reactive方法,用該方法來做響應式處理。
function resolveData( instance: ComponentInternalInstance,dataFn: DataFn,publicThis: ComponentPublicInstance ) { if (__DEV__ && !isFunction(dataFn)) { warn( `The data option must be a function. ` + `Plain object usage is no longer supported.` ) } // 獲取資料物件 const data = dataFn.call(publicThis,publicThis) if (__DEV__ && isPromise(data)) { warn( `data() returned a Promise - note data() cannot be async; If you ` + `intend to perform data fetching before component renders,use ` + `async setup() + <Suspense>.` ) } if (!isObject(data)) { __DEV__ && warn(`data() should return an object.`) } else if (instance.data === EMPTY_OBJ) { // 對data 做響應式處理 instance.data = reactive(data) } else { // existing data: this is a mixin or extends. extend(instance.data,data) } }
進入到reactive裡面,觀察其中的程式碼邏輯;這裡面的createReactiveObject用於對資料進行處理。其中target是最終要轉化的東西。
return createReactiveObject( target,false,mutableHandlers,mutableCollectionHandlers )
其中mutableHandlers裡面有一些get、set、deleteProperty等方法。mutableCollectionHandlers會建立依賴收集之類的操作。
vue2.x資料響應式和3.x響應式對比
到這裡,我們先回顧一下vue2.x是如何處理響應式的。是用defineReactive來攔截每個key,從而可以檢測資料變化,這一套處理方式是有問題的,當資料是一層巢狀一層的時候,就會進行層層遞迴,從而消耗大量的記憶體。由此來看,這一套處理方式算不上友好。Vue3裡面也是用用defineReactive來攔截每個key,與此不同的是,在vue3.x裡面的defineReactive裡面用proxy做了一層代理,相當於加了一層關卡。Vue2.x裡面需要進行遞迴物件所有key,速度慢。陣列響應式需要額外實現。而且新增或刪除屬www.cppcns.com性無法監聽,需要使用特殊api。而現在,直接一個new proxy直接把所有的問題都給解決了。與此同時,之前的那一套方法不知Map,Set、Class等資料結構。
大致流程圖
然後我們梳理一下到響應式的過程中順序
實現依賴收集
在實現響應式的過程中,依賴收集是和其緊密相連的東西,其中setupRenderEffect函式中使用effect函式做依賴收集。進入setupRenderEffect函式內部,在上面的程式碼中有這個函式,這裡不一一贅述,我們繼續往下看。進入到該函式內部,會看到如下程式碼。effect可以建立一個依賴關係:傳入effect的回撥函式和響應式資料之間;effect就相當於的vue2裡面的dep,然後vue3裡面沒有watcher了。
instance.update = effect(function componentEffect() { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el,props } = initialVNode const { bm,m,parent } = instance
繼續往下看,會看到如下程式碼,subTree是當前元件vnode,其中renderComponentRoot方法用於實現渲染元件的根。
const subTree = (instance.subTree = renderComponentRoot(instance))
到這裡,vue3.0的響應式部分就算要告一段落了
程式碼倉庫
手寫vue3.0簡版的實現資料響應式,已上傳到個人倉庫,有興趣的可以看看。喜歡的話可以來個關注,哈哈哈。關注我,你在道路上就多了一個朋友。https://gitee.com/zhang-shichuang/xiangyingshi/tree/master/
結尾
vue的資料響應式在面試的過程中經常會被問到,究其原理,還是要去看原始碼。在讀原始碼的時候難免也會有枯燥乏味的時候,但是堅持下來就是勝利,後期還會分享vue的編譯過程,以及react相關的原始碼知識。
到此這篇關於vue3.x原始碼剖析之資料響應式的文章就介紹到這了,更多相關vue3.x資料響應式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!