1. 程式人生 > 其它 >一次在 Vue3 中使用render函式的事故,從原始碼中找到了答案

一次在 Vue3 中使用render函式的事故,從原始碼中找到了答案

技術標籤:vuevue3vue3 rendervue3 setupsetup

一次在 Vue3 中使用render函式的事故,從原始碼中找到了答案

第一遍看 vue3 的原始碼,有不對的地方,希望各位大佬能指出,感謝!!!

Vue3 版本 3.0.4

Vue3.0 建立元件時,在 render 函式中使用 setup 函式中返回的 ref 資料,無法動態繫結

示例:

Vue.createApp({
    setup() {
        const elRef = Vue.ref(null)
        Vue.onMounted(() => {
        	elRef.
value.innerHTML = "123" }) return { elRef } }, render() { return Vue.h("div", { ref: this.elRef }) } }).mount("#app")

瀏覽器報錯
image-20201209111428308

查詢問題

1、找到 render 函式的執行

原始碼位置:https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentRenderUtils.ts

77行

刪減後的程式碼:

export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const {
    proxy,
    withProxy,
    props,
    render,
    renderCache,
    data,
    setupState,
    ctx
  } = instance

  let result
  try {
    let fallthroughAttrs
    if (vnode.shapeFlag &
ShapeFlags.STATEFUL_COMPONENT) { const proxyToUse = withProxy || proxy result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) ) fallthroughAttrs = attrs } else { // 函式元件 } return result }

render 函式在執行的時候,將 this 改變成了 proxy,(在生產環境是通過在 with 塊中執行改變)

2、找到 proxy 的定義

原始碼位置 https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts 565行

刪減後的程式碼:

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions // type 就是元件的options
  instance.accessCache = Object.create(null)
  // proxy 在這裡定義 
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  // setup 方法在這裡執行
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    pauseTracking()
    const setupResult = callWithErrorHandling( // 這個方法內部只是使用 try catch 捕捉 setup 函式時的異常
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    handleSetupResult(instance, setupResult, isSSR) // 這裡處理 setup 的返回值
  }
}

PublicInstanceProxyHandlersget 方法的處理

原始碼位置:https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentPublicInstance.ts 143行

刪減後的程式碼:

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const {
      ctx,
      setupState,
      data,
      props,
      accessCache,
      type,
      appContext
    } = instance
    let normalizedProps
    if (key[0] !== '$') {
      // 如果快取裡有 key 對應的資料型別,就不需要在判斷 key 的來源
      const n = accessCache![key]
      if (n !== undefined) {
        switch (n) {
          case AccessTypes.SETUP:
            return setupState[key]
          case AccessTypes.DATA:
            return data[key]
          case AccessTypes.CONTEXT:
            return ctx[key]
          case AccessTypes.PROPS:
            return props![key]
        }
      } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        // 快取中沒有 key 的資料來源,把 key 的資料來源存入到快取中,返回資料
        accessCache![key] = AccessTypes.SETUP
        return setupState[key]
      }
    }
  }
}

這裡返回的 setupState 的值,setupState 也是一個 proxy 的型別,在 setup 執行完成後建立

原始碼位置:https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts 610行

刪減後的程式碼:

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // setup 中返回一個函式,這個函式作為render函式使用
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    // 這裡定義 setupState
    instance.setupState = proxyRefs(setupResult) // proxyRefs 函式返回一個 proxy 物件
  }
  finishComponentSetup(instance, isSSR)
}

proxyRefs 函式 返回一個 proxy 物件,這個 proxy 物件的 get 方法,會返回 ref 資料的 value

原始碼位置: https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts 105行

刪減後的程式碼:

export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
  // 會返回 ref 資料的 value 屬性,而不是整個物件
  return isRef(ref) ? (ref.value as any) : ref
}

const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
}

export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

到這裡,就可以知道 為什麼在 render 函式中使用 this. 的方法獲取到的不是 ref 物件,而是原始值了

原因總結:

render 方法中使用 this. 的方式獲取的是 setup 中返回的 ref 資料的 value 屬性,所以不能給元素做動態繫結

怎麼解決:

在上面提到的 setup 方法執行完成後,會執行 handleSetupResult 方法,處理 setup 函式返回的資料,再看一遍

刪減後的程式碼:

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // setup 中返回一個函式,這個函式作為render函式使用
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    // 這裡定義 setupState
    instance.setupState = proxyRefs(setupResult) // proxyRefs 函式返回一個 proxy 物件
  }
  finishComponentSetup(instance, isSSR)
}

setup 的函式返回值是一個函式的時候,這個函式會被作為 render 函式處理(這個返回值會覆蓋在 opitons 中的 render 函式)

所以我們之前的示例就可以改成下面這個樣子:

Vue.createApp({
    setup() {
        const elRef = Vue.ref(null)
        Vue.onMounted(() => {
        	elRef.value.innerHTML = "123"
        })
        return () =>  Vue.h("div", {
        	ref: elRef
        })
    }
}).mount("#app")

image-20201209105641740

頁面中就可以正確的顯示內容了

總結:

setup 中可以返回一個 物件 或者 函式,當返回 函式是,這個函式就是這個元件的 render 函式

當返回 物件的時候,這個物件的資料通過 this 獲取值的時候,只能獲取到 .value 的值,獲取不到 ref 物件