vue3.x源碼剖析之數據響應式的深入講解
前言
如果錯過了秋楓和冬雪,那么春天的櫻花一定會盛開吧。最近一直在準備自己的考試,考完試了,終于可以繼續(xù)研究源碼和寫文章了,哈哈哈。學過vue的都知道,數據響應式在vue框架中極其重要,寫代碼也好,面試也罷,數據響應式都是核心的內容。在vue3的官網文檔中,作者說如果想讓數據更加響應式的話,可以把數據放在reactive里面,官方文檔在講述這里的時候一筆帶過,筆者剛開始也不是很理解。后來看了源碼才知道,在vue3里面響應式已經變成了一個單獨的模塊,而處理響應式的模塊就是reactive;
什么是數據響應式
從一開始使用 Vue 時,對于之前的 jq 開發(fā)而言,一個很大的區(qū)別就是基本不用手動操作 dom,data 中聲明的數據狀態(tài)改變后會自動重新渲染相關的 dom。
換句話說就是 Vue 自己知道哪個數據狀態(tài)發(fā)生了變化及哪里有用到這個數據需要隨之修改。
因此實現數據響應式有兩個重點問題:
- 如何知道數據發(fā)生了變化?
- 如何知道數據變化后哪里需要修改?
對于第一個問題,如何知道數據發(fā)生了變化,Vue3 之前使用了 ES5 的一個 API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是對需要偵測的數據進行 變化偵測 ,添加 getter 和 setter ,這樣就可以知道數據何時被讀取和修改。
第二個問題,如何知道數據變化后哪里需要修改,Vue 對于每個數據都收集了與之相關的 依賴 ,這里的依賴其實就是一個對象,保存有該數據的舊值及數據變化后需要執(zhí)行的函數。每個響應式的數據變化時會遍歷通知其對應的每個依賴,依賴收到通知后會判斷一下新舊值有沒有發(fā)生變化,如果變化則執(zhí)行回調函數響應數據變化(比如修改 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, 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, 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) {
// 初始化走這個
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)
}
}
接下來查看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,
container,
anchor,
isSVG,
optimized
)
} else {
// 初始化走掛載流程
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
進入mountComponent方法,其中比較重要的instance為創(chuàng)建組件實例,setupComponent為安裝組件準備的;做選項處理用的;setupRenderEffec用于建立渲染函數副作用,在依賴收集的時候使用。
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 創(chuàng)建組件實例
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`)
}
// 安裝組件:選項處理
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
}
// 建立渲染函數副作用:依賴收集
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
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 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 {
// 處理選項等事務
finishComponentSetup(instance, isSSR)
}
}
由于咱們的案例里面沒有setup,所以會執(zhí)行 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里面各個選項的優(yōu)先級,其中包括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ù)往下看,會看到這幾行代碼,我們這里面用的不是混入的形式,所以這行這一系列的代碼,,其中涉及到數據相應式的代碼都在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,該方法用于做響應式的處理。選項式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會創(chuàng)建依賴收集之類的操作。
vue2.x數據響應式和3.x響應式對比
到這里,我們先回顧一下vue2.x是如何處理響應式的。是用defineReactive來攔截每個key,從而可以檢測數據變化,這一套處理方式是有問題的,當數據是一層嵌套一層的時候,就會進行層層遞歸,從而消耗大量的內存。由此來看,這一套處理方式算不上友好。Vue3里面也是用用defineReactive來攔截每個key,與此不同的是,在vue3.x里面的defineReactive里面用proxy做了一層代理,相當于加了一層關卡。Vue2.x里面需要進行遞歸對象所有key,速度慢。數組響應式需要額外實現。而且新增或刪除屬性無法監(jiān)聽,需要使用特殊api。而現在,直接一個new proxy直接把所有的問題都給解決了。與此同時,之前的那一套方法不知Map,Set、Class等數據結構。
大致流程圖
然后我們梳理一下到響應式的過程中順序

實現依賴收集
在實現響應式的過程中,依賴收集是和其緊密相連的東西,其中setupRenderEffect函數中使用effect函數做依賴收集。進入setupRenderEffect函數內部,在上面的代碼中有這個函數,這里不一一贅述,我們繼續(xù)往下看。進入到該函數內部,會看到如下代碼。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
繼續(xù)往下看,會看到如下代碼,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數據響應式內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

