mustache是一个很轻的前端模板引擎,因为之前接手的项目用了这个模板引擎,自己就也继续用了一会觉得还不错,最近项目相对没那么忙,于是就抽了点时间看了一下这个的源码。源码很少,也就只有六百多行,所以比较容易阅读。做前端的话,还是要多看优秀源码,这个模板引擎的知名度还算挺高,所以其源码也肯定有值得一读的地方。

  本人前端小菜,写这篇博文纯属自己记录一下以便做备忘,同时也想分享一下,希望对园友有帮助。若解读中有不当之处,还望指出。

  如果没用过这个模板引擎,建议 去 https://github.com/janl/mustache.js/ 试着用一下,上手很容易。

  摘取部分官方demo代码(当然还有其他基本的list遍历输出): 

  1. 数据:
  2. {
  3. "name": {
  4. "first": "Michael",
  5. "last": "Jackson"
  6. },
  7. "age": "RIP"
  8. }
  9.  
  10. 模板写法:
  11. * {{name.first}} {{name.last}}
  12. * {{age}}
  13.  
  14. 渲染效果:
  15. * Michael Jackson
  16. * RIP

  OK,那就开始来解读它的源码吧:

  首先先看下源码中的前面多行代码:

  1. var Object_toString = Object.prototype.toString;
  2. var isArray = Array.isArray || function (object) {
  3. return Object_toString.call(object) === '[object Array]';
  4. };
  5.  
  6. function isFunction(object) {
  7. return typeof object === 'function';
  8. }
  9.  
  10. function escapeRegExp(string) {
  11. return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
  12. }
  13.  
  14. // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
  15. // See https://github.com/janl/mustache.js/issues/189
  16. var RegExp_test = RegExp.prototype.test;
  17. function testRegExp(re, string) {
  18. return RegExp_test.call(re, string);
  19. }
  20.  
  21. var nonSpaceRe = /\S/;
  22. function isWhitespace(string) {
  23. return !testRegExp(nonSpaceRe, string);
  24. }
  25.  
  26. var entityMap = {
  27. "&": "&",
  28. "<": "&lt;",
  29. ">": "&gt;",
  30. '"': '&quot;',
  31. "'": ''',
  32. "/": '/'
  33. };
  34.  
  35. function escapeHtml(string) {
  36. return String(string).replace(/[&<>"'\/]/g, function (s) {
  37. return entityMap[s];
  38. });
  39. }
  40.  
  41. var whiteRe = /\s*/; //匹配0个或以上空格
  42. var spaceRe = /\s+/; //匹配一个或以上空格
  43. var equalsRe = /\s*=/; //匹配0个或者以上空格再加等于号
  44. var curlyRe = /\s*\}/; //匹配0个或者以上空格再加}符号
  45. var tagRe = /#|\^|\/|>|\{|&|=|!/; //匹配 #,^,/,>,{,&,=,!

  这些都比较简单,都是一些为后面主函数准备的工具函数,包括

  · toString和test函数的简易封装

  · 判断对象类型的方法

  · 字符过滤正则表达式关键符号的方法

  · 判断字符为空的方法

  · 转义字符映射表 和 通过映射表将html转码成非html的方法

  · 一些简单的正则。

  一般来说mustache在js中的使用方法都是如下:

  1. var template = $('#template').html();
  2. Mustache.parse(template); // optional, speeds up future uses
  3. var rendered = Mustache.render(template, {name: "Luke"});
  4. $('#target').html(rendered);

  所以,我们接下来就看下parse的实现代码,我们在源码里搜索parse,于是找到这一段

  1. mustache.parse = function (template, tags) {
  2. return defaultWriter.parse(template, tags);
  3. };

  再通过找defaultWriter的原型Writer类后,很容易就可以找到该方法的核心所在,就是parseTemplate方法,这是一个解析器,不过在看这个方法之前,还得先看一个类:Scanner,顾名思义,就是扫描器,源码如下

  1. /**
  2. * 简单的字符串扫描器,用于扫描获取模板中的模板标签
  3. */
  4. function Scanner(string) {
  5. this.string = string; //模板总字符串
  6. this.tail = string; //模板剩余待扫描字符串
  7. this.pos = 0; //扫描索引,即表示当前扫描到第几个字符串
  8. }
  9.  
  10. /**
  11. * 如果模板被扫描完则返回true,否则返回false
  12. */
  13. Scanner.prototype.eos = function () {
  14. return this.tail === "";
  15. };
  16.  
  17. /**
  18. * 扫描的下一批的字符串是否匹配re正则,如果不匹配或者match的index不为0;
  19. * 即例如:在"abc{{"中扫描{{结果能获取到匹配,但是index为4,所以返回"";如果在"{{abc"中扫描{{能获取到匹配,此时index为0,即返回{{,同时更新扫描索引
  20. */
  21. Scanner.prototype.scan = function (re) {
  22. var match = this.tail.match(re);
  23.  
  24. if (!match || match.index !== 0)
  25. return '';
  26.  
  27. var string = match[0];
  28.  
  29. this.tail = this.tail.substring(string.length);
  30. this.pos += string.length;
  31.  
  32. return string;
  33. };
  34.  
  35. /**
  36. * 扫描到符合re正则匹配的字符串为止,将匹配之前的字符串返回,扫描索引设为扫描到的位置
  37. */
  38. Scanner.prototype.scanUntil = function (re) {
  39. var index = this.tail.search(re), match;
  40.  
  41. switch (index) {
  42. case -1:
  43. match = this.tail;
  44. this.tail = "";
  45. break;
  46. case 0:
  47. match = "";
  48. break;
  49. default:
  50. match = this.tail.substring(0, index);
  51. this.tail = this.tail.substring(index);
  52. }
  53.  
  54. this.pos += match.length;
  55. return match;
  56. };

  扫描器,就是用来扫描字符串,在mustache用于扫描模板代码中的模板标签。扫描器中就三个方法:

  eos:判断当前扫描剩余字符串是否为空,也就是用于判断是否扫描完了

  scan:仅扫描当前扫描索引的下一堆匹配正则的字符串,同时更新扫描索引,注释里我也举了个例子

  scanUntil:扫描到匹配正则为止,同时更新扫描索引

  看完扫描器,我们再回归一下,去看一下解析器parseTemplate方法,模板的标记标签默认为"{{}}",虽然也可以自己改成其他,不过为了统一,所以下文解读的时候都默认为{{}}:

  1. function parseTemplate(template, tags) {
  2. if (!template)
  3. return [];
  4.  
  5. var sections = []; // 用于临时保存解析后的模板标签对象
  6. var tokens = []; // 保存所有解析后的对象
  7. var spaces = []; // 保存空格对象在tokens里的索引
  8. var hasTag = false;
  9. var nonSpace = false;
  10.  
  11. // 去除保存在tokens里的空格标记
  12. function stripSpace() {
  13. if (hasTag && !nonSpace) {
  14. while (spaces.length)
  15. delete tokens[spaces.pop()];
  16. } else {
  17. spaces = [];
  18. }
  19.  
  20. hasTag = false;
  21. nonSpace = false;
  22. }
  23.  
  24. var openingTagRe, closingTagRe, closingCurlyRe;
  25.  
  26. //将tag转成正则,默认的tag为{{和}},所以转成匹配{{的正则,和匹配}}的正则,已经匹配}}}的正则(因为mustache的解析中如果是{{{}}}里的内容则被解析为html代码)
  27. function compileTags(tags) {
  28. if (typeof tags === 'string')
  29. tags = tags.split(spaceRe, 2);
  30.  
  31. if (!isArray(tags) || tags.length !== 2)
  32. throw new Error('Invalid tags: ' + tags);
  33.  
  34. openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
  35. closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
  36. closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
  37. }
  38.  
  39. compileTags(tags || mustache.tags);
  40.  
  41. var scanner = new Scanner(template);
  42.  
  43. var start, type, value, chr, token, openSection;
  44. while (!scanner.eos()) {
  45. start = scanner.pos;
  46.  
  47. // Match any text between tags.
  48. // 开始扫描模板,扫描至{{时停止扫描,并且将此前扫描过的字符保存为value
  49. value = scanner.scanUntil(openingTagRe);
  50.  
  51. if (value) {
  52. //遍历{{前的字符
  53. for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
  54. chr = value.charAt(i);
  55.  
  56. //如果当前字符为空格,则用spaces数组记录保存至tokens里的索引
  57. if (isWhitespace(chr)) {
  58. spaces.push(tokens.length);
  59. } else {
  60. nonSpace = true;
  61. }
  62.  
  63. tokens.push([ 'text', chr, start, start + 1 ]);
  64.  
  65. start += 1;
  66.  
  67. // 如果遇到换行符,则将前一行的空格去掉
  68. if (chr === '\n')
  69. stripSpace();
  70. }
  71. }
  72.  
  73. // 判断下一个字符串中是否有{[,同时更新扫描索引至{{后一位
  74. if (!scanner.scan(openingTagRe))
  75. break;
  76.  
  77. hasTag = true;
  78.  
  79. //扫描标签类型,是{{#}}还是{{=}}还是其他
  80. type = scanner.scan(tagRe) || 'name';
  81. scanner.scan(whiteRe);
  82.  
  83. //根据标签类型获取标签里的值,同时通过扫描器,刷新扫描索引
  84. if (type === '=') {
  85. value = scanner.scanUntil(equalsRe);
  86.  
  87. //使扫描索引更新为\s*=后
  88. scanner.scan(equalsRe);
  89.  
  90. //使扫描索引更新为}}后,下面同理
  91. scanner.scanUntil(closingTagRe);
  92. } else if (type === '{') {
  93. value = scanner.scanUntil(closingCurlyRe);
  94. scanner.scan(curlyRe);
  95. scanner.scanUntil(closingTagRe);
  96. type = '&';
  97. } else {
  98. value = scanner.scanUntil(closingTagRe);
  99. }
  100.  
  101. // 匹配模板闭合标签即}},如果没有匹配到则抛出异常,同时更新扫描索引至}}后一位,至此时即完成了一个模板标签{{#tag}}的扫描
  102. if (!scanner.scan(closingTagRe))
  103. throw new Error('Unclosed tag at ' + scanner.pos);
  104.  
  105. // 将模板标签也保存至tokens数组中
  106. token = [ type, value, start, scanner.pos ];
  107. tokens.push(token);
  108.  
  109. //如果type为#或者^,也将tokens保存至sections
  110. if (type === '#' || type === '^') {
  111. sections.push(token);
  112. } else if (type === '/') { //如果type为/则说明当前扫描到的模板标签为{{/tag}},则判断是否有{{#tag}}与其对应
  113.  
  114. // 检查模板标签是否闭合,{{#}}是否与{{/}}对应,即临时保存在sections最后的{{#tag}},是否跟当前扫描到的{{/tag}}的tagName相同
  115. // 具体原理:扫描第一个tag,sections为[{{#tag}}],扫描第二个后sections为[{{#tag}} , {{#tag2}}]以此类推扫描多个开始tag后,sections为[{{#tag}} , {{#tag2}} ... {{#tag}}]
  116. // 所以接下来如果扫描到{{/tag}}则需跟sections的最后一个相对应才能算标签闭合。同时比较后还需将sections的最后一个删除,才能进行下一轮比较
  117. openSection = sections.pop();
  118.  
  119. if (!openSection)
  120. throw new Error('Unopened section "' + value + '" at ' + start);
  121.  
  122. if (openSection[1] !== value)
  123. throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
  124. } else if (type === 'name' || type === '{' || type === '&') {
  125. nonSpace = true;
  126. } else if (type === '=') {
  127. compileTags(value);
  128. }
  129. }
  130.  
  131. // 保证sections里没有对象,如果有对象则说明标签未闭合
  132. openSection = sections.pop();
  133.  
  134. if (openSection)
  135. throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
  136.  
  137. //在对tokens里的数组对象进行筛选,进行数据的合并及剔除
  138. return nestTokens(squashTokens(tokens));
  139. }

  解析器就是用于解析模板,将html标签即内容与模板标签分离,整个解析原理为遍历字符串,通过最前面的那几个正则以及扫描器,将普通html和模板标签{{#tagName}}{{/tagName}}{{^tagName}}扫描出来并且分离,将每一个{{#XX}}、{{^XX}}、{{XX}}、{{/XX}}还有普通不含模板标签的html等全部抽象为数组保存至tokens。

  tokens的存储方式为:

    

  token[0]为token的type,可能值为:# ^ / & name text等分别表示{{#XX}}、{{^XX}}、{{/XX}}、{{&XX}}、{{XX}}、以及html文本等

  token[1]为token的内容,如果是模板标签,则为标签名,如果为html文本,则是html的文本内容

  token[2],token[3]为匹配开始位置和结束位置,后面将数据结构转换成树形结构的时候还会有token[4]和token[5]

  具体的扫描方式为以{{}}为扫描依据,利用扫描器的scanUtil方法,扫描到{{后停止,通过scanner的scan方法匹配tagRe正则(/#|\^|\/|>|\{|&|=|!/)从而判断出{{后的字符是否为模板关键字符,再用scanUtil方法扫描至}}停止,获取获取到的内容,此时就可以获取到tokens[0]、tokens[1]、tokens[2],再调用一下scan更新扫描索引,就可以获取到token[3]。同理,下面的字符串也是如此扫描,直至最后一行return nestTokens(squashTokens(tokens))之前,扫描出来的结果为,模板标签为一个token对象,如果是html文本,则每一个字符都作为一个token对象,包括空格字符。这些数据全部按照扫描顺序保存在tokens数组里,不仅杂乱而且量大,所以最后一行代码中的squashTokens方法和nestTokens用来进行数据筛选以及整合。

  首先来看下squashTokens方法,该方法主要是整合html文本,对模板标签的token对象没有进行处理,代码很简单,就是将连续的html文本token对象整合成一个。

  1. function squashTokens(tokens) {
  2. var squashedTokens = [];
  3.  
  4. var token, lastToken;
  5. for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  6. token = tokens[i];
  7.  
  8. if (token) {
  9. if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
  10. lastToken[1] += token[1];
  11. lastToken[3] = token[3];
  12. } else {
  13. squashedTokens.push(token);
  14. lastToken = token;
  15. }
  16. }
  17. }
  18.  
  19. return squashedTokens;
  20. }

  整合完html文本的token对象后,就通过nestTokens进行进一步的整合,遍历tokens数组,如果当前token为{{#XX}}或者{{^XX}}都说明是模板标签的开头标签,于是把它的第四个参数作为收集器存为collector进行下一轮判断,如果当前token为{{/}}则说明遍历到了模板闭合标签,取出其相对应的开头模板标签,再给予其第五个值为闭合标签的开始位置。如果是其他,则直接扔进当前的收集器中。如此遍历完后,tokens里的token对象就被整合成了树形结构

  1. function nestTokens(tokens) {
  2. var nestedTokens = [];
  3.  
  4. //collector是个收集器,用于收集当前标签子元素的工具
  5. var collector = nestedTokens;
  6. var sections = [];
  7.  
  8. var token, section;
  9. for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  10. token = tokens[i];
  11.  
  12. switch (token[0]) {
  13. case '#':
  14. case '^':
  15. collector.push(token);
  16. sections.push(token); //存放模板标签的开头对象
  17.  
  18. collector = token[4] = []; //此处可分解为:token[4]=[];collector = token[4];即将collector指向当前token的第4个用于存放子对象的容器
  19.  
  20. break;
  21. case '/':
  22. section = sections.pop(); //当发现闭合对象{{/XX}}时,取出与其相对应的开头{{#XX}}或{{^XX}}
  23. section[5] = token[2];
  24. collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; //如果sections未遍历完,则说明还是有可能发现{{#XX}}开始标签,所以将collector指向最后一个sections中的最后一个{{#XX}}
  25. break;
  26. default:
  27. collector.push(token); //如果是普通标签,扔进当前的collector中
  28. }
  29. }
  30.  
  31. //最终返回的数组即为树形结构
  32. return nestedTokens;
  33. }
  1.   经过两个方法的筛选和整合,最终出来的数据就是精简的树形结构数据:

 

   至此,整个解析器的代码就分析完了,然后我们来分析渲染器的代码。

   parseTemplate将模板代码解析为树形结构的tokens数组,按照平时写mustache的习惯,用完parse后,就是直接用 xx.innerHTML = Mustache.render(template , obj),因为此前会先调用parse解析,解析的时候会将解析结果缓存起来,所以当调用render的时候,就会先读缓存,如果缓存里没有相关解析数据,再调用一下parse进行解析。

  1. Writer.prototype.render = function (template, view, partials) {
  2. var tokens = this.parse(template);
  3.  
  4. //将传进来的js对象实例化成context对象
  5. var context = (view instanceof Context) ? view : new Context(view);
  6. return this.renderTokens(tokens, context, partials, template);
  7. };

  可见,进行最终解析的renderTokens函数之前,还要先把传进来的需要渲染的对象数据进行处理一下,也就是把数据包装成context对象。所以我们先看下context部分的代码:

  1. function Context(view, parentContext) {
  2. this.view = view == null ? {} : view;
  3. this.cache = { '.': this.view };
  4. this.parent = parentContext;
  5. }
  6.  
  7. /**
  8. * 实例化一个新的context对象,传入当前context对象成为新生成context对象的父对象属性parent中
  9. */
  10. Context.prototype.push = function (view) {
  11. return new Context(view, this);
  12. };
  13.  
  14. /**
  15. * 获取name在js对象中的值
  16. */
  17. Context.prototype.lookup = function (name) {
  18. var cache = this.cache;
  19.  
  20. var value;
  21. if (name in cache) {
  22. value = cache[name];
  23. } else {
  24. var context = this, names, index;
  25.  
  26. while (context) {
  27. if (name.indexOf('.') > 0) {
  28. value = context.view;
  29. names = name.split('.');
  30. index = 0;
  31.  
  32. while (value != null && index < names.length)
  33. value = value[names[index++]];
  34. } else if (typeof context.view == 'object') {
  35. value = context.view[name];
  36. }
  37.  
  38. if (value != null)
  39. break;
  40.  
  41. context = context.parent;
  42. }
  43.  
  44. cache[name] = value;
  45. }
  46.  
  47. if (isFunction(value))
  48. value = value.call(this.view);
  49.  
  50. console.log(value)
  51. return value;
  52. };

  context部分代码也是很少,context是专门为树形结构提供的工厂类,context的构造函数中,this.cache = {'.':this.view}是把需要渲染的数据缓存起来,同时在后面的lookup方法中,把需要用到的属性值从this.view中剥离到缓存的第一层来,也就是lookup方法中的cache[name] = value,方便后期查找时先在缓存里找

  context的push方法比较简单,就是形成树形关系,将新的数据传进来封装成新的context对象,并且将新的context对象的parent值指向原来的context对象。

  context的lookup方法,就是获取name在渲染对象中的值,我们一步一步来分析,先是判断name是否在cache中的第一层,如果不在,才进行深度获取。然后将进行一个while循环:

  先是判断name是否有.这个字符,如果有点的话,说明name的格式为XXX.XX,也就是很典型的键值的形式。然后就将name通过.分离成一个数组names,通过while循环遍历names数组,在需要渲染的数据中寻找以name为键的值。

  如果name没有.这个字符,说明是一个单纯的键,先判断一下需要渲染的数据类型是否为对象,如果是,就直接获取name在渲染的数据里的值。

  通过两层判断,如果没找到符合的值,则将当前context置为context的父对象,再对其父对象进行寻找,直至找到value或者当前context无父对象为止。如果找到了,将值缓存起来。

  看完context类的代码,就可以看渲染器的代码了:

  1. Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
  2. var buffer = '';
  3.  
  4. var self = this;
  5. function subRender(template) {
  6. return self.render(template, context, partials);
  7. }
  8.  
  9. var token, value;
  10. for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  11. token = tokens[i];
  12.  
  13. switch (token[0]) {
  14. case '#':
  15. value = context.lookup(token[1]); //获取{{#XX}}中XX在传进来的对象里的值
  16.  
  17. if (!value)
  18. continue; //如果不存在则跳过
  19.  
  20. //如果为数组,说明要复写html,通过递归,获取数组里的渲染结果
  21. if (isArray(value)) {
  22. for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
  23. //获取通过value渲染出的html
  24. buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
  25. }
  26. } else if (typeof value === 'object' || typeof value === 'string') {
  27. //如果value为对象,则不用循环,根据value进入下一次递归
  28. buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
  29. } else if (isFunction(value)) {
  30. //如果value是方法,则执行该方法,并且将返回值保存
  31. if (typeof originalTemplate !== 'string')
  32. throw new Error('Cannot use higher-order sections without the original template');
  33.  
  34. // Extract the portion of the original template that the section contains.
  35. value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
  36.  
  37. if (value != null)
  38. buffer += value;
  39. } else {
  40. buffer += this.renderTokens(token[4], context, partials, originalTemplate);
  41. }
  42.  
  43. break;
  44. case '^':
  45. //如果为{{^XX}},则说明要当value不存在(null、undefine、0、'')或者为空数组的时候才触发渲染
  46. value = context.lookup(token[1]);
  47.  
  48. // Use JavaScript's definition of falsy. Include empty arrays.
  49. // See https://github.com/janl/mustache.js/issues/186
  50. if (!value || (isArray(value) && value.length === 0))
  51. buffer += this.renderTokens(token[4], context, partials, originalTemplate);
  52.  
  53. break;
  54. case '>':
  55. //防止对象不存在
  56. if (!partials)
  57. continue;
  58. //>即直接读取该值,如果partials为方法,则执行,否则获取以token为键的值
  59. value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
  60.  
  61. if (value != null)
  62. buffer += this.renderTokens(this.parse(value), context, partials, value);
  63.  
  64. break;
  65. case '&':
  66. //如果为&,说明该属性下显示为html,通过lookup方法获取其值,然后叠加到buffer中
  67. value = context.lookup(token[1]);
  68.  
  69. if (value != null)
  70. buffer += value;
  71.  
  72. break;
  73. case 'name':
  74. //如果为name说明为属性值,不作为html显示,通过mustache.escape即escapeHtml方法将value中的html关键词转码
  75. value = context.lookup(token[1]);
  76.  
  77. if (value != null)
  78. buffer += mustache.escape(value);
  79.  
  80. break;
  81. case 'text':
  82. //如果为text,则为普通html代码,直接叠加
  83. buffer += token[1];
  84. break;
  85. }
  86. }
  87.  
  88. return buffer;
  89. };

   原理还是比较简单的,因为tokens的树形结构已经形成,渲染数据就只需要按照树形结构的顺序进行遍历输出就行了。

  不过还是大概描述一下,buffer是用来存储渲染后的数据,遍历tokens数组,通过switch判断当前token的类型:

  如果是#,先获取到{{#XX}}中的XX在渲染对象中的值value,如果没有该值,直接跳过该次循环,如果有,则判断value是否为数组,如果为数组,说明要复写html,再遍历value,通过递归获取渲染后的html数据。如果value为对象或者普通字符串,则不用循环输出,直接获取以value为参数渲染出的html,如果value为方法,则执行该方法,并且将返回值作为结果叠加到buffer中。如果是^,则当value不存在或者value是数组且数组为空的时候,才获取渲染数据,其他判断都是差不多。

  通过这堆判断以及递归调用,就可以把数据完成渲染出来了。

  至此,Mustache的源码也就解读完了,Mustache的核心就是一个解析器加一个渲染器,以非常简洁的代码实现了一个强大的模板引擎。

  1.  

Mustache.js前端模板引擎源码解读的更多相关文章

  1. jquery template.js前端模板引擎

    作为现代应用,ajax的大量使用,使得前端工程师们日常的开发少不了拼装模板,渲染模板 在刚有web的时候,前端与后端的交互,非常直白,浏览器端发出URL,后端返回一张拼好了的HTML串.浏览器对其进行 ...

  2. 前端日报-20160527 underscore 源码解读

    underscore 源码解读 API文档浏览器 JavaScript 中加号操作符细节 抛弃 jQuery,拥抱原生 JS 从 0 开始学习 GitHub 系列之「加入 GitHub」 js实现克隆 ...

  3. Cocos Creator 源码解读:引擎启动与主循环

    前言 预备 不知道你有没有想过,假如把游戏世界比作一辆汽车,那么这辆"汽车"是如何启动,又是如何持续运转的呢? 如题,本文的内容主要为 Cocos Creator 引擎的启动流程和 ...

  4. js便签笔记(10) - 分享:json2.js源码解读笔记

    1. 如何理解“json” 首先应该意识到,json是一种数据转换格式,既然是个“格式”,就是个抽象的东西.它不是js对象,也不是字符串,它只是一种格式,一种规定而已. 这个格式规定了如何将js对象转 ...

  5. js便签笔记(10) - 分享:json.js源码解读笔记

    1. 如何理解“json” 首先应该意识到,json是一种数据转换格式,既然是个“格式”,就是个抽象的东西.它不是js对象,也不是字符串,它只是一种格式,一种规定而已. 这个格式规定了如何将js对象转 ...

  6. fastclick.js源码解读分析

    阅读优秀的js插件和库源码,可以加深我们对web开发的理解和提高js能力,本人能力有限,只能粗略读懂一些小型插件,这里带来对fastclick源码的解读,望各位大神不吝指教~! fastclick诞生 ...

  7. 前端模板引擎doT.js的用法

    简介 一款简单好用的前端模板引擎 用法 <script type="text/javascript" src="js/doT.min.js">< ...

  8. React16源码解读:开篇带你搞懂几个面试考点

    引言 如今,主流的前端框架React,Vue和Angular在前端领域已成三足鼎立之势,基于前端技术栈的发展现状,大大小小的公司或多或少也会使用其中某一项或者多项技术栈,那么掌握并熟练使用其中至少一种 ...

  9. CesiumJS 2022^ 源码解读[7] - 3DTiles 的请求、加载处理流程解析

    目录 1. 3DTiles 数据集的类型 2. 创建瓦片树 2.1. 请求入口文件 2.2. 创建树结构 2.3. 瓦片缓存机制带来的能力 3. 瓦片树的遍历更新 3.1. 三个大步骤 3.2. 遍历 ...

随机推荐

  1. zip文件jQuery工作地点选择城市代码

    效果 地址下载:http://download.csdn.net/detail/xiaoliu123586/9201925 2.效果 源码:http://download.csdn.net/detai ...

  2. Windows Live Writer离线编写博客

    WLW最新版本为2012,官网下载 Windows Live Writer配置步骤 使用Windows Live Writer 2012和Office Word 2013 发布文章到博客园全面总结 L ...

  3. 使用EntityFramework6完成增删查改和事务

    使用EntityFramework6完成增删查改和事务 上一节我们已经学习了如何使用EF连接数据库,并简单演示了一下如何使用EF6对数据库进行操作,这一节我来详细讲解一下. 使用EF对数据库进行操作, ...

  4. #include <NOIP2009 Junior> 细胞分裂 ——using namespace wxl;

    题目描述 Hanks 博士是 BT (Bio-Tech,生物技术) 领域的知名专家.现在,他正在为一个细胞实 验做准备工作:培养细胞样本. Hanks 博士手里现在有 N 种细胞,编号从 1~N,一个 ...

  5. 2014 Super Training #8 C An Easy Game --DP

    原题:ZOJ 3791 http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3791 题意:给定两个0-1序列s1, s2,操作t ...

  6. mysql 存储过程,以及mybatis如何调用

    说道存储过程,很多人都知道,但是真正用的人其实很少,但是在某些必要的场景,是必须使用的,虽然可以使用java代码解决,但是效率性能远不及存储过程 曾经在sqlserver 以及pgadmin上用过,m ...

  7. AC日记——机器翻译 洛谷 P1540

    题目背景 小晨的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章. 题目描述 这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换.对于每个英文单词,软件会先 ...

  8. Spring 中注入 properties 中的值

    <bean id="ckcPlaceholderProperties" class="org.springframework.beans.factory.confi ...

  9. iOS开发如何提高(from 唐巧的博客)

    http://blog.devtang.com/blog/2014/07/27/ios-levelup-tips/

  10. 用于json的 .ashx 小细节

    public void ProcessRequest(HttpContext context) { switch (action) { case "attribute_field_valid ...