../this returns the view object inside inner loop when the parent and child have the same value

不打扰是莪最后的温柔 提交于 2019-12-02 04:19:14

This was an intriguing discovery and I really wanted to find the reason for such seemingly strange behavior. I did some digging into the Handlebars source code and I found the relevant code was to be found on line 176 of lib/handlebars/runtime.js.

Handlebars maintains a stack of context objects, the top object of which is the {{this}} reference within the currently executing Handlebars template block. As nested blocks are encountered in the executing template a new object is pushed to the stack. This is what permits code like the following:

{{#each this.list}}
    {{this}}
{{/each}}

On the first line, this is an object with an enumerable property called "list". On line two, this is the currently iterated item of the list object. As the question points out, Handlebars allows us to use ../ to access context objects deeper in the stack. In the example above, if we want to access the list object from within the #each helper, we would have had to use {{../this.list}}.

With this brief summary out of the way, the question remains: Why does our context stack appear to break when the value of the current iteration of the outer for loop is equal to that of the inner for loop?

The relevant code from the Handlebars source is the following:

let currentDepths = depths;
if (depths && context != depths[0]) {
  currentDepths = [context].concat(depths);
}

depths is the internal stack of context objects, context is the object passed to the currently executing block, and currentDepths is the context stack that is made available to the executing block. As you can see, the current context is pushed to the available stack only if context is not loosely equal to the current top of the stack, depths[0].

Let's apply this logic to the code in the question.

When the context of the outer #for block is 15 and the context of the inner #for block is 0:

  • depths is: [15, {ROOT_OBJECT}] (Where {ROOT_OBJECT} means the object that was the argument to the template method call.)
  • Becuase 0 != 15, currentDepths becomes: [0, 15, {ROOT_OBJECT}].
  • Within the inner #for block of the template, {{this}} is 0, {{../this}} is 15 and {{../../this}} is {ROOT_OBJECT}.

However, when the outer and inner #for blocks each have a context value of 15, we get the following:

  • depths is: [15, {ROOT_OBJECT}].
  • Because 15 == 15, currentDepths = depths = [15, {ROOT_OBJECT}].
  • Within the inner #for block of the template, {{this}} is 15, {{../this}} is {ROOT_OBJECT}, and {{../../this}} is undefined.

This is why it appears that your {{../this}} skips a level when the outer and inner #for blocks have the same value. It is actually because the value of the inner #for is not pushed to the context stack!

It is at this point that we should ask why Handlebars behaves this way and to determine whether this is a feature or a bug.

It so happens that this code was added intentionally to solve an issue that users were experiencing with Handlebars. The issue can be demonstrated by way of example:

Assuming a context object of:

{
    config: {
        showName: true
    },
    name: 'John Doe'
}

Users found the following use case to be counter-intuitive:

{{#with config}}
    {{#if showName}}
        {{../../name}}
    {{/if}}
{{/with}}

The specific issue was with the necessity for the double ../ to access the root object: {{../../name}} rather than {{../name}}. Users felt that since the context object within {{#if showName}} was the config object, then stepping-up one level, ../, should access the "parent" of config - the root object. The reason that two steps were necessary was because Handlebars was creating a context stack object for each block helper. This means that two steps are required to get to the root context; the first step gets the context of {{#with config}}, and the second step gets the context of the root.

A commit was made that prevents the pushing of a context object to the available context stack when the new context object is loosely equal to the object at the top of the context stack. The responsible code is the source code we looked at above. As of version 4.0.0 of Handlebars.js, our config example will fail. It now requires only a single ../ step.

Getting back to the code example in the original question, the reason that the 15 from the outer #for block is determined as equal to the 15 in the inner #for block is due to how number types are compared in JavaScript; two objects of the Number type are equal if they each have the same value. This is in contrast to the Object type, for which two objects are equal only if they reference the same object in memory. This means that if we re-wrote the original example code to use Object types instead of Number types for the contexts, then we would never meet the conditional comparison statement and we would always have the expected context stack within our inner #for block.

The for loop in our helper would be updated to pass an Object type as the context of the frame it creates:

for(var i = from; i <= to; i += incr) {
    accum += block.fn({ value: i });
}

And our template would now need to access the relevant property of this object:

{{log ../this.value}}
<span>{{../this.value}}:{{this.value}}</span><br>

With these edits, you should find your code performing as you expected.

It's somewhat subjective to declare whether or not this is a bug with Handlebars. The conditional was added intentionally and the resultant behavior does what it was intended to do. However, I find it hard to imagine a case in which this behavior would be expected or desirable when the contexts involved are primitives and not Object types. It might be reasonable for the Handlebars code to be made to do the comparison on Object types only. I think there is a legitimate case here to open an issue.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!