jQuery 2.0.3源码分析Sizzle引擎

开发 前端
从Sizzle1.8开始,这是Sizzle的分界线了,引入了编译函数机制,网上基本没有资料细说这个东东的,Sizzle引入这个实现主要的作用是分词的筛选,提高逐个匹配的效率,我们不直接看代码的实现,通过简单的实现描述下原理。

什么是JavaScript的“预编译”?

  1. function Aaron() { 
  2.     alert("hello"); 
  3. }; 
  4. Aaron(); //这里调用Aaron,输出world而不是hello 
  5.  
  6. function Aaron() { 
  7.     alert("world"); 
  8. }; 
  9. Aaron(); //这里调用Aaron,当然输出world 

按理说,两个签名完全相同的函数,在其他编程语言中应该是非法的。但在JavaScript中,这没错。不过,程序运行之后却发现一个奇怪的现象:两次调用都只是***那个函数里输出的值!显然***个函数没有起到任何作用。这又是为什么呢?

JavaScript执行引擎并非一行一行地分析和执行程序,而是一段一段地进行预编译后让后 再执行的。而且,在同一段程序中,函数 在被执行之前 会被预定义,后定定义的 同名函数 会覆盖 先定义的函数。在调用函数的时候,只会调用后一个预定义的函数(因为后一个预定义的函数把前一个预定义的函数覆盖了)。也就是说,在***次调用myfunc之前,***个函数语句定义的代码逻辑,已被第二个函数定义语句覆盖了。所以,两次都调用都是执行***一个函数逻辑了。

我们用实际证明下:

  1. //***段代码 
  2. <script> 
  3.     function Aaron() { 
  4.         alert("hello"); 
  5.     }; 
  6.     Aaron(); //hello 
  7. </script> 
  8.  
  9. //第二段代码 
  10. <script> 
  11.     function Aaron() { 
  12.         alert("world"); 
  13.     }; 
  14.     Aaron(); //world 
  15. </script> 

一段代码中的定义式函数语句会优先执行,这似乎有点象静态语言的编译概念。所以,这一特征也被有些人称为:JavaScript的“预编译”

所以总结下:JS 解析器在执行语句前会将函数声明和变量定义进行"预编译",而这个"预编译",并非一个页面一个页面地"预编译",而是一段一段地预编译,所谓的段就是一 个 <script> 块。

那么我们再来看看

什么是编译函数?

这个概念呢,我只用自己的语言表述下吧,先看看我在实际项目中的一种使用吧~

这里大概介绍下,偶做的是phonegap项目,基本实现了一套ppt的模板动画

PPT的的功能设置(支持生成3个平台的应用)

5M32C932CWKMGQH_WC_thumb1 6A`{YX`%}M)XF0OMPE4T(JV_thumb[2]

通过这个PPT直接描述出用户行为的数据,然后直接打包生成相对应的实现应用了,实现部分是JS+CSS3+html5 ,关键是可以跨平台哦

PC上的效果

页面的元素都是动态的可运行可以交互的

image_thumb7 

移动端的效果

image_thumb10

编译出来的的APK

image_thumb8

通过一套PPT软件生成的,页面有大量的动画,声音,视频,路径动画,交互,拖动 等等效果,这里不细说了,那么我引入编译函数这个概念我是用来干什么事呢?

一套大的体系,流程控制是非常重要的,简单的来说呢就是在某个阶段该干哪一件事件了

但是JS呢其实就是一套异步编程的模型

编写异步代码是时常的事,比如有常见的异步操作:

Ajax(XMLHttpRequest)

Image Tag,Script Tag,iframe(原理类似)

setTimeout/setInterval

CSS3 Transition/Animation

HTML5 Web Database

postMessage

Web Workers

Web Sockets

and more…

JavaScript是一门单线程语言,因此一旦有某个API阻塞了当前线程,就相当于阻塞了整个程序,所以“异步”在JavaScript编程中占有很重要的地位。异步编程对程序执行效果的好处这里就不多谈了,但是异步编程对于开发者来说十分麻烦,它会将程序逻辑拆分地支离破碎,语义完全丢失。因此,许多程序员都在打造一些异步编程模型已经相关的API来简化异步编程工作,例如Promise模型

现在有的异步流程控制大多是基于CommonJS Promises规范,比如  jsdeferred,jQuery自己的deferred等等

从用户角度来说呢,越是功能强大的库,则往往意味着更多的API,以及更多的学习时间,这样开发者才能根据自身需求选择最合适的方法

从开发者角度,API的粒度问题,粒度越大的API往往功能越强,可以通过少量的调用完成大量工作,但粒度大往往意味着难以复用。越细粒度的API灵活度往往越高,可以通过有限的API组合出足够的灵活性,但组合是需要付出“表现力”作为成本的。JavaScript在表现力方面有一些硬伤。

好像这里有点偏题了,总的来说呢,各种异步编程模型都是种抽象,它们是为了实现一些常用的异步编程模式而设计出来的一套有针对性的API。但是,在实际使用过程中我们可能遇到千变万化的问题,一旦遇到模型没有“正面应对”的场景,或是触及这种模型的限制,开发人员往往就只能使用一些相对较为丑陋的方式来“回避问题”

那么在我们实际的开发中呢,我们用JS表达一段逻辑,由于在各种环境上存在着各种不同的异步情景,代码执行流程会在这里“暂停”,等待该异步操作结束,然后再继续执行后续代码

如果是这样的情况

  1. var a = 1;  setTimeout(function(){     a++; },1000)  alert(a)//1 

这段代码很简单,但是结果确不是我们想要的,我们修改一下

  1. var a = 1;  var b = function(callback) {     setTimeout(function() {        a++;         callback();     }, 1000) }  b(function(){     alert(a)  //2 }) 

任何一个普通的JavaScript程序员都能顺利理解这段代码的含义,这里的“回调”并不是“阻塞”,而会空出执行线程,直至操作完成。而且,假如系统本身没有提供阻塞的API,我们甚至没有“阻塞”代码的方法(当然,本就不该阻塞)。

到底编译函数这个概念是干嘛?

JavaScript是单线程的,代码也是同步从上向下执行的,执行流程不会随便地暂停,当遇到异步的情况,从而改变了整个执行流程的时候,我们需要对代码进行自动改写,也就是在程序的执行过程中动态生成并执行新的代码,这个过程我想称之为编译函数的一种运用吧.

我个人理解嘛,这里只是一个概念而已,闭包的一种表现方式,就像MVVM的angular就搞出一堆的概念,什么HTML编译器,指令,表达式,依赖注入等等,当然是跟Javaer有关系…

这里回到我之前的项目上面,我个人引入这个编译函数,是为了解决在流程中某个环节中因为异步导致的整个流程的执行出错,所以在JS异步之后,我会把整个同步代码编译成一个闭包函数,因为这样可以保留整个作用域的访问,这样等异步处理完毕之后,直接调用这个编译函数进行匹配即可,这样在异步的阶段,同步的代码也同时被处理了

其实说白了,就是一种闭包的使用,只是在不同的场景中换了一个优雅的词汇罢了, 那么在sizzle中,引入这个编译函数是解决什么问题了?

sizzle编译函数

文章开头就提到了,sizzle引入这个实现主要的作用是分词的筛选,提高逐个匹配的效率

这里接着上一章节 解析原理

我们在经过词法分析,简单过滤,找到适合的种子集合之后

最终的选择器抽出了input这个种子合集seed 

重组的选择器selector

  1. div > p + div.aaron input[type="checkbox"

还有词法分析合集 group

Sizzle中的元匹配器

通过tokenize最终分类出来的group分别都有对应的几种type

image

每一种type都会有对应的处理方法

  1. Expr.filter = {    
  2.  ATTR   : function (name, operator, check) {     
  3. CHILD  : function (type, what, argument, first, last) {     
  4. CLASS  : function (className) {    
  5.  ID     : function (id) {     
  6. PSEUDO : function (pseudo, argument) {  
  7.    TAG    : function (nodeNameSelector) { } 

可以把“元”理解为“原子”,也就是最小的那个匹配器。每条选择器规则最小的几个单元可以划分为:ATTR | CHILD | CLASS | ID | PSEUDO | TAG
在Sizzle里边有一些工厂方法用来生成对应的这些元匹配器,它就是Expr.filter。
举2个例子(ID类型的匹配器由Expr.filter["ID"]生成,应该是判断elem的id属性跟目标属性是否一致),

拿出2个源码

  1. //ID元匹配器工厂 
  2. Expr.filter["ID"] =  function( id ) { 
  3.   var attrId = id.replace( runescape, funescape ); 
  4.   //生成一个匹配器, 
  5.   return function( elem ) { 
  6.     var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); 
  7.     //去除节点的id,判断跟目标是否一致 
  8.     return node && node.value === attrId; 
  9.   }; 
  10. }; 
  1. //属性元匹配器工厂 
  2. //name :属性名 
  3. //operator :操作符 
  4. //check : 要检查的值 
  5. //例如选择器 [type="checkbox"]中,name="type" operator="=" check="checkbox" 
  6. "ATTR"function(name, operator, check) { 
  7.     //返回一个元匹配器 
  8.     return function(elem) { 
  9.         //先取出节点对应的属性值 
  10.         var result = Sizzle.attr(elem, name); 
  11.  
  12.          //看看属性值有木有! 
  13.         if (result == null) { 
  14.             //如果操作符是不等号,返回真,因为当前属性为空 是不等于任何值的 
  15.             return operator === "!="
  16.         } 
  17.         //如果没有操作符,那就直接通过规则了 
  18.         if (!operator) { 
  19.             return true
  20.         } 
  21.  
  22.         result += ""
  23.  
  24.         //如果是等号,判断目标值跟当前属性值相等是否为真 
  25.         return operator === "=" ? result === check : 
  26.            //如果是不等号,判断目标值跟当前属性值不相等是否为真 
  27.             operator === "!=" ? result !== check : 
  28.             //如果是起始相等,判断目标值是否在当前属性值的头部 
  29.             operator === "^=" ? check && result.indexOf(check) === 0 : 
  30.             //这样解释: lang*=en 匹配这样 <html lang="xxxxenxxx">的节点 
  31.             operator === "*=" ? check && result.indexOf(check) > -1 : 
  32.             //如果是末尾相等,判断目标值是否在当前属性值的末尾 
  33.             operator === "$=" ? check && result.slice(-check.length) === check : 
  34.             //这样解释: lang~=en 匹配这样 <html lang="zh_CN en">的节点 
  35.             operator === "~=" ? (" " + result + " ").indexOf(check) > -1 : 
  36.             //这样解释: lang=|en 匹配这样 <html lang="en-US">的节点 
  37.             operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" : 
  38.             //其他情况的操作符号表示不匹配 
  39.             false
  40.     }; 
  41. }, 

到这里应该想到Sizzle其实是不是就是通过对selector做“分词”,打散之后再分别从Expr.filter 里面去找对应的方法来执行具体的查询或者过滤的操作?

答案基本是肯定的

但是这样常规的做法逻辑上是OK的,但是效率如何?

所以Sizzle有更具体和巧妙的做法

Sizzle在这里引入了 编译函数的概念

通过Sizzle.compile方法内部的,

matcherFromTokens matcherFromGroupMatchers

把分析关系表,生成用于匹配单个选择器群组的函数

matcherFromTokens,它充当了selector“分词”与Expr中定义的匹配方法的串联与纽带的作用,可以说选择符的各种排列组合都是能适应的了。Sizzle巧妙的就是它没有直接将拿到的“分词”结果与Expr中的方法逐个匹配逐个执行,而是先根据规则组合出一个大的匹配方法,***一步执行

我们看看如何用matcherFromTokens来生成对应Token的匹配器?

先贴源码 

Sizzle.compile

  1. //编译函数机制 
  2.         //通过传递进来的selector和match生成匹配器: 
  3.         compile = Sizzle.compile = function(selector, group /* Internal Use Only */ ) { 
  4.             var i, 
  5.                  setMatchers = [], 
  6.                  elementMatchers = [], 
  7.                  cached = compilerCache[selector + " "]; 
  8.              if (!cached) { //依旧看看有没有缓存 
  9.                  // Generate a function of recursive functions that can be used to check each element 
  10.                 if (!group) { 
  11.                     //如果没有词法解析过 
  12.                     group = tokenize(selector); 
  13.                 } 
  14.                 i = group.length; //从后开始生成匹配器 
  15.                 //如果是有并联选择器这里多次等循环 
  16.                 while (i--) { 
  17.                     //这里用matcherFromTokens来生成对应Token的匹配器 
  18.                     cached = matcherFromTokens(group[i]); 
  19.                     if (cached[expando]) { 
  20.                         setMatchers.push(cached); 
  21.                     } else { //普通的那些匹配器都压入了elementMatchers里边 
  22.                         elementMatchers.push(cached); 
  23.                     } 
  24.                 } 
  25.                 // Cache the compiled function 
  26.                 // 这里可以看到,是通过matcherFromGroupMatchers这个函数来生成最终的匹配器 
  27.                 cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers)); 
  28.             } 
  29.             //把这个***匹配器返回到select函数中 
  30.             return cached; 
  31.         }; 

matcherFromTokens

 

  1.  1:      //生成用于匹配单个选择器组的函数 
  2.  2:      //充当了selector“tokens”与Expr中定义的匹配方法的串联与纽带的作用, 
  3.  3:      //可以说选择符的各种排列组合都是能适应的了 
  4.  4:      //Sizzle巧妙的就是它没有直接将拿到的“分词”结果与Expr中的方法逐个匹配逐个执行, 
  5.  5:      //而是先根据规则组合出一个大的匹配方法,***一步执行。但是组合之后怎么执行的 
  6.  6:      function matcherFromTokens(tokens) { 
  7.  7:          var checkContext, matcher, j, 
  8.  8:              len = tokens.length, 
  9.  9:              leadingRelative = Expr.relative[tokens[0].type], 
  10. 10:              implicitRelative = leadingRelative || Expr.relative[" "], //亲密度关系 
  11. 11:              i = leadingRelative ? 1 : 0, 
  12. 12:    
  13. 13:              // The foundational matcher ensures that elements are reachable from top-level context(s) 
  14. 14:              // 确保这些元素可以在context中找到 
  15. 15:              matchContext = addCombinator(function(elem) { 
  16. 16:                  return elem === checkContext; 
  17. 17:              }, implicitRelative, true), 
  18. 18:              matchAnyContext = addCombinator(function(elem) { 
  19. 19:                  return indexOf.call(checkContext, elem) > -1; 
  20. 20:              }, implicitRelative, true), 
  21. 21:    
  22. 22:              //这里用来确定元素在哪个context 
  23. 23:              matchers = [ 
  24. 24:                  function(elem, context, xml) { 
  25. 25:                      return (!leadingRelative && (xml || context !== outermostContext)) || ( 
  26. 26:                          (checkContext = context).nodeType ? 
  27. 27:                          matchContext(elem, context, xml) : 
  28. 28:                          matchAnyContext(elem, context, xml)); 
  29. 29:                  } 
  30. 30:              ]; 
  31. 31:    
  32. 32:          for (; i < len; i++) { 
  33. 33:              // Expr.relative 匹配关系选择器类型 
  34. 34:              // "空 > ~ +" 
  35. 35:              if ((matcher = Expr.relative[tokens[i].type])) { 
  36. 36:                  //当遇到关系选择器时elementMatcher函数将matchers数组中的函数生成一个函数 
  37. 37:                  //(elementMatcher利用了闭包所以matchers一直存在内存中) 
  38. 38:                  matchers = [addCombinator(elementMatcher(matchers), matcher)]; 
  39. 39:              } else { 
  40. 40:                  //过滤  ATTR CHILD CLASS ID PSEUDO TAG 
  41. 41:                  matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches); 
  42. 42:    
  43. 43:                  // Return special upon seeing a positional matcher 
  44. 44:                  //返回一个特殊的位置匹配函数 
  45. 45:                  //伪类会把selector分两部分 
  46. 46:                  if (matcher[expando]) { 
  47. 47:                      // Find the next relative operator (if any) for proper handling 
  48. 48:                      // 发现下一个关系操作符(如果有话)并做适当处理 
  49. 49:                      j = ++i; 
  50. 50:                      for (; j < len; j++) { 
  51. 51:                          if (Expr.relative[tokens[j].type]) { //如果位置伪类后面还有关系选择器还需要筛选 
  52. 52:                              break; 
  53. 53:                          } 
  54. 54:                      } 
  55. 55:                      return setMatcher( 
  56. 56:                          i > 1 && elementMatcher(matchers), 
  57. 57:                          i > 1 && toSelector( 
  58. 58:                              // If the preceding token was a descendant combinator, insert an implicit any-element `*` 
  59. 59:                              tokens.slice(0, i - 1).concat({ 
  60. 60:                                  value: tokens[i - 2].type === " " ? "*" : "" 
  61. 61:                              }) 
  62. 62:                          ).replace(rtrim, "$1"), 
  63. 63:                          matcher, 
  64. 64:                          i < j && matcherFromTokens(tokens.slice(i, j)), //如果位置伪类后面还有选择器需要筛选 
  65. 65:                          j < len && matcherFromTokens((tokenstokens = tokens.slice(j))), //如果位置伪类后面还有关系选择器还需要筛选 
  66. 66:                          j < len && toSelector(tokens) 
  67. 67:                      ); 
  68. 68:                  } 
  69. 69:                  matchers.push(matcher); 
  70. 70:              } 
  71. 71:          } 
  72. 72:    
  73. 73:          return elementMatcher(matchers); 
  74. 74:      } 
 

重点就是

  1. cached = matcherFromTokens(group[i]); 

cached 的结果就是matcherFromTokens返回的matchers编译函数了

matcherFromTokens的分解是有规律的:

语义节点+关系选择器的组合

  1. div > p + div.aaron input[type="checkbox"

Expr.relative 匹配关系选择器类型

当遇到关系选择器时elementMatcher函数将matchers数组中的函数生成一个函数

在递归分解tokens中的词法元素时

提出***个typ匹配到对应的处理方法

  1. matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches); 
  2.  
  3. "TAG": function(nodeNameSelector) {                 
  4. var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase();    
  5.              return nodeNameSelector === "*" ? 
  6.                     function() {                  
  7.        return true;     
  8.  
  9.             } :                     
  10. function(elem) {        
  11.     return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;   
  12.               };             }, 

matcher其实最终结果返回的就是bool值,但是这里返回只是一个闭包函数,不会马上执行,这个过程换句话就是 编译成一个匿名函数

继续往下分解

如果遇到关系选着符就会合并分组了

  1. matchers = [addCombinator(elementMatcher(matchers), matcher)]; 

通过elementMatcher生成一个***匹配器

  1. function elementMatcher(matchers) { 
  2.         //生成一个***匹配器 
  3.         return matchers.length > 1 ? 
  4.         //如果是多个匹配器的情况,那么就需要elem符合全部匹配器规则 
  5.             function(elem, context, xml) { 
  6.                 var i = matchers.length; 
  7.                 //从右到左开始匹配 
  8.                 while (i--) { 
  9.                     //如果有一个没匹配中,那就说明该节点elem不符合规则 
  10.                     if (!matchers[i](elem, context, xml)) { 
  11.                         return false
  12.                     } 
  13.                 } 
  14.                 return true
  15.         } : 
  16.         //单个匹配器的话就返回自己即可 
  17.             matchers[0]; 
  18.     } 

看代码大概就知道,就是分解这个子匹配器了,返回又一个curry函数,给addCombinator方法

  1. //addCombinator方法就是为了生成有位置词素的匹配器。 
  2.     function addCombinator(matcher, combinator, base) { 
  3.         var dir = combinator.dir, 
  4.             checkNonElements = base && dir === "parentNode", 
  5.             donedoneName = done++; //第几个关系选择器 
  6.  
  7.         return combinator.first ? 
  8.         // Check against closest ancestor/preceding element 
  9.         // 检查最靠近的祖先元素 
  10.         // 如果是紧密关系的位置词素 
  11.         function(elem, context, xml) { 
  12.             while ((elemelem = elem[dir])) { 
  13.                 if (elem.nodeType === 1 || checkNonElements) { 
  14.                     //找到***个亲密的节点,立马就用***匹配器判断这个节点是否符合前面的规则 
  15.                     return matcher(elem, context, xml); 
  16.                 } 
  17.             } 
  18.         } : 
  19.  
  20.         // Check against all ancestor/preceding elements 
  21.         //检查最靠近的祖先元素或兄弟元素(概据>、~、+还有空格检查) 
  22.         //如果是不紧密关系的位置词素 
  23.         function(elem, context, xml) { 
  24.             var data, cache, outerCache, 
  25.                 dirkey = dirruns + " " + doneName; 
  26.  
  27.             // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching 
  28.             // 我们不可以在xml节点上设置任意数据,所以它们不会从dir缓存中受益 
  29.             if (xml) { 
  30.                 while ((elemelem = elem[dir])) { 
  31.                     if (elem.nodeType === 1 || checkNonElements) { 
  32.                         if (matcher(elem, context, xml)) { 
  33.                             return true; 
  34.                         } 
  35.                     } 
  36.                 } 
  37.             } else { 
  38.                 while ((elemelem = elem[dir])) { 
  39.                     //如果是不紧密的位置关系 
  40.                     //那么一直匹配到true为止 
  41.                     //例如祖宗关系的话,就一直找父亲节点直到有一个祖先节点符合规则为止 
  42.                     if (elem.nodeType === 1 || checkNonElements) { 
  43.                         outerCache = elem[expando] || (elem[expando] = {}); 
  44.                         //如果有缓存且符合下列条件则不用再次调用matcher函数 
  45.                         if ((cache = outerCache[dir]) && cache[0] === dirkey) { 
  46.                             if ((data = cache[1]) === true || data === cachedruns) { 
  47.                                 return data === true; 
  48.                             } 
  49.                         } else { 
  50.                             cache = outerCache[dir] = [dirkey]; 
  51.                             cache[1] = matcher(elem, context, xml) || cachedruns; //cachedruns//正在匹配第几个元素 
  52.                             if (cache[1] === true) { 
  53.                                 return true; 
  54.                             } 
  55.                         } 
  56.                     } 
  57.                 } 
  58.             } 
  59.         }; 
  60.     } 

matcher为当前词素前的“***匹配器”

combinator为位置词素

根据关系选择器检查

如果是这类没有位置词素的选择器:’#id.aaron[name="checkbox"]‘

从右到左依次看看当前节点elem是否匹配规则即可。但是由于有了位置词素,

那么判断的时候就不是简单判断当前节点了,

可能需要判断elem的兄弟或者父亲节点是否依次符合规则。

这是一个递归深搜的过程。

所以matchers又经过一层包装了

然后用同样的方式递归下去,直接到tokens分解完毕

返回的结果一个根据关系选择器分组后在组合的嵌套很深的闭包函数了

看看结构

image

 

但是组合之后怎么执行?

superMatcher方法是matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出来的,但是***执行起重要作用的是它

下章在继续,这章主要只是要说说这个编译函数的流程,具体还有细节,就需要仔细看代码,我不能一条一条去分解的,还有函数具体的用处,就需要结合后面的才能比较好的理解!

原文链接:http://www.cnblogs.com/aaronjs/p/3322466.html

特此感谢Aaron

【编辑推荐】

  1. 抛砖引玉 自定义jQuery扩展接口
  2. jQuery四大天王:核心函数详解
  3. 改变获取对象方式 ***的jQuery选择器
责任编辑:彭凡 来源: 博客园
相关推荐

2010-07-20 10:11:32

jQuery选择器Sizzle

2013-10-10 14:52:53

jQueryDeferred

2012-09-06 10:07:26

jQuery

2011-03-15 11:33:18

iptables

2014-08-26 11:11:57

AsyncHttpCl源码分析

2012-11-06 11:07:59

jQueryJSjQuery框架

2013-05-27 14:37:31

Hadoop 2.0.

2011-05-26 10:05:48

MongoDB

2022-08-30 07:00:18

执行引擎Hotspot虚拟机

2021-11-11 17:40:08

WatchdogAndroid源码分析

2012-07-10 09:34:50

jQuery

2010-01-05 15:55:33

JQuery源码

2011-05-26 16:18:51

Mongodb

2012-02-01 16:08:05

JavajOOQ

2010-08-26 17:08:20

vsftpd faq

2012-04-13 09:45:53

JavaScriptjQuery

2020-11-24 09:04:55

PriorityBlo

2021-07-12 08:00:21

Nacos 服务注册源码分析

2020-11-20 06:22:02

LinkedBlock

2021-08-09 11:15:28

MybatisJavaSpring
点赞
收藏

51CTO技术栈公众号