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

posted @ 2022-12-07 14:43  ☆♂安♀★  阅读(5039)  评论(4编辑  收藏  举报