vue i18n _ctx.$t is not a function

 

一、問題

runtime-core.esm-bundler.js:38 [Vue warn]: Property "$t" was accessed during render but is not defined on instance. 

runtime-core.esm-bundler.js:38 [Vue warn]: Unhandled error during execution of render function 

runtime-core.esm-bundler.js:38 [Vue warn]: Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core 

Uncaught (in promise) TypeError: _ctx.$t is not a function
at Select.vue:51:95
at renderFnWithContext (runtime-core.esm-bundler.js:852:21)
at renderSlot (runtime-core.esm-bundler.js:6627:55)
at index.vue:18:20
at renderFnWithContext (runtime-core.esm-bundler.js:852:21)
at renderSlot (runtime-core.esm-bundler.js:6627:55)
at Proxy._sfc_render80 (table.vue:31:9)
at renderComponentRoot (runtime-core.esm-bundler.js:895:44)
at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5127:34)
at ReactiveEffect.run (reactivity.esm-bundler.js:185:25)

 

想對應的版本

    "dependencies": {
        "@vueuse/core": "^4.10.0",
        "axios": "^0.21.1",
        "element-plus": "^2.2.10",
        "js-cookie": "^2.2.1",
        "lodash": "^4.17.20",
        "normalize.css": "^8.0.1",
        "nprogress": "^0.2.0",
        "throttle-debounce": "^3.0.1",
        "vue": "^3.2.8",
        "vue-i18n": "^9.1.6",
        "vue-router": "4",
        "vuex": "^4.0.0"
    },
    "devDependencies": {
        "@vitejs/plugin-vue": "^1.6.0",
        "@vue/compiler-sfc": "^3.2.6",
        "sass": "^1.32.12",
        "vite": "^2.9.15"
    },
    "resolutions": {
        "esbuild": "0.14.34"
    }

 

也就是  vue-i18n 版本是9.1.6

我出現錯誤的場景

list.vue 嵌套 add.vue,add.vue 嵌套queryselect.vue。

列表頁面dialog彈出add.vue 子頁面,add.vue有部分需要到queryselect.vue進行勾選。 然後再queryselect.vue勾選完成之後 或者是關閉select.vue的時候報錯。然後這個報錯又不影響功能的執行

 

 

 二、分析

1、在想爲什麼控制檯裏面在關閉的時候會發生警告,進行是有重新渲染

 

後面查詢了資料,發現因爲我用的v-if 。這個會卸載頁面,然後重新生成渲染,然後渲染的時候找不到$t。 這個控制檯warn就是證據

v-if切換有一個局部編譯/卸載的過程,切換過程中合適地銷燬和重建內部的事件監聽和子組件;v-if初始值爲false,就不會編譯了。
v-show其實就是在控制css;v-show都會編譯,初始值爲false,只是將display設爲none,但它也編譯了。 

需要詳細瞭解v-if和v-show的同學可以看   Vue內置指令:v-if和v-show的區別

現在就需要指定v-if 從true到false的時候執行了哪些聲明週期。方便去源代碼裏面看。

2、進行查看源碼進行分析

然後在vue-i18n.cjs.js源代碼裏面搜索關鍵詞 createI18n(  

裏面可以看到一些樣例

備註裏面寫着

 * @remarks
 * If you use Legacy API mode, you need toto specify {@link VueI18nOptions} and `legacy: true` option.
 *
 * If you use composition API mode, you need to specify {@link ComposerOptions}.

翻譯成中文就是

如果你使用Legacy api模式(歷史模式,就是兼容老版本),你需要指定{ 鏈接 VueI18nOptions} 和  legacy =true 選項

如果 composition API  模式(組成模式), 你需要指定 {鏈接ComposerOptions}。

找到 function createI18n(options = {}) {  

function createI18n(options = {}) {
    // prettier-ignore
    const __legacyMode = shared.isBoolean(options.legacy)
        ? options.legacy
        : true;
    const __globalInjection = !!options.globalInjection;
    const __instances = new Map();
    // prettier-ignore
    const __global = __legacyMode
        ? createVueI18n(options)
        : createComposer(options);
    const symbol = shared.makeSymbol('vue-i18n' );
    const i18n = {
        // mode
        get mode() {
            // prettier-ignore
            return __legacyMode
                    ? 'legacy'
                    : 'composition'
                ;
        },
        // install plugin
        async install(app, ...options) {
            // setup global provider
            app.__VUE_I18N_SYMBOL__ = symbol;
            app.provide(app.__VUE_I18N_SYMBOL__, i18n);
            // global method and properties injection for Composition API
            if (!__legacyMode && __globalInjection) {
                injectGlobalFields(app, i18n.global);
            }
            // install built-in components and directive
            {
                apply(app, i18n, ...options);
            }
            // setup mixin for Legacy API
            if (__legacyMode) {
                app.mixin(defineMixin(__global, __global.__composer, i18n));
            }
        },
        // global accessor
        get global() {
            return __global;
        },
        // @internal
        __instances,
        // @internal
        __getInstance(component) {
            return __instances.get(component) || null;
        },
        // @internal
        __setInstance(component, instance) {
            __instances.set(component, instance);
        },
        // @internal
        __deleteInstance(component) {
            __instances.delete(component);
        }
    };
    return i18n;
}

 

 

在兼容模式執行的方法裏面點擊查看 defineMixin 方法,裏面的內容如下

// supports compatibility for legacy vue-i18n APIs
function defineMixin(vuei18n, composer, i18n) {
    return {
        beforeCreate() {
            const instance = vue.getCurrentInstance();
            /* istanbul ignore if */
            if (!instance) {
                throw createI18nError(22 /* UNEXPECTED_ERROR */);
            }
            const options = this.$options;
            if (options.i18n) {
                const optionsI18n = options.i18n;
                if (options.__i18n) {
                    optionsI18n.__i18n = options.__i18n;
                }
                optionsI18n.__root = composer;
                if (this === this.$root) {
                    this.$i18n = mergeToRoot(vuei18n, optionsI18n);
                }
                else {
                    optionsI18n.__injectWithOption = true;
                    this.$i18n = createVueI18n(optionsI18n);
                }
            }
            else if (options.__i18n) {
                if (this === this.$root) {
                    this.$i18n = mergeToRoot(vuei18n, options);
                }
                else {
                    this.$i18n = createVueI18n({
                        __i18n: options.__i18n,
                        __injectWithOption: true,
                        __root: composer
                    });
                }
            }
            else {
                // set global
                this.$i18n = vuei18n;
            }
            vuei18n.__onComponentInstanceCreated(this.$i18n);
            i18n.__setInstance(instance, this.$i18n);
            // defines vue-i18n legacy APIs
            this.$t = (...args) => this.$i18n.t(...args);
            this.$rt = (...args) => this.$i18n.rt(...args);
            this.$tc = (...args) => this.$i18n.tc(...args);
            this.$te = (key, locale) => this.$i18n.te(key, locale);
            this.$d = (...args) => this.$i18n.d(...args);
            this.$n = (...args) => this.$i18n.n(...args);
            this.$tm = (key) => this.$i18n.tm(key);
        },
        mounted() {
        },
        beforeUnmount() {
            const instance = vue.getCurrentInstance();
            /* istanbul ignore if */
            if (!instance) {
                throw createI18nError(22 /* UNEXPECTED_ERROR */);
            }
            delete this.$t;
            delete this.$rt;
            delete this.$tc;
            delete this.$te;
            delete this.$d;
            delete this.$n;
            delete this.$tm;
            i18n.__deleteInstance(instance);
            delete this.$i18n;
        }
    };
}

居然會幾個事件,如 beforeCreate 、mounted、beforeUnmount

 而beforeCreate  就是把一些常用的加載進去,比如$t、$rt、$tc、$t、$d、$n、$tm等

而 beforeUnmount 就是不用delete卸載這些$t、$rt、$tc、$t、$d、$n、$tm 快捷方法的

 

這也就是在頁面執行關閉v-if ,然後需要重新渲染找不到的原因吧??

爲了驗證,在querySelect.vue頁面裏面放上幾個事件


 
export default defineComponent({
 name: 'querySelect',
 props: {
    fileName:{
      type:String,
      default:()=>{
        return 'querySelect'
      }
    },
  },
  beforeCreate() {
    console.log(`${this.fileName}--beforeCreate鉤子函數`)
    console.log(this.$t) //undefined
  },
  created() {
    console.log(`${this.fileName}--觸發了 created 鉤子函數`)
  },
  beforeMount() {
    console.log(`${this.fileName}--beforeMount鉤子函數`)
    console.log(this.$t)
  },
  mounted() {
    console.log(`${this.fileName}--觸發了 mounted 鉤子函數`)
  },
  beforeUpdate() {
    console.log(`${this.fileName}--觸發了 beforeUpdate 鉤子函數`)
  },
  updated() {
    console.log(`${this.fileName}--觸發了 updated 鉤子函數`)
  },
  beforeDestroy() {
    console.log(`${this.fileName}--觸發了 beforeDestroy 鉤子函數`)
  },
  destroyed() {
    console.log(`${this.fileName}--觸發了 destotyed 鉤子函數`)
  },
  beforeUnmount(){
     console.log(`${this.fileName}--觸發了 beforeUnmount 鉤子函數`)
     console.log(this.$t)
  },
  unmounted(){
      console.log(`${this.fileName}--觸發了 unmounted 鉤子函數`)
  },
})

 

add.vue也加上這些事件進行監聽

export default defineComponent({
 name: 'add',
 props: {
    fileName:{
      type:String,
      default:()=>{
        return 'add'
      }
    },
  },
  beforeCreate() {
    console.log(`${this.fileName}--beforeCreate鉤子函數`)
    console.log(this.$t) //undefined
  },
  created() {
    console.log(`${this.fileName}--觸發了 created 鉤子函數`)
  },
  beforeMount() {
    console.log(`${this.fileName}--beforeMount鉤子函數`)
    console.log(this.$t)
  },
  mounted() {
    console.log(`${this.fileName}--觸發了 mounted 鉤子函數`)
  },
  beforeUpdate() {
    console.log(`${this.fileName}--觸發了 beforeUpdate 鉤子函數`)
  },
  updated() {
    console.log(`${this.fileName}--觸發了 updated 鉤子函數`)
  },
  beforeDestroy() {
    console.log(`${this.fileName}--觸發了 beforeDestroy 鉤子函數`)
  },
  destroyed() {
    console.log(`${this.fileName}--觸發了 destotyed 鉤子函數`)
  },
  beforeUnmount(){
     console.log(`${this.fileName}--觸發了 beforeUnmount 鉤子函數`)
     console.log(this.$t)
  },
  unmounted(){
      console.log(`${this.fileName}--觸發了 unmounted 鉤子函數`)
  },
})

 

然後初次打開add.vue

初次打開querySelect.vue

 


querySelect--beforeCreate鉤子函數
(...args) => this.$i18n.t(...args)
querySelect--觸發了 created 鉤子函數
querySelect--beforeMount鉤子函數
(...args) => this.$i18n.t(...args)
querySelect--觸發了 mounted 鉤子函數

執行了beforeCreate、created、mounted

點擊關閉queryselect.vue

querySelect--觸發了 beforeUnmount 鉤子函數
 undefined
querySelect--觸發了 unmounted 鉤子函數

執行了beforeUnmount 、unmounted ,然後接着就報錯了,肯定 beforeUnmount  之後執行了什麼操作?? 

就要看最開始控制檯給的報錯了,根據這個來了解vue的原因

renderFnWithContext
  withCtx: 將傳遞的fn包裹成renderFnWithContext在返回。
  在執行fn的時候包裹一層currentRenderInstance,確保當前的實例不出錯。
renderSlot 重新渲染父組件的 v-slot
renderComponentRoot 調用render方法獲取基於當前實例的VNode Tree,並將VNode Tree進行patch到容器中。
componentUpdateFn 開啓組件重新渲染,只有第一次的話執行掛載,後續都是更新邏輯
renderFnWithContext有以下三個屬性:
  _n:如果有這個屬性代表當前函數已經被包裹過了,不應該被重複包裹。
  _c: 標識的是當前的插槽是通過編譯得到的,還是用戶自己寫的。
 _d: 表示執行fn的時候是否需要禁止塊跟蹤,true代表禁止塊跟蹤,false代表允許塊跟蹤。

 

/**
 * Wrap a slot function to memoize current rendering instance
 * @private compiler helper
 */
function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot // false only
) {
    if (!ctx)
        return fn;
    // already normalized
    if (fn._n) {
        return fn;
    }
    const renderFnWithContext = (...args) => {
        // If a user calls a compiled slot inside a template expression (#1745), it
        // can mess up block tracking, so by default we disable block tracking and
        // force bail out when invoking a compiled slot (indicated by the ._d flag).
        // This isn't necessary if rendering a compiled `<slot>`, so we flip the
        // ._d flag off when invoking the wrapped fn inside `renderSlot`.
        if (renderFnWithContext._d) {
            setBlockTracking(-1);
        }
        const prevInstance = setCurrentRenderingInstance(ctx);
        const res = fn(...args);
        setCurrentRenderingInstance(prevInstance);
        if (renderFnWithContext._d) {
            setBlockTracking(1);
        }
        if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
            devtoolsComponentUpdated(ctx);
        }
        return res;
    };
    // mark normalized to avoid duplicated wrapping
    renderFnWithContext._n = true;
    // mark this as compiled by default
    // this is used in vnode.ts -> normalizeChildren() to set the slot
    // rendering flag.
    renderFnWithContext._c = true;
    // disable block tracking by default
    renderFnWithContext._d = true;
    return renderFnWithContext;
}

這裏主要是執行代碼塊跟蹤

看下網絡被人給我翻譯

function withCtx(
  fn,
  ctx = getCurrentRenderingInstance(),
  isNonScopedSlot
) {
  if (!ctx) return fn;
  if (fn._n) {
    return fn;
  }
  //設置currentRenderingInstance,通過閉包確保調用fn的時候
  //currentRenderingInstance實例爲當前實例
  /**
   * 如果用戶調用模板表達式內的插槽
   *  <Button>
   *    <template>
   *      <slot></slot>
   *    </template>
   *  </Button>
   * 可能會擾亂塊跟蹤,因此默認情況下,禁止塊跟蹤,當
   * 調用已經編譯的插槽時強制跳出(由.d標誌指示)。
   * 如果渲染已編譯的slot則無需執行此操作、因此
   * 我們在renderSlot中調用renderFnWithContext
   * 時,.d設置爲false
   */
  const renderFnWithContext = (...args) => {
    //禁止塊追蹤,將isBlockTreeEnabled設置爲0將會停止追蹤
    if (renderFnWithContext._d) {
      setBlockTracking(-1);
    }
    const prevInstance = setCurrentRenderingInstance(ctx);
    const res = fn(...args);
    setCurrentRenderingInstance(prevInstance);
    //開啓塊追蹤
    if (renderFnWithContext._d) {
      setBlockTracking(1);
    }
    return res;
  };
  //如果已經是renderFnWithContext則不需要在包裝了
  renderFnWithContext._n = true; //_n表示已經經過renderFnWithContext包裝
  renderFnWithContext._c = true; //表示經過compiler編譯得到
  //true代表禁止塊追蹤,false代表開啓塊追蹤
  renderFnWithContext._d = true;
  return renderFnWithContext;
}

根據上面大概可以看出,在關閉querySelecy.vue的時候,v-if進行了remove querySelect.vue 移除之後,然後把querySelect.vue 放到tree中,這個從 const setupRenderEffect 的方法中可以看出

const nextTree = renderComponentRoot(instance);
if ((process.env.NODE_ENV !== 'production')) {
    endMeasure(instance, `render`);
}
const prevTree = instance.subTree;
instance.subTree = nextTree;
if ((process.env.NODE_ENV !== 'production')) {
    startMeasure(instance, `patch`);
}
patch(prevTree, nextTree, 
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el), 
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree), instance, parentSuspense, isSVG);
if ((process.env.NODE_ENV !== 'production')) {
    endMeasure(instance, `patch`);
}
next.el = nextTree.el;
if (originNext === null) {
    // self-triggered update. In case of HOC, update parent component
    // vnode el. HOC is indicated by parent instance's subTree pointing
    // to child component's vnode
    updateHOCHostEl(instance, nextTree.el);
}

而放入之前需要進行編譯,也就是renderComponentRoot中的

 

if (vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */) {
            // withProxy is a proxy with a different `has` trap only for
            // runtime-compiled render functions using `with` block.
            const proxyToUse = withProxy || proxy;
            result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx));
            fallthroughAttrs = attrs;
}

這個英文註釋的意思是這個是一個代理,之後在編譯的時候纔行執行這個塊,但是報錯就是在這裏,也就是remove之後需要重新編譯。
而在編譯的時候因爲前面的querySelect.vue的beforeUnmount 方法中做了delete this.$t。所以找不到就編譯報錯。

而在function renderSlot中的

const validSlotContent = slot && ensureValidVNode(slot(props));

ensureValidVNode  就是校驗是否是有效的VNode節點。

根據上面可以得出,是在預編譯的時候報錯。

所以就不讓他remove就好了。也就是配置全局依賴!!!

三、解決辦法

在 createI18n 方法的時候加上

   globalInjection:true,   //進行全局依賴
   legacy:false,        //過去式,爲了兼容老版本,不寫默認是true

如下圖所示

 

以下加載多語言

// 提示信息僅在開發環境生效
import { createI18n } from 'vue-i18n/index'
import store from '@/store'

const files= import.meta.globEager('./modules/*.js')

let messages = {}
Object.keys(files).forEach((c) => {
  const module = files[c].default
  const moduleName = c.replace(/^\.\/(.*)\/(.*)\.\w+$/, '$2')
  messages[moduleName] = module
})

 
//const lang = store.state.app.lang  || navigator.userLanguage || navigator.language // 初次進入,採用瀏覽器當前設置的語言,默認採用中文
const lang =   navigator.userLanguage || navigator.language
const locale = lang.indexOf('en') !== -1 ? 'en' : 'zh-cn'

const i18n = createI18n({
  __VUE_I18N_LEGACY_API__: false,
  __VUE_I18N_FULL_INSTALL__: false,
  locale: locale,
  fallbackLocale: 'zh-cn',
   globalInjection:true,   //進行全局依賴
   legacy:false,        //過去式,爲了兼容老版本,不寫默認是true
  messages
})
document.querySelector('html').setAttribute('lang', locale)

export default i18n

 

 

而子頁面往父頁面傳值的方法

vue2的方法,在methods裏面,這這裏面的refreshSelectClose是父頁面的事件,一下是queryselect.vue代碼

<template>
          <el-button type="primary" @click="submit">提交</el-button>
</template>

<script>
import { defineComponent, ref, watch } from 'vue'
 
export default defineComponent({
 methods: {
    submit () {
    
      const chooseData = ref([])
this.$emit('refreshSelectClose',chooseData )  //關閉後反饋的事件,
        }
    },   
})
</script>

 

add.vue頁面,嵌套queryselect.vue子頁面, refreshSelectClose是定義的時間,而selectClose是真實執行的方法

<template>
 <QuerySelect  v-if="layer.show" @refreshSelectClose="selectClose" /> 

</template>


<script>
import {
  defineComponent,
  ref,
  reactive,
} from 'vue'
import QuerySelect from './querySelect.vue' 

export default defineComponent({
  components: {
     QuerySelect,
  },
    methods: {
      selectClose (data) {
         console.log("test",data)
      }
    }
})

 

 

而vue3的setup裏面不能用this,需要在setup定義參數

queryselect.vue

<template>
          <el-button type="primary" @click="submit">提交</el-button>
</template>
<script>
import { defineComponent, ref, watch } from 'vue'

export default defineComponent({
  name: 'uaisKeyQuery',
  props: {
    selectlayer: {
      type: Object,
      default: () => {
        return {
          show: false,
          title: '',
          showButton: true,
          operationType: 'view',
          zindex:1001,
        }
      }
    }
  },
 setup(props,context) {
    const chooseData = ref([])
    const submit = () => {  
        context.emit('refreshSelectClose',chooseData )  //關閉後反饋的事件,
        }
    }
   return {
      chooseData,
      submit, 
  }
})
</script>

setup參數裏面context就是vue2裏面的this,而在setup裏面就沒有this這個概念了,而這裏的參數props就是defineComponent 裏面的props這個key,比如頁面需要初始化默認值的話就在props這裏面加。

以下是add.vue

<template>
 <QuerySelect  v-if="layer.show" @refreshSelectClose="selectClose" /> 

</template>


<script>
import {
  defineComponent,
  ref,
  reactive,
} from 'vue'
import QuerySelect from './querySelect.vue' 

export default defineComponent({
  components: {
     QuerySelect,
  },
  setup(props,context) {
     const selectClose= (data)=> {
         console.log("test",data)
      }
      return {
         return selectClose,
      }
    }
})

 

四參考

Vue3組件掛載初始化 http://www.qb5200.com/article/551284.html
Vue.js面試學習知識點記錄 https://www.cnblogs.com/hejiyuan/p/16364711.html
Vue 3.0組件的更新流程和diff算法詳解 https://www.jianshu.com/p/99b314b9faab

深入淺出Vue.js——虛擬DOM之VNode https://www.jianshu.com/p/90699a4b6ed9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章