我启用了延迟更新。
我有两个组件。
第一个是列表,它简单地实现为带有 foreach 数据绑定的div:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
第二个是列表项:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender
为foreach
中的每个项目调用。
我的afterRenderPerson
函数非常简单:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
问题是,当afterRenderPerson
被称为子组件时,listitem-person
尚未呈现。
这意味着传递给afterRenderPerson
的元素数组有 4 个节点:
- 包含
n
的文本节点,即换行符。 - 包含
<!-- ko component: { name: "listitem-person", params: { person: $data } } -->
的注释节点。 - 包含
<!-- /ko -->
的注释节点。 - 包含
n
的文本节点,即换行符。
这些都不适合获取top
像素,即使它们适合,正在渲染的子组件也可能会影响该位置的布局,从而更改我尝试获取的像素的值。
不幸的是,foreach 的文档似乎没有考虑到组件的延迟性质。
如果需要在生成的 DOM 元素上运行一些进一步的自定义逻辑,可以使用下面描述的任何 afterRender/afterAdd/beforeRemove/beforeMove/afterMove 回调。
注意:这些回调仅用于触发与列表中更改相关的动画。
我遇到了两种解决方法,这两种解决方法都不是很好,但这就是为什么它们是解决方法而不是解决方案!
User3297291 在注释中给出了创建放置在子组件上的scrollTo
绑定的建议。
我能想到的唯一解决方法是定义自定义 scrollTo 绑定并将其包含在组件模板中......很容易实现,但仍然感觉很黑客,并且使您的内部组件更难重用。您可能还希望跟踪此功能请求 – user3297291
这只是一个自定义绑定,它根据提供给它的值有条件地执行某些代码。
在将 HTML 插入到 DOM 之前,不会调用绑定。这并不完美,因为以后对 DOM 的更改可能会影响插入的 HTML 元素的位置,但它应该适用于许多情况。
不过,我不太热衷于修改子组件,当保留在父组件中时,我更喜欢解决方案。
第二种解决方法是通过 ID 检查子组件 HTML 元素是否存在于 DOM 中。由于我不知道它们何时会出现,因此必须在某种循环中完成。
while 循环不合适,因为它会在"紧密"循环中过于频繁地运行检查,因此使用setTimeout
来代替。
setTimeout
是一个可怕的黑客,使用它让我觉得很脏,但它确实适用于这种情况。
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
我确保限制它可以运行多长时间,所以如果出现问题,它不会永远循环。
可能应该指出的是,这个问题有一个"终极"解决方案,那就是TKO,又名淘汰赛4。
但这还不是"生产就绪"。
如何知道组件何时完成 DOM 更新?
布莱恩亨特在6月20日评论
淘汰赛/TKO(KO 4 候选人(最新的主分支有这个。
更具体地说,applyBindings 系列函数现在返回一个 Promise,该承诺在绑定子子项(包括异步子项(时解析。
API 尚未设置或记录,但骨骼已设置。
这似乎有效。我创建了一个绑定处理程序,它在其init
中运行回调(它使用tasks.schedule
来允许渲染周期(。在父级别附加它不会及时渲染子元素,但将其附加到虚拟元素可以。
我将其设计为与签名类似于afterRender
的函数一起使用。由于它针对每个元素运行,因此回调函数必须测试数据是否适用于其中的第一个元素。
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>