对反应变量的“each...in”渲染的误解

Misunderstanding `each...in` rendering with reactive variable

我是 Meteor/Blaze 的新手,但这是我公司正在使用的。

我很难理解 Blaze 如何决定渲染基于 ReactiveDict

的内容

TL;DR

我从 parent 数组中的 ReactiveDict 数组创建了一些 children 模板。当 ReactiveDict 更改时,children 模板中的数据未刷新,我不明白为什么。

我可能对 Blaze 渲染有一些误解。你能帮帮我吗?


Parent

Parent Html 模板

<template name="parent">
    {{#each child in getChildren}}
        {{> childTemplate (childArgs child)}}
    {{/each}}
</template>

ParentJavascript

反应变量

模板从 getChildren 助手呈现 children 模板,该助手仅检索 ReactiveDict.

// Child data object
const child = () => ({
  id: uuid.v4(),
  value: ""
});

// Create a reactive dictionary
Template.parent.onCreated(function() {
  this.state = new ReactiveDict();
  this.state.setDefault({ children: [child()] });
});
// Return the children from the reactive dictionary
Template.parent.helpers({
  getChildren() {
    return Template.instance().state.get('children');
  }
});
Child 模板参数(来自 parent 模板)

parent 模板为 child 模板提供了一些用于设置默认值和回调的数据。 每个都使用 childArgs 函数实例化,该函数使用 child 的 id 来设置正确的数据和回调。 单击 add 按钮时,它会将 child 添加到 ReactiveDict 中的 children 数组。 单击 delete 按钮时,它会从 ReactiveDict 中的 children 数组中删除 child。

Template.parent.helpers({
    // Set the children arguments: default values and callbacks
    childArgs(child) {
        const instance = Template.instance();
        const state = instance.state;
        const children = state.get('children');
        return {
           id: child.id,
           // Default values
           value: child.value, 
           // Just adding a child to the reactive variable using callback
           onAddClick() {
                const newChildren = [...children, child()];
                state.set('children', newChildren); 
           },
           // Just deleting the child in the reactive variable in the callback
           onDeleteClick(childId) { 
                const childIndex = children.findIndex(child => child.id === childId);
                children.splice(childIndex, 1);
                state.set('children', children); 
            }
        }
    }
})

Child

Child html 模板

模板显示来自 parent 和 2 个按钮 adddelete 的数据。

<template name="child">
        <div>{{value}}</div>
        <button class="add_row" type="button">add</button>
        <button class="delete_row" type="button">delete</button>
</template>

Child javascript(事件)

这里调用的两个函数是作为参数从 parent 模板传递的回调。

// The two functions are callbacks passed as parameters to the child template
Template.child.events({
  'click .add_row'(event, templateInstance) {
    templateInstance.data.onAddClick();
  },
  'click .delete_row'(event, templateInstance) {
    templateInstance.data.onDeleteClick(templateInstance.data.id);
  },

问题

我的问题是,当我删除 child(使用回调来设置 ReactiveDict,就像 onAddClick() 函数)时,我的数据没有正确呈现。

文本示例:

我这样添加行。

child 1 | value 1
child 2 | value 2
child 3 | value 3

当我删除 child 2 时,我得到这个:

child 1 | value 1
child 3 | value 2

我想要这个:

child 1 | value 1
child 3 | value 3

我在 Template.child.onRendered() 函数中使用来自 childArgs 的数据初始化 child

  1. :删除ReactiveDict中的child时调用getChildren()函数,我在变量中有正确的数据(children 中的 ReactiveDict)。
  2. :如果我有 3 个 children,我删除了一个,parent 模板呈现只有2children。
  3. :然而 child 的 onRendered() 从未被调用(child 的 onCreated() 也未被调用功能)。这意味着 child 模板显示的数据是错误的。

图片示例

我正在添加图片以帮助理解:

正确 html

显示的HTML是正确的:我有3个children,我删除了第二个。在我的 HTML 中,我可以看到显示的两个 children 在它们的 div 中具有正确的 ID。然而显示的数据是错误的。

陈旧数据

我已经删除了第一张图片中的第二个child。显示的children应该是第一个和第三个。 在控制台日志中,我的数据是正确的。红色数据是第一位的。紫色是第三。

然而我们可以看到删除的child的数据显示出来了(asdasdasd)。删除标签时,我可以在日志中看到第二个 child 的 ID,尽管它应该不再存在了。第二个 child ID 为绿色。

我可能误会了什么。你能帮帮我吗?

我不确定从哪里开始,但有很多错误,我更愿意在这里提供 运行 解决方案并附上评论。

首先,each 函数应该正确传递 id 而不是整个子项,否则 find 将导致错误:

<template name="parent">
  {{#each child in getChildren}}
    {{#with (childArgs child.id)}}
      {{> childTemplate this}}
    {{/with}}
  {{/each}}
</template>

在帮助程序中,您可以使用 lexical scoping:

避免调用过多的 Template.instance() 函数
childArgs (childId) {
  const instance = Template.instance()
  const children = instance.state.get('children')
  const childData = children.find(child => child.id === childId)
  const value = {
    // Default values
    data: childData,

    // Just adding a child to the reactive variable using callback
    onAddClick () {
      const children = instance.state.get('children')
      const length = children ? children.length : 1
      const newChild = { id: `data ${length}` }
      const newChildren = [...children, newChild]
      instance.state.set('children', newChildren)
    },

    // Just deleting the child in the reactive variable in the callback
    onDeleteClick (childId) {
      const children = instance.state.get('children')
      const childIndex = children.findIndex(child => child.id === childId)
      children.splice(childIndex, 1)
      instance.state.set('children', children)
    }
  }
  return value
}

然后请注意,在事件回调 'click .delete_row' 中,您正在使用 templateInstance.data.id,但这是 always undefined 与您当前的结构。它应该是 templateInstance.data.data.id 因为 data 总是为模板实例中的所有数据定义的,如果你命名 属性 data 那么你必须通过 [=23 访问它=]:

Template.childTemplate.events({
  'click .add_row' (event, templateInstance) {
    templateInstance.data.onAddClick()
  },
  'click .delete_row' (event, templateInstance) {
    templateInstance.data.onDeleteClick(templateInstance.data.id)
  }
})

现在你的数据为什么被奇怪地排序也是有道理的。看看 onDeleteClick 回调:

onDeleteClick (childId) {
  // childId is always undefined in your code
  const children = instance.state.get('children')
  const childIndex = children.findIndex(child => child.id === childId)
  // childIndex is always -1 in your code
  // next time make a dead switch in such situations:
  if (childIndex === -1) {
    throw new Error(`Expected child by id ${childId}`)
  }
  children.splice(childIndex, 1)
  // if index is -1 this removes the LAST element
  instance.state.set('children', children)
}

所以你的问题是拼接行为和将未经检查的索引传递给拼接:

The index at which to start changing the array. If greater than the length of the array, start will be set to the length of the array. If negative, it will begin that many elements from the end of the array (with origin -1, meaning -n is the index of the nth last element and is therefore equivalent to the index of array.length - n). If array.length + start is less than 0, it will begin from index 0.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice

我解决了我的问题。但是我还是不明白Blaze是如何选择渲染的。 现在,该解决方案看起来有点像 @Jankapunkt 在其解决方案的第一部分中给出的解决方案,但不完全相同。获得 child 的 find 工作得很好。但是现在我使模板渲染依赖于反应性助手,它 re-renders id 更改时的模板(当它仅依赖于 [=17= 中的 child 本身时它没有]循环)。

最后,我不明白each...in循环是做什么的,它是如何利用数据循环的。请参阅注意事项

为了在应有的地方给予学分,我想到了从 .

实现该依赖项的想法

从原始代码编辑

我编辑 parent 模板使 child 渲染依赖于它自己的 id。这样,当 child.id 更改时,模板 re-renders.

Html 模板

我将 child.id 的依赖项添加到 re-render child 模板。

<template name="parent">
    {{#each childId in getChildrenIds}}
        {{#let child=(getChild childId)}}
            {{> childTemplate (childArgs child)}}
        {{/let}}
    {{/each}}
</template>

Javascript

我现在有两个帮手。一个到 return each...in 循环的 id,另一个到 return 来自 id 的 child 并强制 child 模板 re-render。

Template.parent.helpers({
    // Return the children ids from the reactive dictionary
    getChildrenIds() {
        const children = Template.instance().state.get('children');
        const childrenIds = children.map(child => child.id);
        return childrenIds;
    },
    // Return the child object from its id
    getChild(childId) {
        const children = Template.instance().state.get('children');
        const child = children.find(child => child.id === childId);
        return child;
    }
});

完整代码

这是完整的解决方案。

Parent

Html模板

<template name="parent">
    {{#each childId in getChildrenIds}}
        {{#let child=(getChild childId)}}
            {{> childTemplate (childArgs child)}}
        {{/let}}
    {{/each}}
</template>

Javascript

// Child data object
const child = () => ({
  id: uuid.v4(),
  value: ""
});

// Create a reactive dictionary
Template.parent.onCreated(function() {
  this.state = new ReactiveDict();
  this.state.setDefault({ children: [child()] });
});

Template.parent.helpers({
    // Return the children ids from the reactive dictionary
    getChildrenIds() {
        const children = Template.instance().state.get('children');
        const childrenIds = children.map(child => child.id);
        return childrenIds;
    },
    // Return the child object from its id
    getChild(childId) {
        const children = Template.instance().state.get('children');
        const child = children.find(child => child.id === childId);
        return child;
    },
    // Set the children arguments: default values and callbacks
    childArgs(child) {
        const instance = Template.instance();
        const state = instance.state;
        const children = state.get('children');
        return {
           id: child.id,
           // Default values
           value: child.value, 
           // Just adding a child to the reactive variable using callback
           onAddClick() {
                const newChildren = [...children, child()];
                state.set('children', newChildren); 
           },
           // Just deleting the child in the reactive variable in the callback
           onDeleteClick(childId) { 
                const childIndex = children.findIndex(child => child.id === childId);
                children.splice(childIndex, 1);
                state.set('children', children); 
            }
        }
    }
});

Child

Html模板

<template name="child">
    <div>{{value}}</div>
    <button class="add_row" type="button">add</button>
    <button class="delete_row" type="button">delete</button>
</template>

Javascript

Template.child.events({
    'click .add_row'(event, templateInstance) {
        templateInstance.data.onAddClick();
    },
    'click .delete_row'(event, templateInstance) {
        templateInstance.data.onDeleteClick(templateInstance.data.id);
    }
});

注意事项

解决方案有效。但是我的 each..in 循环很奇怪。

当我删除 child 时,调用 getChildrenIds() 助手时我得到了正确的 ID。

但是 each..in 循环遍历原始 ID,即使那些已被删除并且在 getChildrenIds() return 值中 NOT 的 ID 也是如此。模板当然没有呈现,因为 getChild(childId) 抛出错误(child 被删除)。然后显示正确。

我完全不理解这种行为。有人知道这里发生了什么吗?

如果有人有明确的答案,我很想听听。

解决这个问题的正确方法

解决此问题的正确方法是创建您自己的 _id,每次对象数组更改时它都会获得一个新的唯一 _id。它在此处的 Blaze 文档中进行了概述:http://blazejs.org/api/spacebars.html#Reactivity-Model-for-Each

这只会在您处理带有非游标的#each 块时发生,例如数组或对象数组。

基于游标的数据与#each 块一起工作正常并正确重新呈现,如 Pages.findOne(id)。

示例,如果你需要处理数组和#each 块

不工作

[
  {
    name: "Fred",
    value: 1337
  },
  {
    name: "Olga",
    value: 7331
  }
]

工作

[
  {
    name: "Fred",
    value: 1337,
    _id: "<random generated string>"
  },
  {
    name: "Olga",
    value: 7331,
    _id: "<random generated string>"
  }
]