vue3.x源碼剖析之?dāng)?shù)據(jù)響應(yīng)式的深入講解
前言
如果錯(cuò)過(guò)了秋楓和冬雪,那么春天的櫻花一定會(huì)盛開(kāi)吧。最近一直在準(zhǔn)備自己的考試,考完試了,終于可以繼續(xù)研究源碼和寫文章了,哈哈哈。學(xué)過(guò)vue的都知道,數(shù)據(jù)響應(yīng)式在vue框架中極其重要,寫代碼也好,面試也罷,數(shù)據(jù)響應(yīng)式都是核心的內(nèi)容。在vue3的官網(wǎng)文檔中,作者說(shuō)如果想讓數(shù)據(jù)更加響應(yīng)式的話,可以把數(shù)據(jù)放在reactive里面,官方文檔在講述這里的時(shí)候一筆帶過(guò),筆者剛開(kāi)始也不是很理解。后來(lái)看了源碼才知道,在vue3里面響應(yīng)式已經(jīng)變成了一個(gè)單獨(dú)的模塊,而處理響應(yīng)式的模塊就是reactive;
什么是數(shù)據(jù)響應(yīng)式
從一開(kāi)始使用 Vue 時(shí),對(duì)于之前的 jq 開(kāi)發(fā)而言,一個(gè)很大的區(qū)別就是基本不用手動(dòng)操作 dom,data 中聲明的數(shù)據(jù)狀態(tài)改變后會(huì)自動(dòng)重新渲染相關(guān)的 dom。
換句話說(shuō)就是 Vue 自己知道哪個(gè)數(shù)據(jù)狀態(tài)發(fā)生了變化及哪里有用到這個(gè)數(shù)據(jù)需要隨之修改。
因此實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式有兩個(gè)重點(diǎn)問(wèn)題:
- 如何知道數(shù)據(jù)發(fā)生了變化?
- 如何知道數(shù)據(jù)變化后哪里需要修改?
對(duì)于第一個(gè)問(wèn)題,如何知道數(shù)據(jù)發(fā)生了變化,Vue3 之前使用了 ES5 的一個(gè) API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是對(duì)需要偵測(cè)的數(shù)據(jù)進(jìn)行 變化偵測(cè) ,添加 getter 和 setter ,這樣就可以知道數(shù)據(jù)何時(shí)被讀取和修改。
第二個(gè)問(wèn)題,如何知道數(shù)據(jù)變化后哪里需要修改,Vue 對(duì)于每個(gè)數(shù)據(jù)都收集了與之相關(guān)的 依賴 ,這里的依賴其實(shí)就是一個(gè)對(duì)象,保存有該數(shù)據(jù)的舊值及數(shù)據(jù)變化后需要執(zhí)行的函數(shù)。每個(gè)響應(yīng)式的數(shù)據(jù)變化時(shí)會(huì)遍歷通知其對(duì)應(yīng)的每個(gè)依賴,依賴收到通知后會(huì)判斷一下新舊值有沒(méi)有發(fā)生變化,如果變化則執(zhí)行回調(diào)函數(shù)響應(yīng)數(shù)據(jù)變化(比如修改 dom)。
數(shù)據(jù)響應(yīng)式的大體流程
在vue3.0的響應(yīng)式的部分,我們需要找的核心文件是vue3.0源碼的packages里面的runtime-core下面的src里面的;我們今天研究的這條線,就是沿著render這條線走下去的;
return { render, hydrate, createApp: createAppAPI(render, hydrate) }
在該文件下找到render函數(shù),如下所示;該函數(shù)的作用是渲染傳入vnode,到指定容器中;
const render: RootRenderFunction = (vnode, container) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { patch(container._vnode || null, vnode, container) } flushPostFlushCbs() container._vnode = vnode }
查看patch方法,初始化的話會(huì)走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, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { // 初始化走這個(gè) processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process( n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals ) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } // set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2) } }
接下來(lái)查看processComponent方法,接下來(lái)走我們熟悉的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, container, anchor, isSVG, optimized ) } else { // 初始化走掛載流程 mountComponent( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } } else { updateComponent(n1, n2, optimized) } }
進(jìn)入mountComponent方法,其中比較重要的instance為創(chuàng)建組件實(shí)例,setupComponent為安裝組件準(zhǔn)備的;做選項(xiàng)處理用的;setupRenderEffec用于建立渲染函數(shù)副作用,在依賴收集的時(shí)候使用。
const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // 創(chuàng)建組件實(shí)例 const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode, parentComponent, 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`) } // 安裝組件:選項(xiàng)處理 setupComponent(instance) if (__DEV__) { endMeasure(instance, `init`) } // 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 } // 建立渲染函數(shù)副作用:依賴收集 setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) if (__DEV__) { popWarningContext() endMeasure(instance, `mount`) } }
進(jìn)入到setupComponent函數(shù)里面,觀看setupComponent函數(shù)的內(nèi)部邏輯,在這里面有屬性插槽的初始化; 在這里面可以看到setupStatefulComponent方法,它就是用來(lái)處理響應(yīng)式的。
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 }
進(jìn)入方法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 setupContext = (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 { // 處理選項(xiàng)等事務(wù) finishComponentSetup(instance, isSSR) } }
由于咱們的案例里面沒(méi)有setup,所以會(huì)執(zhí)行 finishComponentSetup(instance, isSSR)來(lái)處理選項(xiàng)式api相關(guān)的東西。進(jìn)入該函數(shù)里面查看代碼邏輯,會(huì)看到如下的代碼,該部分的代碼用于處理選項(xiàng)式API相關(guān)的東西,用于支持vue2.x的版本。
// support for 2.x options // 支持選項(xiàng)API if (__FEATURE_OPTIONS_API__) { currentInstance = instance applyOptions(instance, Component) currentInstance = null }
進(jìn)入applyOptions方法里面;往下翻,會(huì)看到這幾行注釋,這幾行注釋清晰地解釋了vue2.x里面各個(gè)選項(xiàng)的優(yōu)先級(jí),其中包括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)
繼續(xù)往下看,會(huì)看到這幾行代碼,我們這里面用的不是混入的形式,所以這行這一系列的代碼,,其中涉及到數(shù)據(jù)相應(yīng)式的代碼都在resolveData方法里面。
if (!asMixin) { if (deferredData.length) { deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis)) } if (dataOptions) { // 數(shù)據(jù)響應(yīng)式 resolveData(instance, dataOptions, publicThis) }
進(jìn)入resolveData里面,可以看到const data = dataFn.call(publicThis, publicThis),這一行代碼用于獲取數(shù)據(jù)對(duì)象。instance.data = reactive(data)這一行代碼用于對(duì)data做響應(yīng)式處理。其中核心的就是reactive,該方法用于做響應(yīng)式的處理。選項(xiàng)式api也好,setup也罷,最終走的都是reactive方法,用該方法來(lái)做響應(yīng)式處理。
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.` ) } // 獲取數(shù)據(jù)對(duì)象 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) { // 對(duì)data 做響應(yīng)式處理 instance.data = reactive(data) } else { // existing data: this is a mixin or extends. extend(instance.data, data) } }
進(jìn)入到reactive里面,觀察其中的代碼邏輯;這里面的createReactiveObject用于對(duì)數(shù)據(jù)進(jìn)行處理。其中target是最終要轉(zhuǎn)化的東西。
return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers )
其中mutableHandlers里面有一些get、set、deleteProperty等方法。mutableCollectionHandlers會(huì)創(chuàng)建依賴收集之類的操作。
vue2.x數(shù)據(jù)響應(yīng)式和3.x響應(yīng)式對(duì)比
到這里,我們先回顧一下vue2.x是如何處理響應(yīng)式的。是用defineReactive來(lái)攔截每個(gè)key,從而可以檢測(cè)數(shù)據(jù)變化,這一套處理方式是有問(wèn)題的,當(dāng)數(shù)據(jù)是一層嵌套一層的時(shí)候,就會(huì)進(jìn)行層層遞歸,從而消耗大量的內(nèi)存。由此來(lái)看,這一套處理方式算不上友好。Vue3里面也是用用defineReactive來(lái)攔截每個(gè)key,與此不同的是,在vue3.x里面的defineReactive里面用proxy做了一層代理,相當(dāng)于加了一層關(guān)卡。Vue2.x里面需要進(jìn)行遞歸對(duì)象所有key,速度慢。數(shù)組響應(yīng)式需要額外實(shí)現(xiàn)。而且新增或刪除屬性無(wú)法監(jiān)聽(tīng),需要使用特殊api。而現(xiàn)在,直接一個(gè)new proxy直接把所有的問(wèn)題都給解決了。與此同時(shí),之前的那一套方法不知Map,Set、Class等數(shù)據(jù)結(jié)構(gòu)。
大致流程圖
然后我們梳理一下到響應(yīng)式的過(guò)程中順序
實(shí)現(xiàn)依賴收集
在實(shí)現(xiàn)響應(yīng)式的過(guò)程中,依賴收集是和其緊密相連的東西,其中setupRenderEffect函數(shù)中使用effect函數(shù)做依賴收集。進(jìn)入setupRenderEffect函數(shù)內(nèi)部,在上面的代碼中有這個(gè)函數(shù),這里不一一贅述,我們繼續(xù)往下看。進(jìn)入到該函數(shù)內(nèi)部,會(huì)看到如下代碼。effect可以建立一個(gè)依賴關(guān)系:傳入effect的回調(diào)函數(shù)和響應(yīng)式數(shù)據(jù)之間;effect就相當(dāng)于的vue2里面的dep,然后vue3里面沒(méi)有watcher了。
instance.update = effect(function componentEffect() { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode const { bm, m, parent } = instance
繼續(xù)往下看,會(huì)看到如下代碼,subTree是當(dāng)前組件vnode,其中renderComponentRoot方法用于實(shí)現(xiàn)渲染組件的根。
const subTree = (instance.subTree = renderComponentRoot(instance))
到這里,vue3.0的響應(yīng)式部分就算要告一段落了
代碼倉(cāng)庫(kù)
手寫vue3.0簡(jiǎn)版的實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式,已上傳到個(gè)人倉(cāng)庫(kù),有興趣的可以看看。喜歡的話可以來(lái)個(gè)關(guān)注,哈哈哈。關(guān)注我,你在編程道路上就多了一個(gè)朋友。https://gitee.com/zhang-shichuang/xiangyingshi/tree/master/
結(jié)尾
vue的數(shù)據(jù)響應(yīng)式在面試的過(guò)程中經(jīng)常會(huì)被問(wèn)到,究其原理,還是要去看源碼。在讀源碼的時(shí)候難免也會(huì)有枯燥乏味的時(shí)候,但是堅(jiān)持下來(lái)就是勝利,后期還會(huì)分享vue的編譯過(guò)程,以及react相關(guān)的源碼知識(shí)。
到此這篇關(guān)于vue3.x源碼剖析之?dāng)?shù)據(jù)響應(yīng)式的文章就介紹到這了,更多相關(guān)vue3.x數(shù)據(jù)響應(yīng)式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue3.0 響應(yīng)式系統(tǒng)源碼逐行分析講解
- vue3 響應(yīng)式對(duì)象如何實(shí)現(xiàn)方法的不同點(diǎn)
- 淺析vue3響應(yīng)式數(shù)據(jù)與watch屬性
- vue3中defineProps傳值使用ref響應(yīng)式失效詳解
- vue3.0響應(yīng)式函數(shù)原理詳細(xì)
- 詳解Vue3的響應(yīng)式原理解析
- setup+ref+reactive實(shí)現(xiàn)vue3響應(yīng)式功能
- 一文帶你了解vue3.0響應(yīng)式
- 手寫?Vue3?響應(yīng)式系統(tǒng)(核心就一個(gè)數(shù)據(jù)結(jié)構(gòu))
相關(guān)文章
詳解vue2.0 不同屏幕適配及px與rem轉(zhuǎn)換問(wèn)題
這篇文章主要介紹了詳解vue2.0 不同屏幕適配及px與rem轉(zhuǎn)換問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02詳解Vue用自定義指令完成一個(gè)下拉菜單(select組件)
本篇文章主要介紹了詳解Vue用自定義指令完成一個(gè)下拉菜單(select組件),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10Vue?實(shí)現(xiàn)新國(guó)標(biāo)紅綠燈效果實(shí)例詳解
這篇文章主要為大家介紹了Vue?實(shí)現(xiàn)新國(guó)標(biāo)紅綠燈效果實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08element-plus中如何實(shí)現(xiàn)按需導(dǎo)入與全局導(dǎo)入
本文主要介紹了element-plus中如何實(shí)現(xiàn)按需導(dǎo)入與全局導(dǎo)入,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Vue實(shí)現(xiàn)內(nèi)部組件輪播切換效果的示例代碼
這篇文章主要介紹了Vue實(shí)現(xiàn)內(nèi)部組件輪播切換效果的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04