vue3.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, 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為創建組件實例,setupComponent為安裝組件準備的;做選項處理用的;setupRenderEffec用於建立渲染函數副作用,在依賴收集的時候使用。

const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 創建組件實例
    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,所以會執行 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,該方法用於做響應式的處理。選項式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,速度慢。數組響應式需要額外實現。而且新增或刪除屬性無法監聽,需要使用特殊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數據響應式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: