误解 `each...in` 渲染与反应变量

Posted

技术标签:

【中文标题】误解 `each...in` 渲染与反应变量【英文标题】:Misunderstanding `each...in` rendering with reactive variable 【发布时间】:2020-04-02 12:17:10 【问题描述】:

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

我很难理解 Blaze 是如何决定渲染基于 ReactiveDict 的内容

TL;DR

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

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


家长

html 模板

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

javascript

反应变量

模板呈现来自 getChildren 助手的子模板,该助手仅检索 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');
  
);
子模板参数(来自 parent 模板)

父模板给子模板一些用于设置默认值和回调的数据。 每个都使用childArgs 函数进行实例化,该函数使用孩子的id 来设置正确的数据和回调。 单击add 按钮时,它会将一个子级添加到ReactiveDict 中的children 数组中。 单击delete 按钮时,它会从ReactiveDict 中的children 数组中删除子元素。

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); 
            
        
    
)

孩子

子 html 模板

模板显示来自父级和 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>

子 javascript(事件)

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

// 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);
  ,

问题

我的问题是当我删除一个孩子时(使用回调来设置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

    很好:删除ReactiveDict 中的child 时调用getChildren() 函数,并且我在变量中有正确的数据(ReactiveDict 中的children)。 :如果我有 3 个孩子并且我删除了一个,那么 parent 模板只会呈现 2 个孩子。 不好:但是子进程的 onRendered() 从未被调用(子进程的 onCreated() 函数也没有)。这意味着为 child 模板显示的数据是错误的。

图片示例

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

正确的html

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

过时的数据

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

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

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

【问题讨论】:

这个问题不完整,无法回答。我们基本上还需要parent 的html 部分,如果有child 模板也是html 和js 代码。我真的很好奇你是如何调用 onDeleteClick 的,因为通常模板事件应该用于 ui 事件。 @Jankapunkt 这应该是所有需要的信息。不要犹豫,问您是否觉得缺少其他东西。 【参考方案1】:

我不知道从哪里开始,但是有很多错误,我宁愿在这里用 cmets 提供一个运行的解决方案。

首先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,但这是始终 undefined 与您当前的结构。它应该是templateInstance.data.data.id,因为data 始终是为模板实例中的所有数据定义的,如果您将属性命名为data,那么您必须通过data.data 访问它:

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)

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

开始更改数组的索引。如果大于数组的长度,start 将被设置为数组的长度。如果为负数,它将从数组末尾开始那么多元素(原点 -1,意味着 -n 是倒数第 n 个元素的索引,因此等效于 array.length - n 的索引)。如果 array.length + start 小于 0,则从索引 0 开始。

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

【讨论】:

首先,非常感谢您花时间检查我的问题。关于id,很抱歉我忘了在我设置它的地方添加代码部分。我现在将它添加到问题中。我确实没有add/delete 函数有任何问题。一切正常,我的反应式词典始终处于正确状态。 getChildren() 助手被调用,反应式字典的值是正确的。问题是从反应式字典中删除了正确的子模板,删除了一个子模板,但未重新渲染剩余的子模板,因此数据已过时。 PS:我没有在删除最后一行的情况下进行保护,因为它们已经存在于生产代码中,并且与我的问题无关。关于实例变量,我已经在示例中对其进行了更正,但它已经在我的生产代码中,并且与我的问题无关。 我不想听起来很粗鲁,但有人可以解释为什么这个答案在没有回答问题的情况下不断得到支持吗?如果是为了付出努力,我同意这是应得的,特别是因为我第一次发布问题时忘记添加完整的代码。谢谢你们! :)【参考方案2】:

我解决了我的问题。但我还是不明白 Blaze 是如何选择渲染的。 现在,该解决方案看起来有点像 @Jankapunkt 在其解决方案的第一部分中给出的解决方案,但并不完全一样。 find 让孩子工作得很好。但是现在我使模板渲染依赖于响应式助手,它会在 id 更改时重新渲染模板(当它仅依赖于来自 each...in 循环的子节点本身时,它不会重新渲染模板)。

最后,我不明白each...in 循环的作用以及它如何使用数据进行循环。请参阅注意事项

为了给予应有的荣誉,我想到了从 this post 实现该依赖项。

从原始代码修改

我编辑父模板以使子渲染依赖于它自己的 id。这样,当child.id 更改时,模板会重新渲染。

HTML 模板

我添加了对child.id 的依赖以重新渲染子模板。

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

Javascript

我现在有两个助手。一个返回 each...in 循环的 id,另一个从 id 返回子模板并强制子模板重新渲染。

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;
    
);

完整代码

这是完整的解决方案。

家长

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); 
            
        
    
);

孩子

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 循环很奇怪。

当我删除一个孩子时,我会在调用 getChildrenIds() 帮助程序时获得正确的 ID。

each..in 会循环遍历原始 ID,即使是那些被删除且在getChildrenIds() 返回值中NOT 的 ID。模板当然不会被渲染,因为getChild(childId) 会抛出错误(子元素被删除)。显示正确。

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

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

【讨论】:

getChildrenIds() 似乎不是 reactive 助手。这意味着当状态改变时它不会自动刷新,因此它会返回一个陈旧的列表。不知道为什么你的模板状态不是反应性的,它应该是。为什么要手动管理状态而不是让 Meteor 管理它? 您好!对不起,我不明白你的问题。 “让 Meteor 管理它”是什么意思? Meteor 将如何为我管理我的状态?我正在创建孩子并且需要一个 id,因为它们保存数据,并且我希望在删除任何索引处的输入行时保持正确的用户输入,并且 Meteor/Blaze 不会自行刷新模板。我还没有找到正确的方法,这就是我使用我的解决方案的原因。如果您有更好的方法来做到这一点,我很想听听!感谢您的宝贵时间。 通常情况下,Meteor 应用程序中的客户端状态在 minimongo 中进行管理,您的模板助手只需执行 collectionName.find() 即可为您提供要在模板中显示的项目列表。删除项目将导致模板自动重新渲染,并删除已删除的元素。您已经添加了自己的状态管理,由于某种原因它没有触发重新渲染。 我不明白应该如何使用 minimongo 来管理客户端中的 UI 状态。我什至在文档中的任何地方都没有看到它推荐。为什么要使用发布/订阅机制来保存 UI 相关数据?为什么我应该调用 dB 来让项目显示正确输入的数据?您建议我将数据保存在客户端 dB 中只是为了记住用户在我的 UI 中键入的一些内容?这对我来说似乎很hacky。我并不是要显得粗鲁或不屑一顾,我只是不明白这将是一种管理 UI 状态的理智和正确的方法。我认为反应式 var/dict 就是为此而存在的。 取决于你的状态是纯 UI 还是同步到服务器的状态。如果您的代码中的children 代表数据库对象,那么您真的希望将集合与发布和订阅一起使用。如果它们是没有后端的纯 UI 对象,那么请确保您还有其他许多其他选项。你也可以纯粹在客户端使用 minimongo,它非常有用,例如:themeteorchef.com/tutorials/…【参考方案3】:

解决此问题的正确方法

解决此问题的正确方法是创建自己的 _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>"
  
]

【讨论】:

以上是关于误解 `each...in` 渲染与反应变量的主要内容,如果未能解决你的问题,请参考以下文章

老铁,你是不是对自动化运维有什么误解?

golang中对“引用传递”的误解

i++与++i的误解

Ghidra 是不是误解了函数调用?

关于Java常见的误解

我对随机化有啥误解吗?