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,
keywords: keywords,
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(),
_context: Ember.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中的數據的訪問,提供模版組件化的服務,這部分內容可以查在線的文檔來了解,以本文的技術分析爲基礎,應該不難理解在線文檔的確切的含義。