一、問題
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