Пора запуститься в браузере
Nov 23, 2016
4 minutes read

Сегодня, наконец, хочу увидеть что-нибудь в браузере.

Moй первый JavaScript

Для начала, могу сказать что я вообще не знаю как работают scopes в JavaScript (и моя предыдущая проблема с переменной цикла без var тому пример), я решил для начала вывести DOM в консоль, и для этого погуглил функцию вывода n пробелов (я уже веду себя как настоящий JS-разработчик, не начав программировать). Реализация вылезла следующая:

function space(num) {
  return new Array(num + 1).join(" ");
}

Тут я полностью охуел и понял, что JS-программистом мне не быть никогда. Я просто не смогу такое придумать - в прошлом я не протоптал ни одной тропинки в мозгу, которая могла бы привести к такому решению задачи. И, честно говоря, протаптывать такие тропинки (учиться) мне пока не хочется. Поэтому в пизду - будем сразу вываливать DOM.

mergeInto(LibraryManager.library, {

  $renderElement: function (p) {
    p = p | 0;

    const tags = [
      "DOCUMENT_ROOT", "TEXT", "a", "button", "div", "h1", "header", "hr", "input",
      "label", "li", "p", "section", "span", "table", "tbody", "td", "tr", "ul"
    ];

    const A = ["TEXT", "class", "for", "placeholder", "type", "value", "width", "title"];

    var t = HEAP32[((p)>>2)];
    p += 4;
    var kids = HEAP32[((p)>>2)];
    p += 4;
    var attrs = HEAP32[((p)>>2)];
    p += 4;

    var e;
    switch(t) {
      case 0:
        console.log("render DOCUMENT_ROOT");
        e = createTextNode("error: DOCUMENT ROOT HERE");
        break;
      case 1:
        p += 4; //attrid == text
        e = document.createTextNode(Pointer_stringify(HEAP32[((p)>>2)]));
        p += 4;
        break;
      default:
        e = document.createElement(tags[t]);
        for (var i=0; i<attrs; i++) {
          var a = HEAP32[((p)>>2)];
          p += 4;
          var v = HEAP32[((p)>>2)];
          p += 4;
          e.setAttribute(A[a], Pointer_stringify(v));
        }
        for (var i=0; i<kids; i++) {
          var kid = HEAP32[((p)>>2)];
          p += 4;          
          e.appendChild( renderElement(kid) );
        }
    }
    return e;
  },

  JSrender: function(p) {
    p = p | 0;
    var root = document.getElementById("app");
    console.log("Hello from emscripten: ", root.childElementCount);
    var app = renderElement(p, 0);
    if (root.childElementCount == 0) {
      root.appendChild( app );
    }          
  },
  JSrender__deps: ["$renderElement"],
})

Я намеренно вываливаю DOM на экран лишь один раз - хочу замерить генерацию JS DOM off-screen. Ну что-ж: 18.5 ms на итерацию в Safari (это притом что asm.js часть справилась за 4.2ms). Chrome: хром вообще охуел от того что я ему сунул, пришлось считать на 250 итерациях - 75ms на итерацию. Это вообще ни в какие ворота, но как бы радует что asm.js часть все делает за 7.5ms.

Из “тяжелого” у меня в цикле только Pointer_stringify, который работает с UTF-8, пробую заменить на более быстрый AsciiToString, что, впрочем не совсем верно, но надо посмотреть. Safari - 17.1ms. Выйграл миллисекунду, и понятно, что дело не в “математике”, а в большом количестве создаваемых и собираемых сборщиком мусора нод. Вот он корень зла, и подтверждение того, что вроде бы Virtual DOM - нужная вещь. Для проверки, отключил все операции с JS DOM - 6.9ms на итерацию.

Ну что-ж остался основной ингридиент - diff/patch, и наступит момент истины.

Момент истины

Итак, примитивный (не значит что медленный) diff реализован, запускаю без UI. На, нахуй! 1.3ms на итерацию нативный код, 7.8ms на итерацию в V8 (node.js). Это генерация модели + посторение DOM + простроение Diff. Какой-то запас еще есть. Осталось патчить DOM в браузере.

21:54 центрально-европейского времени

Готово применение патча в браузере - 9.8ms на итерацию! в Safari. Рано радоваться, поскольку итерация не отрисовывается (я не передаю управление в main loop), отрисовывается только результат последней. Но время обнадеживает: думаю что до 8ms эта история оптимизируется, 8ms оставим на отрисовку, и здравствуй 60 FPS. Правда Chrome дает 14ms, но будем смотреть - сейчас все довольно схематично.

Выглядит примерно так:

  JSrender: function(p) {
    p = p | 0;
    var element = document.getElementById("app");
    //console.log("RENDER: ", p);
    // p is PATCH address
    while(true) {
      var cmd = HEAP32[((p)>>2)] | 0;
      //console.log("cmd: ", cmd);
      p += 4;
      switch(cmd) {
        case 0: // HALT
          return;
        case 1: // NAV_UP
          var times = HEAP32[((p)>>2)] | 0;
          p += 4;
          for(var i=0;i<times;i++)
            element = element.parentNode;
          break;
        case 2: // NAV_KID
          var nkid = HEAP32[((p)>>2)] | 0;
          p += 4;
          element = element.children[nkid];
          break;
        case 3: // NAV_PARENT
          element = element.parentNode;
          break;
        case 4: // NAV_GRAND_PARENT
          element = element.parentNode.parentNode;
          break;
        case 5: // NAV_FIRST_CHILD
          element = element.firstChild;
          break;
        case 6: // NAV_SECOND_CHILD
          element = element.firstChild.nextSibling;
          break;
        case 7: // NAV_NEXT_SIBLING
          element = element.nextSibling;
          break;
        case 8: // APPEND
          var e = HEAP32[((p)>>2)] | 0;
          p += 4;          
          element.appendChild(renderElement(e));
          break;
        case 9: // REMOVE_LAST
          element.removeChild(element.lastChild);
          break;
        case 10: // REMOVE_LAST_MANY
          var times = HEAP32[((p)>>2)] | 0;
          p += 4;   
          for(var i=0;i<times;i++)
            element.removeChild(element.lastChild);
          break;
        case 11: // ATTR_SET
          var attr = HEAP32[((p)>>2)] | 0;
          p += 4;   
          var value = AsciiToString(HEAP32[((p)>>2)]);
          p += 4;
          if(attr == 0) {
            //console.log("current value: ", element.nodeValue);
            //console.log("text: ", value);
            element.nodeValue = value;
          } else {
            element.setAttribute(attributeNames[attr], value);
          }
          break;
        case 12: // ATTR_REMOVE
          var attr = HEAP32[((p)>>2)] | 0;
          p += 4;   
          element.removeAttribute(attributeNames[attr]);
          break;
        default:
          console.log("SHIT HAPPENS");
          return;
      }
    }
  }

Завтра лечу в Прагу, а в дороге пока не получается работать. А пока результаты под тегом day-6 https://github.com/platoff/faxma/tree/day-6.


Назад, к записям


comments powered by Disqus