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页面里面放上几个事件
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>
</template>
methods: {
submit () {
this.$emit('refreshSelectClose',chooseData ) //关闭后反馈的事件, } },
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({
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