var relative = { ">":{ dir:"parentNode", first: true }, " ":{ dir: "parentNode" }, "+":{ dir:"previousSibling", first: true }, "~":{ dir: "previousSibling" } }
function addCombinator(elems){ var elem; while((elem = elems['parentNode'])){ if(elem.nodeType===1){ return elem; } } }
CSS选择器的位置关系
1、祖宗和后代 // 用空格 2、父亲和儿子 // 用 > 3、临近兄弟 // 用 + 4、普通兄弟 // 用 ~
Sizzle超级匹配器的流程:
div > p + div.aaron input[type="checkbox"]
1、从右边剥离出原生API能使用的接口属性:id、class、tagname。所以找到了input,因为只可以用tag是查询,但是此时结果是个合集,引入seed的概念,称之为种子合集。
context.getElementsByTagName( input )
2、重组选择器,踢掉input,得到新的tokens词法元素哈希表
div > p + div.aaron [type="checkbox"]
3、通过matcherFromTokens函数,然后根据关系选择器 【">","空","~","+"】拆分分组,因为DOM中的节点都是存在关系的,所以引入Expr.relative -> first:true 两个关系的“紧密”程度,用于组合最佳的筛选。
4、依次按照如下顺序解析并且编译闭包函数,编译规则:div > p + div.aaron [type="checkbox"]。编译成4组闭包函数,然后在前后在合并组合成一组
div > p + div.aaron input[type="checkbox"]
A. 抽出div元素,对应的是TAG类型
B. 通过Expr.filter找到对应匹配的处理器,返回一个闭包处理器
C. 将返回的curry方法放入到matchers匹配器组中,继续分解
D. 抽出子元素选择器 '>' ,对应的类型 type: ">"
E. 通过Expr.relative找到elementMatcher方法,分组合并多个词素的的编译函数
F:返回的这个匹配器还是不够的,因为没有规范搜索范围的优先级,所以这时候还要引入addCombinator方法
G:根据Expr.relative -> first:true 两个关系的“紧密”程度,找到对应的第一个亲密节点
elementMatchers:就是通过分解词法器生成的闭包函数了,也就是“终极匹配器
superMatcher遍历seed
while ( (matcher = elementMatchers[j++]) ) { if ( matcher( elem, context, xml ) ) { results.push( elem ); break; } }
为什么是while?
1、前面就提到了,tokenize选择器是可以用过 “,”逗号分组 group,所以就会有个合集的概念了,matcher就得到了每一个终极匹配器。
2、通过代码很能看出来matcher方法运行的结果都是bool值,对里面的元素逐个使用预先生成的matcher方法做匹配,如果结果为true的则直接将元素堆入返回结果集。
superMatcher函数
1、这个方法并不是一个直接定义的方法,通过matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出来的一个curry化的函数,但是最后执行起重要作用的是它。
2、superMatcher这个方法并不是一个直接定义的方法,通过matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出来的一个curry化的函数,但是最后执行起重要作用的是它
3、superMatcher方法会根据参数seed 、expandContext和context确定一个起始的查询范围:有可能是直接从seed中查询过滤,也有可能在context或者context的父节点范围内。如果不是从seed开始,那只能把整个DOM树节点取出来过滤了,把整个DOM树节点取出来过滤了,它会先执行Expr.find["TAG"]( "*", outermost )这句代码等到一个elems集合(数组合集)
elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
Sizzle编译原理(下)
1、matcherFromTokens,它充当了selector“分词”与Expr中定义的匹配方法的串联与纽带的作用,可以说选择符的各种排列组合都是能适应的了。Sizzle巧妙的就是它没有直接将拿到的“分词”结果与Expr中的方法逐个匹配逐个执行,而是先根据规则组合出一个大的匹配方法,最后一步执行。
2、matcherFromTokens的分解是有规律的,语义节点+关系选择器的组合,Expr.relative 匹配关系选择器类型,当遇到关系选择器时elementMatcher函数将matchers数组中的函数生成一个函数。
3、如果遇到关系选择符就会合并分组了,通过elementMatcher生成一个终极匹配器
4、matcher为当前词素前的“终极匹配器”,combinator为位置词素,根据关系选择器检查,判断elem的兄弟或者父亲节点是否依次符合规则。
5、编译的过程是一个递归深搜的过程。
整个流程总结就干了那么几件事:
1、在Expr.filter找出每一个选择器类型对应的处理方法
2、从右边往左,向父级匹配的时候,注意词素关系,引入relative记录这个映射的关系
3、把对应的处理函数压入matchers数组
整个编译过程,其实粗看就是把函数一层一层包装下去,之后通过匹配器传入对应的种子合集seed一层一层的解开
每条选择器规则最小的几个单元可以划分为:ATTR | CHILD | CLASS | ID | PSEUDO | TAG
在Sizzle里边有一些工厂方法用来生成对应的这些元匹配器,它就是Expr.filter。
Expr.filter = { ATTR : function (name, operator, check) { CHILD : function (type, what, argument, first, last) { CLASS : function (className) { ID : function (id) { PSEUDO : function (pseudo, argument) { TAG : function (nodeNameSelector) { }
两层过滤查找:向上迭代查找最近的父级元素
function addCombinator(elems) { var elem; while ((elem = elems['parentNode'])) { if (elem.nodeType === 1) { return elem } } };
first的意思就是一个快速条件,因为“>”选择器是一个很明确的父子关系所以通过标记first只需要查找一层即可。
function addCombinator(elems) { var elem; // 向上迭代查找 while ((elem = elems['parentNode'])) { if (elem.nodeType === 1) { return elem } } };
过滤效率问题:
按照解析原理,词法分析,过滤器原理处理之后得到的种子合集通过一次用循环递归去匹配查找,这样的效率是很慢的。
解决方式:
sizzle从给1.8开始就引入了编译的概念,sizzle引入这个编译函数主要的作用是为分词的筛选,提高逐个匹配的效率,实现闭包缓存。
原理:
闭包是js的特性,我们经常会用来作为私有变量的保存处理,那么sizzle就很好的利用了这一特性,把选择器中每一个选择原子都变成了函数的处理方法,然后通过闭包保存着。再缓存在内存中去,这样有重复使用的时候就会首先调用缓存。
Sizzle过滤器原理(下)
针对选择器的层级关系:
1、首先“>”与“空”是祖辈关系,这样可以理解是线型的,那么我们只要递归检测每次元素的 parentNode 属性返回指定节点的父节点。
2、同理“+”与“~”也是类似的兄弟关系,无非就是扩展的范围不同,所以针对层级的关系问题。
jQuery引入了词素关系:
relative: { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }
过滤处理我们需要考虑的问题:
1 怎么有效的匹配这些选择器的最小判断单元,也就是通过词法分割出后的结果
2 如何处理层级选择器的判断问题
过滤是通过一层一层往上回溯不断的循环去查找,这样虽然结果可以拿到,但是效率是非常低的。所以sizzle从1.8后采用了空间换时间的方式,通过把各种过滤器编译成闭包的函数,所以这个过程也可说是"编译函数"。
其实我们看过滤器的就是一个具体的判断方法,通过传递一个上下文元素,来判断是否存在,得到这一个布尔值,这样有效了缓存了重复的处理,来节约判断的过程。
属性选择器分支的单元结构:
"[input[name=ttt]]" matches = [ 0: "type" 1: "=" 2: "ttt" ] type: "ATTR" value: [name=ttt]"
种子合集:通过三个基本查询,生成种子合集
1、我们从右到左边开始匹配最终合集单元,从左边开始很明显是属性选择器,但是“input[name=ttt]”原生的API是不认识的。这种标签用Expr.find能匹配到了。find:ID、Tag、Class
2、这里引入了seed - 种子合集(搜索器搜到符合条件的标签),放入到这个初始集合seed中。这种我们找到了最终的一个合集,那么我们需要的就是根据剩余的条件筛选出真正的选择器就OK了。找到一个集合后就暂停,不再往下匹配了,因为如果再用这样的方式往下匹配效率就慢了。
元素的匹配器:
Expr.filter :TAG, ID, CLASS, ATTR, CHILD, PSEUDO
属性选择器有点复杂,通过第一次正则只能匹配器出整体,所以需要第二次分解,引入了Expr.preFilter,Expr.preFilter保留了3个兼容处理分别是ATTR,CHILD,PSEUDO复杂的选择器。
最终匹配结果的解构:
groups: [ tokens: { matches: ?, type : ?, value : ? }, tokens: { matches: ?, type : ?, value : ? } ]
什么是词法分析器(tokenize):
词法分析器又称扫描器,词法分析是指将我们编写的文本代码流解析为一个一个的记号,分析得到的记号以供后续语法分析使用
词法分析涉及了3大块
1、分组逗号
2、层级关系
3、每种元素处理
sizzle对于分组过滤处理都用正则,其中都有一个特点,就是都是元字符^开头,限制匹配的初始,所以tokenize也是从左边开始一层一层的剥离。其中会用到的正则有:
//分组 var rcomma = /^[\x20\t\r\n\f]*,[\x20\t\r\n\f]*/; //关系符 var rcombinators = /^[\x20\t\r\n\f]*([>+~]|[\x20\t\r\n\f])[\x20\t\r\n\f]*/; //空白 var whitespace = "[\\x20\\t\\r\\n\\f]"; // 字符串要多加个斜线
\x20 表示空格
\t 表示水平制表符。将当前位置移到下一个tab位置。
\r 表示回车。将当前位置移到本行的开头。
\n 表示回车换行。将当前位置移到下一行的开头。
\f 表示换页。将当前位置移到下一页的开头。
Sizzle解析原理:
1、浏览器支持高级API时,直接调用querySelectorAll
2、浏览器不支持高级API时,降级通过sizzle处理,那么内部会有一个规则把选择器分组groups,然后通过从右边往左边查找,加入编译函数的方式节约重复查找的性能问题
一个节点跟另一个节点有以下几种关系:
祖宗和后代
父亲和儿子
临近兄弟
普通兄弟
在Sizzle里有一个对象是记录跟选择器相关的属性以及操作:Expr。它有以下属性:
relative = { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }
所以在Expr.relative里边定义了一个first属性,用来标识两个节点的“紧密”程度,例如父子关系和临近兄弟关系就是紧密的。在创建位置匹配器时,会根据first属性来匹配合适的节点
选择器结尾加上「*」就大大降低了这种优势,这也就是很多优化原则提到的尽量避免在选择器末尾添加通配符的原因
Sizzle设计思路:
我们知道CSS的匹配规则是从右边向左筛选,jQuery在Sizzle中延续了这样的算法,先搜寻页面中所有的a标签,在之后的操纵中再往后判定它的父节点(包括父节点以上)是否为div,一层一层往上过滤,最后返回该操纵序列。
浏览器渲染原理:
1、浏览器从下载文档到显示页面的过程是个复杂的过程,这里包含了重绘和重排。各家浏览器引擎的工作原理略有差别,但也有一定规则。
2、简单讲,通常在文档初次加载时,浏览器引擎会解析HTML文档来构建DOM树,之后根据DOM元素的几何属性构建一棵用于渲染的树。渲染树的每个节点都有大小和边距等属性,类似于盒子模型(由于隐藏元素不需要显示,渲染树中并不包含DOM树中隐藏的元素)。
3、当渲染树构建完成后,浏览器就可以将元素放置到正确的位置了,再根据渲染树节点的样式属性绘制出页面。由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成,所以我们知道浏览器最终会将HTML文档(或者说页面)解析成一棵DOM树
浏览器提供的查找接口,基本靠谱的就只有三个:
Expr.find = { 'ID' : context.getElementById, 'CLASS' : context.getElementsByClassName, 'TAG' : context.getElementsByTagName }
Sizzle选择器
不能不说jQuery的反模式非职责单一深受开发者喜欢,一个接口承载的职责越多内部处理就越复杂了
jQuery查询的对象是dom元素,查询到目标元素后,如何存储?
1、查询的到结果储存到jQuery对象内部,由于查询的dom可能是单一元素,也可能是合集
2、jQuery内部应该要定义一个合集数组,用于存在选择后的dom元素
3、当然啦,根据API,jQuery构建的不仅仅只是DOM元素,还有HTML字符串、Object、[] 等等
源码缩进后的结构:
1、处理""、null、undefined、false、返回this、增加程序的健壮性
2、处理字符串
3、处理DOMElement,返回修改过后的实例对象this
4、处理$(function(){})
jQuery的异步设计逻辑也确实很复杂,需要思维跳转很活跃,某一个时间在这里,下一个片段又要另一个地方去了,不是按照同步代码这样执行的。需要大家有一定的空间跳跃力了。
when方法的设计。我们通过模拟的代码,可以很简单的分析整个流程:
1. 传递了多个异步对象,然后遍历每个异步对象给每一个对象绑定done、fail、progess方法,无非就是监听每一个异步的状态(成功,失败),如果是完成了自然会激活done方法。
2. updateFunc是监听方法,通过判断异步对象执行的次数来决定是不是已经完成了所有的处理或者是失败处理
3. 因为when也要形成异步操作,比如when().done(),所以内部必须新建一个jQuery.Deferred()对象,用来给后面链式调用。
4. 此刻监听所有异步对象(d1,d2...)的updateFunc的处理都完毕了,会给一个正确的通知给when后面的done方法,因为done是通过第三步jQuery.Deferred()创建的,所以此时就需要发送消息到这个上面,即:
deferred.resolveWith(contexts, values);
5. 内部的jQuery.Deferred()因为外部绑定了when().done(),所以done自然就收到了updateFunc给的消息了,可以继续之后的操作了。
Deferred的then处理:
我们可以把每一次的then操作,当做是创建一个新的deferred对象,那么每一个对象都够保存自己的状态与各自的处理方法。通过一个办法把所有的对象操作都串联起来,这就是then或者pipe管道设计的核心思路了。
其实在内部创建了一个新的Deferred对象,不过这里的不同是通过传递一个回调函数,参数是newDefer,其实Deferred内部就是为了改变下上下文this为deferred,然后传递deferred给这个回调函数了,所以newDefer就指向内部的deferred对象了。
如果then返回的是一个promise对象(ajax)的时候,也可以直接处理了
if (returned && jQuery.isFunction(returned.promise)) { returned.promise() .done(newDefer.resolve) .fail(newDefer.reject) .progress(newDefer.notify);
Deferred源码剖析
Deferred自身则围绕这三组数据进行更高层次的抽象
1、触发回调函数列表执行(函数名)
2、添加回调函数(函数名)
3、回调函数列表(jQuery.Callbacks对象)
4、Deferred最终状态(第三组数据除外)
var tuples = [ // action, add listener, listener list, final state ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"], ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"], ["notify", "progress", jQuery.Callbacks("memory")] ]
通过stateString有值这个条件,预先向doneList,failList中的list添加三个回调函数,分别是:
doneList : [changeState, failList.disable, processList.lock] failList : [changeState, doneList.disable, processList.lock]
1、changeState 改变状态的匿名函数,deferred的状态,分为三种:pending(初始状态), resolved(解决状态), rejected(拒绝状态);
2、不论deferred对象最终是resolve(还是reject),在首先改变对象状态之后,都会disable另一个函数列表failList(或者doneList);
3、然后lock processList保持其状态,最后执行剩下的之前done(或者fail)进来的回调函数。
4、所以第一步最终都是围绕这add方法:done/fail/是list.add也就是callbacks.add,将回调函数存入回调对象中。
没有使用promise的情况下,实现了类似promise的功能,666!
观察者模式中的订阅方法:
Done (操作完成)
Fail (操作失败)
Progress (操作进行中
观察中模式中的发布方法:
resolve(解决)
reject(拒绝)
notify(通知)
而且还提供了可以定义运行时的this对象的fire,fireWith,所以扩展了3个可以定义上下文的的接口:
resolveWith
rejectWith
notifyWith
“管道”风格
CommonJS Promise/A 标准这样定义的,promise对象上的then方法负责添加针对已完成和拒绝状态下的处理函数。then方法会返回另一个promise对象,这样可以形成“管道”风格。
promise背后的思想:不是执行一个方法,然后阻塞应用程序等待结果返回,而是返回一个promise对象来满足未来值,即承诺、许诺完成当前操作后,会执行预先设定的后续操作
querySelector的IE8兼容处理:在上下文调用的context元素上指定一个id,通过这个限制范围,这个方法用的非常广泛
具体过程:
1、关键是给context设置一个id,所以上下文content,就会存在这个id限制范围
2、拼接出查询的选择器,附上这个ID前缀
newSelector: "[id='sizzle-1405486760710'] div[class='text']"
3、查询
newContext.querySelectorAll( newSelector )
4、因为强制加了ID,所以需要删除
context.removeAttribute("id");