@Link修饰的状态变量变更只在前2次生效问题分析报告
@Link修饰的状态变量变更只在前2次生效问题分析报告 1 关键字 @Link;CustomDialogController 2 问题描述 能复现问题的代码如下(3.1release及截至当前[2022/10/8] master版本可复现): @CustomDialog struct MyDialog { private controller: CustomDialogControlle
1 关键字
@Link;CustomDialogController
2 问题描述
能复现问题的代码如下(3.1release及截至当前[2022/10/8] master版本可复现):
@CustomDialog
struct MyDialog {
private controller: CustomDialogController
@Link count: number
build() {
Row() {
Button("Change").fontSize(32)
.onClick(() => {
this.count = Math.random();
})
}
}
}
@Preview
@Entry
@Component
struct Index {
@State private count: number = 10;
dialogController: CustomDialogController = new CustomDialogController({
builder: MyDialog({ count: $count })
})
build() {
Column() {
Text(`${this.count}`).fontSize(82)
Text(`OpenDialog`)
.fontSize(50).onClick(() => {
this.dialogController.open()
})
}
}
}
测试步骤:
-
点击'OpenDialog',弹出自定义弹窗
-
点击自定义弹窗里面的'Change'超过2次
Index组件里count变量对应展示的数字只变化了2次,第3次及后续点击数字不再变化
3 问题原因
3.1 正常机制
点击自定义弹窗里面的'Change'每次点击count变化会变化为一个新的随机数,由@Link装饰的变量可以和父组件的@State变量建立双向数据绑定,父组件Index展示新的随机数
3.2 异常机制
父组件Index只在前面2次点击后展示新的随机数
4 解决方案
使用自定义组件实现类似弹窗效果,避免使用CustomDialogController实现带有与其他组件实现数据交互的弹窗效果.
5 定位过程
eTS的声明式开发范式下,组件最终都继承自NativeView,源码:\foundation\ace\ace_engine\frameworks\bridge\declarative_frontend\jsview\js_view.cpp NativeView的相关实现类里中以下两个成员customViewChildren和lastAccessedViewIds,分表表示:存储组件内子级组件的unordered_map(子组件id为key),存储组件内仍然处于可关联状态的子级组件的id的unordered_set。
// hold handle to the native and javascript object to keep them alive
// until they are abandoned
std::unordered_map<std::string, JSRef<JSObject>> customViewChildren_;
// a set of valid viewids on a renderfuntion excution
// its cleared after cleaning up the abandoned child.
std::unordered_set<std::string> lastAccessedViewIds_;
因数据变化界面需要刷新界面时,会执行到 JSView::InternalRender,这个方法会执行 JSView::CleanUpAbandonedChild,主要逻辑用来对一些已经不再被其他组件关联的组件进行Destroy操作,并且清空lastAccessedViewIds_:
void JSView::CleanUpAbandonedChild()
{
auto startIter = customViewChildren_.begin();
auto endIter = customViewChildren_.end();
while (startIter != endIter) {
//遍历查找customViewChildren_,是否存在于lastAccessedViewIds_
auto found = lastAccessedViewIds_.find(startIter->first);
if (found == lastAccessedViewIds_.end()) {
//如果lastAccessedViewIds_里找不到节点id,则执行view->Destroy
removedViewIds.emplace_back(startIter->first);
auto* view = startIter->second->Unwrap<JSView>();
if (view != nullptr) {
view->Destroy(this);
}
}
++startIter;
}
...
//清空lastAccessedViewIds_
lastAccessedViewIds_.clear();
}
对应到示例上,当第一次点击自定义弹窗内的的'Change'时,执行到CleanUpAbandonedChild,在代码的最后清空了lastAccessedViewIds。第二次点击时,执行到CleanUpAbandonedChild方法的while循环内,此时在lastAccessedViewIds内已经查询不到自定义弹窗组件的对应id,执行view->Destroy(this);:
void JSView::Destroy(JSView* parentCustomView)
{
...
{
ACE_SCORING_EVENT("Component[" + viewId_ + "].AboutToBeDeleted");
jsViewFunction_->ExecuteAboutToBeDeleted(); //执行组件view内的aboutToDisappear函数
}
}
执行自定义弹窗view内的aboutToDisappear函数,这个函数在ets编译后的js文件内定义如下:
aboutToBeDeleted() {
this.__count.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id());
}
此处涉及到ETS状态管理部分,执行的大致逻辑如下: 1:SubscriberManager '订阅器总管'删除了弹窗view的id 2:弹窗view内基于count的SynchedPropertySimpleTwoWay变量的id也从'订阅器总管'中删除了
这意味着: 1:此后弹窗view内的count变量的变化不再通知到父级组件 2:父级组件通过@link关联的count变量的变化不再通知到弹窗view
class SynchedPropertySimpleTwoWay<T> extends ObservedPropertySimpleAbstract<T>
//source_解除关联id,@link不再生效,状态变化source_与SynchedPropertySimpleTwoWay之间不再相互通知
aboutToBeDeleted() {
this.source_.unlinkSuscriber(this.id__());
this.source_ = undefined;
super.aboutToBeDeleted();
}
即第3次点击后父组件界面已经不会再刷新
但普通的自定义组件不会产生此现象,原因在于ETS对于普通组件编译后的js文件形式如下:
render() {
Column.create();
let earlierCreatedChild_2 = this.findChildById("2");
状态变化致render执行时,通过这个findChildById,最终把普通的自定义组件的viewId添加到了lastAccessedViewIds_:
void JSView::FindChildById(const JSCallbackInfo& info)
{
std::string viewId = info[0]->ToString();
info.SetReturnValue(GetChildById(viewId)); //执行GetChildById
}
JSRef<JSObject> JSView::GetChildById(const std::string& viewId)
{
auto id = ViewStackProcessor::GetInstance()->ProcessViewId(viewId);
auto found = customViewChildren_.find(id);
if (found != customViewChildren_.end()) {
ChildAccessedById(id); //执行ChildAccessedById
...
}
void JSView::ChildAccessedById(const std::string& viewId)
{
//lastAccessedViewIds_添加viewId
lastAccessedViewIds_.emplace(viewId);
}
如此在CleanUpAbandonedChild方法内,不会执行到view->Destroy(this);,不会致使@link失效。 但是对于CustomDialogController自定义弹窗,编译后的js文件里,并没有对应代码最终能实现JSview更新lastAccessedViewIds_的机制。
首先从现象上看非常类似组件被'销毁',通过应用内添加日志验证这一猜测: 应用ETS通过打印 SubscriberManager.INSTANCE.subscriberById查看数据变化时对应ID的变化:
Button("Change").fontSize(32)
.onClick(() => {
for (let [id, subscriber] of SubscriberManager.INSTANCE_.subscriberById_) {
// @ts-ignore
console.log(`Id: ${id} -> ${subscriber['info'] ? subscriber['info']() : 'view'}`);
}
this.count = Math.random();
})
LOG:
//第一次点击
[default][Console DEBUG] app Log: Id: 0 -> view
[default][Console DEBUG] app Log: Id: 1 -> count
[default][Console DEBUG] app Log: Id: 2 -> view
[default][Console DEBUG] app Log: Id: 3 -> count
//第二次点击
[default][Console DEBUG] app Log: Id: 0 -> view
[default][Console DEBUG] app Log: Id: 1 -> count
[default][Console DEBUG] app Log: Id: 2 -> view
[default][Console DEBUG] app Log: Id: 3 -> count
//第三次点击
[default][Console DEBUG] app Log: Id: 0 -> view
[default][Console DEBUG] app Log: Id: 1 -> count
即view类型的id在第三次少了ID为2的view。最有可能的是aboutToBeDeleted,进而通过添加底层源码日志(不再详细列举)编译进一步验证,从而缩小排查引起问题出现的原因范围。
6 知识分享
@link装饰的变量与其父组件中的数据共享相同的值,父子组件双向同步。子组件被@link装饰的变量与其父组件中对应的数据建立双向数据绑定
更多推荐


所有评论(0)