Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.2k views
in Technique[技术] by (71.8m points)

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

I am just starting with handlebars and I am trying to do a simple double for loop in order to have all the day's time with 15 minute intervals. A very weird thing is happening where if the child and parent have the same values, the view object is being returned instead. This is my code:

var handlebarsExpress = require('express-handlebars').create({
    helpers: {
        for: function(from, to, incr, block) {
            var accum = '';
            for(var i = from; i <= to; i += incr) {
                accum += block.fn(i);
            }
            return accum;
        }
    }
});

app.engine('handlebars', handlebarsExpress.engine);
app.set('view engine', 'handlebars');

...
...

In the view file (sth.handlebars) i have this:

<div class="row columns large-12">
    {{#for 0 23 1}}
        {{#for 0 45 15}}
            {{log ../this}}
            <span>{{../this}}:{{this}}</span><br>
        {{/for}}
    {{/for}}
</div>

The html output is something like this:

[object Object]:0
0:15
0:30
0:45
1:0
...
...
13:45
14:0
14:15
14:30
14:45
15:0
[object Object]:15
15:30
15:45
16:0
16:15

The log helper reports the following in the terminal:

{ settings: 
   { 'x-powered-by': true,
     etag: 'weak',
     'etag fn': [Function: wetag],
     env: 'development',
     'query parser': 'extended',
     'query parser fn': [Function: parseExtendedQueryString],
     'subdomain offset': 2,
     'trust proxy': false,
     'trust proxy fn': [Function: trustNone],
     view: [Function: View],
     views: '/home/elsa/Projects/rental-borg/project/views',
     'jsonp callback name': 'callback',
     'view engine': 'handlebars',
     port: 8765 },
  flash: { info: undefined, error: undefined },
  _locals: { flash: { info: undefined, error: undefined } },
  cache: false }
0
0
0
1
1
1
1
2
2
2
2
3
3
3
3
4
...
...
13
13
13
13
14
14
14
14
15
{ settings: 
   { 'x-powered-by': true,
     etag: 'weak',
     'etag fn': [Function: wetag],
     env: 'development',
     'query parser': 'extended',
     'query parser fn': [Function: parseExtendedQueryString],
     'subdomain offset': 2,
     'trust proxy': false,
     'trust proxy fn': [Function: trustNone],
     view: [Function: View],
     views: '/home/elsa/Projects/rental-borg/project/views',
     'jsonp callback name': 'callback',
     'view engine': 'handlebars',
     port: 8765 },
  flash: { info: undefined, error: undefined },
  _locals: { flash: { info: undefined, error: undefined } },
  cache: false }
15
15
16
16
16
16
...
23
23
23
23

It is apparent that when this and ../this are equal, ../this actually returns something like ../../this which is the view object itself, I suppose. This happens twice in my specific case, on 00:00 and on 15:15.

Is this a bug?

EDIT: I solved the problem with the new Block Parameters feature of Handlebars 3, but the initial question remains. This is what I did to solve the problem:

.handlebars file:

{{#for 0 23 1 as |hour|}}
    {{#for 0 45 15 as |minute|}}
        <span>{{hour}}:{{minute}}</span>
    {{/for}}
{{/for}}

and the helper:

for: function(from, to, incr, block) {
    var args = [], options = arguments[arguments.length - 1];
    for (var i = 0; i < arguments.length - 1; i++) {
        args.push(arguments[i]);
    }

    var accum = '';
    for(var i = from; i <= to; i += incr) {
        accum += options.fn(i, {data: options.data, blockParams: [i]});
    }
    return accum;
},
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...