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中的数据的访问,提供模版组件化的服务,这部分内容可以查在线的文档来了解,以本文的技术分析为基础,应该不难理解在线文档的确切的含义。