所谓的“显式赋值断言”,其实就是!:
运算符。这是TypeScript 2.7引入的特性,可以参考TypeScript官方文档。不过还是建议看一下英文的文档,更准确一点。我必须吐槽,中文文档居然删掉了一部分原文。最过分的是,中文文档甚至删去了官方给出的一些用法!不知是何居心?
这次遇到的问题是Vue的计算属性不触发。计算属性不触发的原因想必大家都很清楚,无非是属性没有更新(避免重复计算以提高性能),或者依赖了不可响应的属性,这些官方文档已经说得很清楚了:
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
如果是用JavaScript写,这些其实都不是问题。但是这次我在项目里引入了TypeScript,而且为了写起来更舒服,还引入了vue-property-decorator
和vuex-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-decorator
和vuex-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,到底会变成什么样呢?尤大会怎么处理这些问题呢?真让人期待啊。