ember.js的render過程分析


write by yinmingjun, 引用請註明。

 

序言

 本文中,我們對ember.js的render過程做一個技術分析,理解在render的過程中route、controller和template+view的角色和分工。

 

route的render過程分析

 

1、render的時序描述

 

先從時序上簡單描述一下route的render的過程: 

router.handleURL(url)    //處理請求

          +collectObjects(router, results, index, objects)   //收集url中的參數信息

                  +handler.deserialize(result.params)            //根據參數產生model,使用其作爲下面的context的值

                          +var model = this.model(params);       //參考下面的代碼,產生router的currentModel

                          +return this.currentModel = model;

          +router.setContext(handler, context)                   //將context保存在route實例之中

                  +handler.context = context;

          +handler.setup(context)                                        //設置使用context完成route的setup過程

                 +this.setupController(controller, context);    //將context設置到controller的model屬性之中

                         +set(controller, 'model', context);

                 +this.renderTemplate(controller, context);   //完成route的render

                         +this.render();                                          //完成route的render

                                 +name = name ? name.replace(/\//g, '.') : this.routeName;           //獲取name

                                 +view = container.lookup('view:' + name),                                      //找name對應的view

                                 +template = container.lookup('template:' + name);                        //找name對應的template

                                 +options = normalizeOptions(this, name, template, options);       //初始化options

                                 +view = setupView(view, container, options);                                 //設置view

                                 +appendView(this, view, options);                                                    //生成view

Ember.run的flush方法觸發回調:

                                 +view.createElement()

                                          +var buffer = this.renderToBuffer();

                                                  +this.beforeRender(buffer);
                                                  +this.render(buffer);
                                                  +this.afterRender(buffer);

                                          +set(this, 'element', buffer.element());


 

 在render的過程中,一個核心的問題是route的context的產生和維護的過程,route的context最終會作爲controller的model屬性的值被設置到controller之中。

 

然後,route根據其routeName查找對應的template和view,最終將template設置到view中,並通過view的render方法將DOM節點創建出來,最後添加到DOM樹上。

 

2、route對model的處理

 

route的model的產生過程是需要特別描述一下。如果提供的參數中,包含形式爲'xxxx_id'的屬性,那麼'xxxx'會被看成是一個model class的類名,而參數值會被看成是其id的值,會嘗試通過model class的find方法查找對應於id的數據。

 

如果沒有上面的字段信息,那麼params會被作爲最終的model返回。

 

route的model方法的代碼:

model: function(params) {
    var match, name, sawParams, value;

    for (var prop in params) {
      if (match = prop.match(/^(.*)_id$/)) {
        name = match[1];
        value = params[prop];
      }
      sawParams = true;
    }

   if (!name && sawParams) { returnparams; }
    else if (!name) { return; }

    var className =classify(name),
        namespace = this.router.namespace,
       modelClass = namespace[className];

    Ember.assert("You used the dynamic segment " + name + "_id in your router, but " + namespace + "." + className + " did not exist and you did not override your route's `model` hook.", modelClass);
    returnmodelClass.find(value);
  },

 

2、route的render過程

 

route的render過程大致分2個階段,第一個階段是構造render的options參數,通過normalizeOptions方法完成。第二個階段是根據options參數來初始化或創建view,通過setupView方法完成;最後,是根據template產生對應的DOM節點,這個過程在appendView方法中完成。

 

setupView方法中,會將當前的controller設置到view的'controller'屬性之中,這實際上就是在設置template的thisContext,因爲view的'controller'屬性的值就是view的'context'屬性的數據來源之一。

 

最後頭通過appendView方法,會使用view的appendTo方法將view添加到指定的rootElement之中(可以設置Application的'rootElement'屬性來指定使用的rootElement,如果沒有指定,使用的是body作爲rootElement)。

 

view的appendTo方法會通過Ember.run.scheduleOnce服務調用創建DOM的方法,最終會調用到view的render方法來創建DOM,整個view的創建過程是先父後子、遞歸向下的創建過程。

 

需要關注的是view的teardown和append的概念,與之類似的是route的enter、setup和exit的概念,表示過程中的時序,在ember.js中很常見。

 

route的相關代碼如下: 

function normalizeOptions(route, name,template, options) {
  options = options || {};
 options.into = options.into ? options.into.replace(/\//g, '.') : parentTemplate(route);
 options.outlet = options.outlet || 'main';
 options.name = name;
 options.template = template
;
 options.LOG_VIEW_LOOKUPS = get(route.router, 'namespace.LOG_VIEW_LOOKUPS');

  Ember.assert("An outlet ("+options.outlet+") was specified but this view will render at the root level.", options.outlet === 'main' || options.into);

  var controller = options.controller, namedController;

  if (options.controller) {
    controller = options.controller;
  } else if (namedController = route.container.lookup('controller:' + name)) {
    controller = namedController;
  } else {
    controller = route.routeName;
  }

  if (typeof controller === 'string') {
    controller = route.container.lookup('controller:' + controller);
  }

 options.controller = controller;

 return options;
}

 

function setupView(view, container, options) {
  if (view) {
    if (options.LOG_VIEW_LOOKUPS) {
      Ember.Logger.info("Rendering " + options.name + " with " + view, { fullName: 'view:' + options.name });
    }
  } else {
   var defaultView = options.into 'view:default' 'view:toplevel';
   view = container.lookup(defaultView);

    if (options.LOG_VIEW_LOOKUPS) {
      Ember.Logger.info("Rendering " + options.name + " with default view " + view, { fullName: 'view:' + options.name });
    }
  }

  if (!get(view, 'templateName')) {
    set(view, 'template', options.template);

    set(view, '_debugTemplateName', options.name);
  }

  set(view, 'renderedName', options.name);
  set(view, 'controller'options.controller);

  return view;
}

 

function appendView(route, view, options) {
  if (options.into) {
    var parentView = route.router._lookupActiveView(options.into);
    route.teardownView = teardownOutlet(parentView, options.outlet);
    parentView.connectOutlet(options.outlet, view);
  } else {
    var rootElement = get(route, 'router.namespace.rootElement');
    // tear down view if one is already rendered
    if (route.teardownView) {
      route.teardownView();
    }
    route.router._connectActiveView(options.name, view);
    route.teardownView = teardownTopLevel(view);
    view.appendTo(rootElement);
  }
}

 

3、view的render過程

 

在view的render過程,ember.js會爲handlebars的template設置運行的上下文,在爲template準備的上下文中,有這麼兩個變量需要特別的關注一下,一個是傳遞給template的context,來自view的'context'屬性,而view的'context'屬性實際上是一個計算屬性,並且不允許ember對其屬性值做緩存,其屬性值來自view的'_context'屬性,而view的'_context'屬性又是一個計算屬性,會優先從view的'controller'屬性獲取值,或從parentView的'_context'屬性獲取值。注意,如果對view的'context'屬性賦值,會改寫默認的context的獲取過程。最終獲取到的context會作爲template的thisContext傳入,也就是說如果在template中引用的變量,默認是從context中檢索的。看到這,我們會清楚,在ember的template中的thisContext很明確就是controller

 

另外一個需要關注的是data中的keywords,view的keywords的初值來自其''templateData''屬性,並填充了'view'、'_view'和'controller'三個成員,這部分的名稱是在template中可以直接使用的名稱,在處理複雜的view的時候很有用,是從template中訪問view上特定數據的窗口。'view'對應的是邏輯(概念層面的)view;'_view'明確對應當前的view;'controller'對應view的controller。

 

view的相關代碼:

  render: function(buffer) {
    // If this view has a layout, it is the responsibility of the
    // the layout to render the view's template. Otherwise, render the template
    // directly.
    var template = get(this, 'layout') || get(this, 'template');

    if (template) {
      var context get(this, 'context');
      var keywords = this.cloneKeywords();
      var output;

      var data = {
        view: this,
        buffer: buffer,
        isRenderData: true,
        keywordskeywords,
        insideGroup: get(this, 'templateData.insideGroup')
      };

      // Invoke the template with the provided template context, which
      // is the view's controller by default. A hash of data is also passed that provides
      // the template with access to the view and render buffer.

      Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function');
      // The template should write directly to the render buffer instead
      // of returning a string.
      output = template(context{ data: data });

      // If the template returned a string instead of writing to the buffer,
      // push the string onto the buffer.
      if (output !== undefined) { buffer.push(output); }
    }
  },

 

  cloneKeywords: function() {
    var templateData = get(this, 'templateData');

    var keywords = templateData ? Ember.copy(templateData.keywords) : {};
    set(keywords'view'get(this, 'concreteView'));
    set(keywords, '_view'this);
    set(keywords'controller'get(this, 'controller'));

    return keywords;
  },

 

  context: Ember.computed(function(key, value) {
    if (arguments.length === 2) {
      set(this, '_context', value);
      return value;
    } else {
      return get(this, '_context');
    }
  }).volatile(),

 

  _contextEmber.computed(function(key) {
    var parentView, controller;

    if (controller = get(this, 'controller')) {
      return controller;
    }

    parentView = this._parentView;
    if (parentView) {
      return get(parentView'_context');
    }

    return null;
  }),

 

註釋,關於綁定的名稱的解析:

前面對ember的data的keywords的解析沒有深入進去,只是提及這是ember對名稱解析的一個上下文環境,這種解釋可能會讓一些人感到迷惑,因此在這裏簡單的說明一下。

 

ember對template中類似{{path}}的內容認爲是一個bind的過程,併爲此封裝了對應的bind方法。由於ember中的上下文環境遠比handlebars中的JSON要複雜,因此ember在上下文的處理上做了特別的支持。在Ember.Handlebars.ViewHelper的contextualizeBindingPath方法中,可以看到對綁定的path的解析過程:

 contextualizeBindingPath: function(path, data) {
    var normalized = Ember.Handlebars.normalizePath(null, path, data);
    if (normalized.isKeyword) {
      return 'templateData.keywords.' + path;
    } else if (Ember.isGlobalPath(path)) {
      return null;
    } else if (path === 'this') {
      return '_parentView.context';
    } else {
      return '_parentView.context.' + path;
    }
  },

 

normalizePath 見下面的代碼:

var normalizePath = Ember.Handlebars.normalizePath = function(root, path, data) {
  var keywords = (data && data.keywords) || {},
      keyword, isKeyword;

  // Get the first segment of the path. For example, if the
  // path is "foo.bar.baz", returns "foo".
  keyword path.split('.', 1)[0];

  // Test to see if the first path is a keyword that has been
  // passed along in the view's data hash. If so, we will treat
  // that object as the new root.
  if (keywords.hasOwnProperty(keyword)) {
    // Look up the value in the template's data hash.
    root = keywords[keyword];
    isKeyword = true;

    // Handle cases where the entire path is the reserved
    // word. In that case, return the object itself.
    if (path === keyword) {
      path = '';
    } else {
      // Strip the keyword from the path and look up
      // the remainder from the newly found root.
      path = path.substr(keyword.length+1);
    }
  }

  return { root: root, path: path, isKeyword: isKeyword };
};

 

其對path的解析過程是先通過normalizePath 檢查是否是keywords中的路徑,如果是,給出其root;其次,檢查是否是全局變量,通過正則表達式'^([A-Z$]|([0-9][A-Z$]))'來測試;再下來,判斷path是否是'this';最後,從其view的context中檢索變量。

 

上面是ember對綁定名稱的大致處理過程。

 

小結

 

上面,對ember的render的過程做了一個分析,route是render的發起者,controller提供數據上下文,而view+template提供最終的DOM的模版,整個過程十分精緻。

 

數據的來源需要認真的梳理一下,如果存在route,那麼數據會來自route的model,並會填充到controller的'model'屬性之中。而在view的render的過程中,會將template的thisContext設置成controller,並填充data的keywords對象,對template中的數據訪問提供支持。

 

ember提供了很多helper,對template的書寫提供支持,從根本上來說,都是封裝、簡化對controller的成員和data的keywords中的數據的訪問,提供模版組件化的服務,這部分內容可以查在線的文檔來了解,以本文的技術分析爲基礎,應該不難理解在線文檔的確切的含義。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章