缘起
关于this指向问题,有个开发中的小插曲:
我在组件A中创建一个@BuilderParam成员作为插槽,在页面中调用A组件,并把该页面的一个@Builder传入这个插槽。在这个@Builder中有个按钮动作,一点就导致App闪退。
下面复现一下这个问题。首先是组件代码:
@Component
struct CompWillCrash {
@BuilderParam slot?: () => void
build() {
Column() {
Text("This is `CompWillCrash`")
if(typeof this.slot === 'function') {
this.slot()
}
}
}
}
页面调用代码:
@Entry
@Component
struct Index {
@State message: string = 'Index'
@Builder
Slot() {
Button('Let's crash').onClick(() => {
promptAction.showToast({
message: this.message
})
})
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }){
CompWillCrash({
slot: this.Slot, // 这里传入内部builder插槽
})
}
.size({ width: '100%', height: '100%' })
}
}
先读代码:我们在页面中定义组件Slot
,组件中有个按钮,点击后toast页面状态message
的值。
这个Slot
组件通过slot
参数传递给组件CompWillCrash
,最终是由CompWillCrash
将Slot
组件渲染到页面上。
可能有些同学到这里就能感知到问题了。看下效果:
如果到这里还没看出问题,那么我展示一下一行代码解决app闪退
的功力:
@Component
struct CompWillCrash {
@BuilderParam slot?: () => void
@State message: string = 'Watch out!' // ---> 添加一行代码
build() {
Column() {
Text("This is `CompWillCrash`")
if(typeof this.slot === 'function') {
this.slot()
}
}
}
}
来看效果:
缘由
因为Slot
(注意大小写)组件是个Function
,这个function
是在组件中调用的,在this.slot()
运行的时候,Slot
的上下文会变成this.slot()
里的this
,也就是组件CompWillCrash
。
所以,Slot
组件的onClick
触发时,promptAction.showToast
调用的this.message
,实际上是CompWillCrash.prototype.message
,是undefined
,导致参数错误并闪退:
Error message:Parameter error. The type of message is incorrect.
Error code:401
SourceCode:
promptAction.showToast({
复习一下this上下文
我们在web
范畴讨论下面的代码:
(function() { console.log(this) })();
(() => { console.log(this) })();
const obj = {
f1(){ console.log(this) },
f2: function() { console.log(this) },
f3: () => { console.log(this) }
}
class A { f4(){ console.log(this) } f5 = () => { console.log(this) } }
obj.f1()
obj.f2()
obj.f3()
new A().f4()
new A().f5()
输出:
Window
Window
{f1: ƒ, f2: ƒ, f3: ƒ}
{f1: ƒ, f2: ƒ, f3: ƒ}
Window
A
A
其中,obj.f1
和obj.f2
都是function
声明,只不过f1
是简化写法。
整体来讲输出符合预期,不赘述。下面进阶一下:
const of1 = obj.f1;
const of2 = obj.f2;
const of3 = obj.f3;
const of4 = new A().f4;
const of5 = new A().f5;
of1();
of2();
of3();
of4();
of5();
输出结果:
Window
Window
Window
undefined
A
这个结果倒也不是很意外。
其中obj
内部的3个函数,前两个of1
of2
是因为调用方式变化,所以指向了Window
对象;后一个of3
是箭头函数,this
总是指向定义时所在位置的上下文,这和of5
箭头函数输出A
同理。
关于of4
输出undefined
的原因,大概说一下我的理解:f4
作为function
声明,会被记录在原型链上(f5
并不在原型链上)。我们重新赋值出来of4
,由于f4
调用方式发生变化,相当于打断了原型链,原本this
指向的实例消失,所以输出undefined
。
回到ArkTS
首先页面中的Slot
在定义时是function
声明(也不支持箭头函数声明呢),所以一定会受到调用方式的变化。那么在组件中,this.slot()
方式调用Slot
,Slot
的上下文自然就变成组件的实例。
同时Slot
内部的onClick事件绑定的是箭头函数,按箭头函数总是指向定义时所在位置的上下文
来推算,那里面的this
自然会随Slot
变成组件自己。
缘解
解决this指向问题自然办法很多,首先来一套bind
:
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }){
CompWillCrash({
slot: this.Slot.bind(this),
})
}
.size({ width: '100%', height: '100%' })
}
别说,这样真能解决:
但是官方不支持呢:
同样不支持的还有call
apply
。
寻求官方issue支持
找官方提了issue,希望要么支持箭头函数
组件,也就是@Builder
需要支持箭头函数;要么支持function
声明函数的bind
支持。
当时挺急的,issue的言辞也比较硬,因为这种指向问题,与代码观感严重不符,掉坑几率是百分百的;另外,如果没有有效的解决方案,不但开发受影响,而且几乎可以说明ArkTS
的组件模式不能自洽。
几轮沟通之后,我终于说清楚了问题,官方给了推荐做法:
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }){
CompWillCrash({
slot: () => {
this.Slot()
},
})
}
.size({ width: '100%', height: '100%' })
}
看了之后不吱声了,怪自己代码还不够骚……
然后呢
这应该是卡我开发的最后一个问题,后面就ArkTS
起飞了。