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

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值