../это возвращает объект представления внутри внутреннего цикла, когда родитель и дочерний элемент имеют одинаковое значение

Я только начинаю с handlebars и пытаюсь выполнить простой двойной цикл for, чтобы иметь все дневное время с 15-минутными интервалами. Происходит очень странная вещь: если дочерний и родительский элементы имеют одинаковые значения, вместо них возвращается объект view. Это мой код:

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

...
...

В файле просмотра (sth.handlebars) у меня есть это:

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

Вывод html примерно такой:

[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

Помощник log сообщает в терминал следующее:

{ 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

Очевидно, что когда this и ../this равны, ../this фактически возвращает что-то вроде ../../this, которое, как я полагаю, является самим объектом представления. В моем конкретном случае это происходит дважды, в 00:00 и в 15:15.

Это ошибка?

РЕДАКТИРОВАТЬ: я решил проблему с новой функцией параметров блока в Handlebars 3, но первоначальный вопрос остается. Вот что я сделал, чтобы решить проблему:

.handlebars файл:

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

и помощник:

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;
},

person hytromo    schedule 02.12.2016    source источник


Ответы (1)


Это было интригующее открытие, и мне очень хотелось найти причину такого, казалось бы, странного поведения. Я немного покопался в исходном коде Handlebars и обнаружил, что соответствующий код можно найти на строка 176 файла lib/handlebars/runtime.js .

Handlebars поддерживает стек объектов контекста, верхний объект которого является ссылкой {{this}} в текущем выполняемом блоке шаблона Handlebars. Когда в исполняющемся шаблоне встречаются вложенные блоки, в стек помещается новый объект. Это то, что позволяет использовать следующий код:

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

В первой строке this — это объект с перечисляемым свойством, называемым «список». Во второй строке this — это текущий итерируемый элемент объекта list. Как видно из вопроса, Handlebars позволяет нам использовать ../ для доступа к объектам контекста глубже в стеке. В приведенном выше примере, если мы хотим получить доступ к объекту list из помощника #each, нам пришлось бы использовать {{../this.list}}.

После этого краткого обзора остается вопрос: почему наш стек контекста кажется разорванным, когда значение текущей итерации внешнего цикла for равно значению внутреннего цикла for?

соответствующий код из Handlebars источник следующий:

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

depths — это внутренний стек объектов контекста, context — это объект, переданный в текущий исполняемый блок, а currentDepths — это стек контекста, доступный для исполняемого блока. Как видите, текущий context помещается в доступный стек только если context не приблизительно равно текущей вершине стека, depths[0].

Давайте применим эту логику к коду в вопросе.

Когда контекст внешнего блока #for равен 15, а контекст внутреннего блока #for равен 0:

  • depths равно: [15, {ROOT_OBJECT}] (где {ROOT_OBJECT} означает объект, который был аргументом вызова метода шаблона.)
  • Поскольку 0 != 15, currentDepths становится: [0, 15, {ROOT_OBJECT}].
  • Во внутреннем блоке шаблона #for {{this}} соответствует 0, {{../this}} соответствует 15, а {{../../this}} соответствует {ROOT_OBJECT}.

Однако, когда внешний и внутренний #for блоки имеют контекстное значение 15, мы получаем следующее:

  • depths is: [15, {ROOT_OBJECT}].
  • Потому что 15 == 15, currentDepths = depths = [15, {ROOT_OBJECT}].
  • Во внутреннем блоке #for шаблона {{this}} – это 15, _43  – {ROOT_OBJECT}, а {{../../this}} – undefined.

Вот почему кажется, что ваш {{../this}} пропускает уровень, когда внешний и внутренний блоки #for имеют одинаковое значение. Дело в том, что значение внутреннего #for не помещается в стек контекста!

Именно в этот момент мы должны спросить, почему Handlebars ведет себя таким образом, и определить, является ли это особенностью или ошибкой.

Так получилось, что этот код был добавлен намеренно для решения проблемы, которую пользователи опыт работы с рулем. Проблема может быть продемонстрирована на примере:

Предполагая объект контекста:

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

Пользователи сочли следующий вариант использования нелогичным:

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

Конкретная проблема заключалась в необходимости двойного ../ для доступа к корневому объекту: {{../../name}}, а не {{../name}}. Пользователи считали, что, поскольку объект контекста в {{#if showName}} был объектом config, то при переходе на один уровень, ../, следует получить доступ к «родителю» config — корневому объекту. Причина, по которой потребовались два шага, заключалась в том, что Handlebars создавал объект стека контекста для каждого помощника блока. Это означает, что для доступа к корневому контексту требуется два шага; первый шаг получает контекст {{#with config}}, а второй шаг получает контекст корня.

Был сделан коммит, который предотвращает перемещение объекта контекста в доступный контекст стек, когда новый объект контекста приблизительно равен объекту наверху стека контекста. Ответственный код — это исходный код, который мы рассмотрели выше. Начиная с версии 4.0.0 Handlebars.js, наш пример конфигурации не удастся. Теперь требуется только один ../ шаг.

Возвращаясь к примеру кода в исходном вопросе, причина, по которой 15 из внешнего блока #for определяется как равная 15 во внутреннем блоке #for, связана с тем, как сравниваются числовые типы в JavaScript; два объекта типа Number равны, если каждый из них имеет одинаковое значение. Это отличается от типа Object, для которого два объекта равны, только если они ссылаются на один и тот же объект в памяти. Это означает, что если бы мы переписали исходный код примера, чтобы использовать типы Object вместо типов Number для контекстов, то мы бы никогда не встретились с оператором условного сравнения, и мы всегда имели бы ожидаемый стек контекста внутри наш внутренний блок #for.

Цикл for в нашем помощнике будет обновлен для передачи типа Object в качестве контекста создаваемого им фрейма:

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

И нашему шаблону теперь потребуется доступ к соответствующему свойству этого объекта:

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

С этими изменениями вы должны обнаружить, что ваш код работает так, как вы ожидали.

Несколько субъективно заявлять, является ли это ошибкой с Handlebars. Условие было добавлено намеренно, и результирующее поведение делает то, для чего оно было предназначено. Однако мне трудно представить случай, в котором такое поведение было бы ожидаемым или желательным, когда задействованные контексты являются примитивными, а не типами Object. Было бы разумно, если бы код Handlebars выполнял сравнение типов объектов только. Я думаю, что здесь есть законный повод открыть задачу.

person 76484    schedule 04.12.2016
comment
Отличный ответ, большое спасибо, мне тоже было любопытно, но не было времени исследовать - person hytromo; 05.12.2016