日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 Vue 正文

詳解vue-class遷移vite的一次踩坑記錄_vue.js

作者:clench ? 更新時間: 2022-04-15 Vue

what happen

最進項目從 vue-cli 遷移到了 vite,因為是 vue2 的項目,使用了 vue-class-component 類組件做 ts 支持。
當然遷移過程并沒有那么一帆風順,瀏覽器控制臺報了一堆錯,大致意思是某某方法為 undefined,無法調用。打印了下當前 this,為 undefined 的方法都來自于 vuex-class 裝飾器下的方法。這就是一件很神奇的事,為什么只有 vuex-class 裝飾器下的方法才會為 undefined ?

探究

在網上搜了下并沒有類似的問題,只能自己在 node_modules 中一步一步打斷點看是哪里出了問題。最先覺得有問題的是 vuex-class ,調試了下 /node_modules/vuex-class/lib/bindings.js 下的代碼,發現 vuex-class 只是做了一層方法替換,通過 createDecorator 方法存到 vue-class-component 下的 __decorators__ 數組中。

import { createDecorator } from "vue-class-component";

function createBindingHelper(bindTo, mapFn) {
? function makeDecorator(map, namespace) {
? ? // 存入到 vue-class-component 的 __decorators__ 數組中
? ? return createDecorator(function (componentOptions, key) {
? ? ? if (!componentOptions[bindTo]) {
? ? ? ? componentOptions[bindTo] = {};
? ? ? }
? ? ? var mapObject = ((_a = {}), (_a[key] = map), _a);
? ? ? componentOptions[bindTo][key] =
? ? ? ? namespace !== undefined
? ? ? ? ? ? mapFn(namespace, mapObject)[key]
? ? ? ? ? : mapFn(mapObject)[key];
? ? ? var _a;
? ? });
? }
? function helper(a, b) {
? ? if (typeof b === "string") {
? ? ? var key = b;
? ? ? var proto = a;
? ? ? return makeDecorator(key, undefined)(proto, key);
? ? }
? ? var namespace = extractNamespace(b);
? ? var type = a;
? ? return makeDecorator(type, namespace);
? }
? return helper;
}

那就只能來看看 vue-class-component 了。

vue-class-component 的 @Component 裝飾器會返回一個 vue對象的構造函數。

// vue-class-component/lib/component.js
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
? if (typeof options === 'function') {
? ? return componentFactory(options)
? }
? return function (Component: VueClass<Vue>) {
? ? return componentFactory(Component, options)
? }
}

// 類組件
@Component
export default class HelloWorld extends Vue { ... }

Component 方法會把 class HelloWorld 傳入 componentFactory , 在其內部將 name 生命周期 methods computed 等注冊到 options 中,然后傳入 Vue.extend, 返回一個 vue對象的構造函數 。

export function componentFactory(
? Component: VueClass<Vue>,
? options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
? // 。。。無關代碼

? options.name =
? ? options.name || (Component as any)._componentTag || (Component as any).name;

? const proto = Component.prototype;

? (options.methods || (options.methods = {}))[key] = descriptor.value;

? // typescript decorated data
? (options.mixins || (options.mixins = [])).push({
? ? data(this: Vue) {
? ? ? return { [key]: descriptor.value };
? ? },
? });

? // computed properties
? (options.computed || (options.computed = {}))[key] = {
? ? get: descriptor.get,
? ? set: descriptor.set,
? };

? // add data hook to collect class properties as Vue instance's data
? (options.mixins || (options.mixins = [])).push({
? ? data(this: Vue) {
? ? ? return collectDataFromConstructor(this, Component);
? ? },
? });

? // vuex-class 包裝的方法會在此處注入
? const decorators = (Component as DecoratedClass).__decorators__;
? if (decorators) {
? ? decorators.forEach((fn) => fn(options));
? ? delete (Component as DecoratedClass).__decorators__;
? }

? const Super =
? ? superProto instanceof Vue ? (superProto.constructor as VueClass<Vue>) : Vue;
? const Extended = Super.extend(options);

? // 。。。無關代碼

? return Extended;
}

至此基本沒有什么問題,那么壓力就來到 vue 這里。返回的 Extended 是 Vue.extend 生成的 vue對象構造函數。

Vue.extend = function (extendOptions) {
? // 。。。無關代碼

? var Sub = function VueComponent(options) {
? ? this._init(options);
? };

? // 。。。無關代碼
? return Sub;
};

在 new Extended 的時候會調用 _init 初始化 vm 對象。

Vue.prototype._init = function (options) {
? // 。。。無關代碼

? initLifecycle(vm);
? initEvents(vm);
? initRender(vm);
? callHook(vm, "beforeCreate");
? initInjections(vm); // resolve injections before data/props
? initState(vm);
? initProvide(vm); // resolve provide after data/props
? callHook(vm, "created");

? // 。。。無關代碼
};

接下來就是無聊的打斷點調試了,最終找到在執行完 initState 方法后 vm 內的有些方法變為了 undefined ,initState 的作用是將 data methods 等注冊到 vm 上。

function initState(vm) {
? vm._watchers = [];
? var opts = vm.$options;
? if (opts.props) {
? ? initProps(vm, opts.props);
? }
? if (opts.methods) {
? ? initMethods(vm, opts.methods);
? }
? if (opts.data) {
? ? initData(vm);
? } else {
? ? observe((vm._data = {}), true /* asRootData */);
? }
? if (opts.computed) {
? ? initComputed(vm, opts.computed);
? }
? if (opts.watch && opts.watch !== nativeWatch) {
? ? initWatch(vm, opts.watch);
? }
}

再打斷點找到 initData 方法后產生的問題,initData 方法的作用是將 data 對象注冊到 vm 上,如果 data 是一個函數,則會調用該函數,那么問題就出現在 getData 中的 data.call(vm, vm) 這一句了。

function initData(vm) {
? var data = vm.$options.data;
? data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};

? // 。。。無關代碼
}

function getData(data, vm) {
? // #7573 disable dep collection when invoking data getters
? pushTarget();

? try {
? ? const a = data.call(vm, vm);
? ? return a;
? } catch (e) {
? ? handleError(e, vm, "data()");
? ? return {};
? } finally {
? ? popTarget();
? }
}

調用的 data.call(vm, vm) 是 vue-class-component 注冊的方法。好吧,又回到了 vue-class-component,我們來看看 vue-class-component 的代碼。

export function componentFactory(
? Component: VueClass<Vue>,
? options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
? // 。。。無關代碼
? (options.mixins || (options.mixins = [])).push({
? ? data(this: Vue) {
? ? ? return collectDataFromConstructor(this, Component);
? ? },
? });
? // 。。。無關代碼
}

在上面的 componentFactory 方法中,data 返回一個 collectDataFromConstructor 方法。在 collectDataFromConstructor 我們應該就可以解開謎題了。

function collectDataFromConstructor(vm, Component) {
? Component.prototype._init = function () {
? ? var _this = this;
? ? // proxy to actual vm
? ? var keys = Object.getOwnPropertyNames(vm); // 2.2.0 compat (props are no longer exposed as self properties)

? ? if (vm.$options.props) {
? ? ? for (var key in vm.$options.props) {
? ? ? ? if (!vm.hasOwnProperty(key)) {
? ? ? ? ? keys.push(key);
? ? ? ? }
? ? ? }
? ? }

? ? keys.forEach(function (key) {
? ? ? Object.defineProperty(_this, key, {
? ? ? ? get: function get() {
? ? ? ? ? return vm[key];
? ? ? ? },
? ? ? ? set: function set(value) {
? ? ? ? ? vm[key] = value;
? ? ? ? },
? ? ? ? configurable: true,
? ? ? });
? ? });
? }; // should be acquired class property values

? var data = new Component(); // restore original _init to avoid memory leak (#209)

? // 。。。無關代碼

? return data;
}
function Vue(options) {
? this._init(options);
}

傳下來的 Component 參數即 export default class HelloWorld extends Vue { ... }, new Component() 會獲取到 HelloWorld 內的所有參數。 Component 繼承于 Vue ,因此在 new Component() 時,會像 Vue 一樣先調用一遍 _init 方法,collectDataFromConstructor 置換了 Component 的 _init。

在置換的 _init 方法中,會遍歷 vm 上的所有屬性,并且將這些屬性通過 Object.defineProperty 再指回 vm 上。原因在于 initData 前會先 initProps initMethods 意味著,那么在 new Component() 時,探測到屬于 props methods 的值時就會指向 vm,而剩下的就是 data 值。

整個流程跑下來好像沒什么問題。不過既然使用了 Object.defineProperty 做 get set ,那會不會和 set 方法有關系呢?在 set 方法里打了一層斷點,果然觸發了,觸發的條件有些奇特。

@Component
export default class HelloWorld extends Vue {
? // vuex
? @model.State
? count: number;
? @model.Mutation("increment")
? increment: () => void;
? @model.Mutation("setCount")
? setCount: () => void = () => {
? ? this.count = this.count + 1;
? };

? // data
? msg: string = "Hello Vue 3 + TypeScript + Vite";
? // ? methods
? incrementEvent() {
? ? console.log(this);
? ? this.increment();
? ? this.msg = this.msg + " + " + this.count;
? }
? // ? 生命周期
? beforeCreate() {}
? created() {
? ? console.log(this);
? ? this.msg = this.msg + " + " + this.count;
? }
}

上面是一個很基礎的類組件,increment setCount 的 set 觸發,一個被傳入了 undefined 一個被傳入 () => { this.count = this.count + 1 },兩個都屬于 methods 但都是不是以 fn(){} 的方式賦予初始值,所以 incrementEvent 的 set 沒有觸發,increment 被傳入了 undefined,setCount 被傳入了一個函數

class A {
? increment;
? setCount = () => {};
? incrementEvent() {}
}

increment 和 setCount 為一個變量,而 incrementEvent 會被看做一個方法

奇怪的是在 vue-cli 中沒什么問題,set 方法不會觸發,為什么切換到 vite 之后 會觸發 set 重置掉一些變量的初始值。我想到是不是二者的編譯又問題。我對比了下二者編譯后的文件,果然。

vue-cli

export default class HelloWorld {
? constructor() {
? ? this.setCount = () => {
? ? ? this.count = this.count + 1;
? ? };
? ? // data
? ? this.msg = "Hello Vue 3 + TypeScript + Vite";
? }
? // ? methods
? incrementEvent() {
? ? console.log(this);
? ? this.increment();
? ? this.msg = this.msg + " + " + this.count;
? }
? // ? 生命周期
? beforeCreate() {}
? created() {
? ? console.log(this);
? ? this.msg = this.msg + " + " + this.count;
? }
}

vite

export default class HelloWorld {
? // vuex
? count;
? increment;
? setCount = () => {
? ? this.count = this.count + 1;
? };
? // data
? msg = "Hello Vue 3 + TypeScript + Vite";
? // ? methods
? incrementEvent() {
? ? console.log(this);
? ? this.increment();
? ? this.msg = this.msg + " + " + this.count;
? }
? // ? 生命周期
? beforeCreate() {}
? created() {
? ? console.log(this);
? ? this.msg = this.msg + " + " + this.count;
? }
}

可以看到 vue-cli vite 的編譯結果并不一致,vite 比 vue-cli 多出了 count increment 兩個默認值,這兩個值默認值是 undefined,在 vue-cli 并沒有編譯進去。下面只能去翻 vite 文檔了,一個屬性吸引了我。

查了下這個 useDefineForClassFields 屬性,簡單來講,useDefineForClassFields 為 false 的情況下 ts 會 跳過為 undefined 的變量,為 true 就會將默認值為 undefined 的變量屬性依然編譯進去。正常情況下不會有什么問題,但是 vue-class-component 會對 props methods 的屬性做一層劫持,那 new 初始化 的時候探測到這些值就會觸發 set,如果沒有默認值就會被賦值為 undefined。

解決

想要解決很簡單,只要在 tsconfig 中加入 useDefineForClassFields 屬性,并設置為 false 就可以了。

{
? "compilerOptions": {
? ? "target": "ESNext",
? ? "useDefineForClassFields": false,
? ? "module": "ESNext",
? ? "lib": ["ESNext", "DOM"],
? ? "moduleResolution": "Node",
? ? "strict": true,
? ? "sourceMap": false,
? ? "resolveJsonModule": true,
? ? "esModuleInterop": true,
? ? "noEmit": true,
? ? "noUnusedLocals": true,
? ? "noUnusedParameters": true,
? ? "noImplicitReturns": true
? },
? "include": ["./src"]
}

總結

在轉到 vite 的過程中,還是有許多坑要踩的,有時候并不是 vite 的問題,而是來自多方的問題,useDefineForClassFields 帶來的變化也不僅僅是會編譯為 undefined 的屬性,可以多了解一下,也可以拓寬一些知識。

原文鏈接:https://juejin.cn/post/7061878088056963085

欄目分類
最近更新