TypeScript显式赋值断言导致Vue属性非响应

所谓的“显式赋值断言”,其实就是!:运算符。这是TypeScript 2.7引入的特性,可以参考TypeScript官方文档。不过还是建议看一下英文的文档,更准确一点。我必须吐槽,中文文档居然删掉了一部分原文。最过分的是,中文文档甚至删去了官方给出的一些用法!不知是何居心?


这次遇到的问题是Vue的计算属性不触发。计算属性不触发的原因想必大家都很清楚,无非是属性没有更新(避免重复计算以提高性能),或者依赖了不可响应的属性,这些官方文档已经说得很清楚了:

计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。

如果是用JavaScript写,这些其实都不是问题。但是这次我在项目里引入了TypeScript,而且为了写起来更舒服,还引入了vue-property-decoratorvuex-class

问题是这样的,也算是一个很常见的业务场景,一个导航组件NavBar,需要从vuex里读取token,并且放进计算属性来检验登录状态;为了避免刷新后vuex状态丢失的问题,还用了localStorage来进一步存储token。然后,点击退出后要清空所有的token。简化的写法大概是这样:

@Component({})
export default class NavBar extends Vue {
    // 获取token
    @State('token') token!: string;
	// 登录状态
	get hasLogin() {
        return !!this.token || !!localStorage.getItem('token');
    }
	// 更新token
	@Action('updateToken') updateToken!: (payload: RootPayload) => void;
    // 登出
    logout() {
        this.updateToken({
            token: ''
        });
        localStorage.removeItem('token');
    }
}

我假设看到这里的人都大概看过了vue-property-decoratorvuex-class的用法,也用过注解(或者说装饰器)。为什么这里要用所谓的显式赋值断言呢?因为vuex-class的官方文档的写法是这样的:

@State('foo') stateFoo;

而这样就会因为TypeScript编译器的null检查而报错。我看网上有人说可以在tsconfig.json里面加上"strictNullChecks": false关闭null检查来通过编译,但我不是很喜欢为了一点点方便而关闭代码检查,导致可能存在的质量降低,所以就使用了!:这个语法来告诉编译器我这里一定有值。

但是问题来了,触发logout函数之后登录状态没有发生变化。也就是说,并没有退出。经过调试,我发现计算属性并没有触发。localStorage显然是不可响应的,那么需要检查的就是token了,也就是说,要么token值没有变,要么token不可响应。这就很奇怪了,理论上来说vuex中的变化是会映射到Vue实例里的,而且这里获取的token也应该是挂载到data里的,因为按照vue-class-component文档上的说法:

Initial data can be declared as class properties.

@Component({...})
export default class App extends Vue {
  // initial data
  msg = 123
}

再次调试,发现token的值会发生变化。那么,唯一的解释就是token不可响应。但这是为什么?难道是这个库错了?但我查了vue-class-component的issue,并没有发现有人反馈这个问题。那么,姑且认为不是库的问题。

其实之前我怀疑过是异步问题导致的,因为Action会返回一个Promise,而我在代码里并没有显式地处理。于是我换成了同步的Mutation,但没有变化。

既然不可响应,那就加一个可响应的属性好了。修改代码:

@Component({})
export default class NavBar extends Vue {
    // 获取token
    // @State('token') token!: string;
	respondToken: string = ''; // 可响应的token
	// 登录状态
	get hasLogin() {
        return !!this.respondToken || !!localStorage.getItem('token');
    }
	mounted() {
        this.respondToken = localStorage.getItem('token') as string;
    }
	// 更新token
	@Action('updateToken') updateToken!: (payload: RootPayload) => void;
    // 登出
    logout() {
        this.updateToken({
            token: ''
        });
        localStorage.removeItem('token');
        this.respondToken = '';
    }
}

问题解决。但是为什么呢?想了半天,我想起了Vue的生命周期。data里的可响应属性是在create期间挂载的。也就意味着,这里的token没有在create期间挂载。我把改动前后的代码用tsc编译了一下,发现了端倪:

// 修改前
var NavBar = /** @class */ (function (_super) {
    __extends(NavBar, _super);
    function NavBar() {
        var _this = _super !== null && _super.apply(this, arguments) || this;
        // props
        // 空的!
        return _this;
    }
    // ...
    __decorate([
        vuex_class_1.State("token")
    ], NavBar.prototype, "token");
    __decorate([
        vuex_class_1.Action('updateToken')
    ], NavBar.prototype, "updateToken");
    return NavBar;
}(vue_property_decorator_1.Vue));
// 修改后
var NavBar = /** @class */ (function (_super) {
    __extends(NavBar, _super);
    function NavBar() {
        var _this = _super !== null && _super.apply(this, arguments) || this;
        // props
        _this.respondToken = ''; // 可响应的token,出现了!
        return _this;
    }
    // ...
    __decorate([
        vuex_class_1.State('token')
    ], NavBar.prototype, "token");
    __decorate([
        vuex_class_1.Action('updateToken')
    ], NavBar.prototype, "updateToken");
    return NavBar;
}(vue_property_decorator_1.Vue));

从代码里可以看出,data是被放在构造器里初始化的。而装饰器,也就是我们的vuex-class,是在整个组件初始化之后才挂载的。当然,并不能全怪它,TypeScript的编译器也要背锅,因为TypeScript的编译器在构造时会忽略显式赋值断言的变量。我们随便写一个类编译一下就知道了:

// ts
class Test {
	test!: string;
}
// js
var Test = /** @class */ (function () {
    function Test() {
    }
    return Test;
}());

看到了吧。这玩意着实害人不浅。不过,这也让我对Vue3产生了更强烈的兴趣:用TypeScript重构后的Vue,到底会变成什么样呢?尤大会怎么处理这些问题呢?真让人期待啊。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值