声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢!

从Sizzle1.8开始,这是Sizzle的分界线了,引入了编译函数机制

网上基本没有资料细说这个东东的,sizzle引入这个实现主要的作用是分词的筛选,提高逐个匹配的效率

我们不直接看代码的实现,通过简单的实现描述下原理:

以下是个人的理解,如果有错误欢迎指出!


Javascript有预编译与我们说的编译函数是不同的概念

什么是JavaScript的“预编译”?

functionAaron() {alert("hello");
};
Aaron();//这里调用Aaron,输出world而不是hellofunctionAaron() {alert("world");
};
Aaron();//这里调用Aaron,当然输出world

  • 按理说,两个签名完全相同的函数,在其他编程语言中应该是非法的。但在JavaScript中,这没错。不过,程序运行之后却发现一个奇怪的现象:两次调用都只是最后那个函数里输出的值!显然第一个函数没有起到任何作用。这又是为什么呢?
  • JavaScript执行引擎并非一行一行地分析和执行程序,而是一段一段地进行预编译后让后 再执行的。而且,在同一段程序中,函数 在被执行之前 会被预定义,后定定义的 同名函数 会覆盖 先定义的函数。在调用函数的时候,只会调用后一个预定义的函数(因为后一个预定义的函数把前一个预定义的函数覆盖了)。也就是说,在第一次调用myfunc之前,第一个函数语句定义的代码逻辑,已被第二个函数定义语句覆盖了。所以,两次都调用都是执行最后一个函数逻辑了。

我们用实际证明下:

//第一段代码
<script>functionAaron() {alert("hello");};Aaron();//hello
</script>//第二段代码
<script>functionAaron() {alert("world");};Aaron();//world
</script>

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

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

那么我们在来看看


什么是编译函数?

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

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

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

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

PC上的效果

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

移动端的效果

编译出来的的APK

通过一套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表达一段逻辑,由于在各种环境上存在着各种不同的异步情景,代码执行流程会在这里“暂停”,等待该异步操作结束,然后再继续执行后续代码

如果是这样的情况

var a = 1;setTimeout(function(){a++;
},1000)alert(a)//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

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

还有词法分析合集 group


Sizzle中的元匹配器

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

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

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) {
}

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

拿出2个源码

//ID元匹配器工厂
Expr.filter["ID"] =  function( id ) {var attrId =id.replace( runescape, funescape );//生成一个匹配器,return function( elem ) {var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");//去除节点的id,判断跟目标是否一致return node && node.value ===attrId;};
};

//属性元匹配器工厂//name :属性名//operator :操作符//check : 要检查的值//例如选择器 [type="checkbox"]中,name="type" operator="=" check="checkbox"
"ATTR": function(name, operator, check) {//返回一个元匹配器return function(elem) {//先取出节点对应的属性值var result =Sizzle.attr(elem, name);//看看属性值有木有!if (result == null) {//如果操作符是不等号,返回真,因为当前属性为空 是不等于任何值的return operator === "!=";}//如果没有操作符,那就直接通过规则了if (!operator) {return true;}result+= "";//如果是等号,判断目标值跟当前属性值相等是否为真return operator === "=" ? result ===check ://如果是不等号,判断目标值跟当前属性值不相等是否为真operator === "!=" ? result !==check ://如果是起始相等,判断目标值是否在当前属性值的头部operator === "^=" ? check && result.indexOf(check) === 0://这样解释: lang*=en 匹配这样 <html lang="xxxxenxxx">的节点operator === "*=" ? check && result.indexOf(check) > -1://如果是末尾相等,判断目标值是否在当前属性值的末尾operator === "$=" ? check && result.slice(-check.length) ===check ://这样解释: lang~=en 匹配这样 <html lang="zh_CN en">的节点operator === "~=" ? (" " + result + " ").indexOf(check) > -1://这样解释: lang=|en 匹配这样 <html lang="en-US">的节点operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-"://其他情况的操作符号表示不匹配false;};
},

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

答案基本是肯定的

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

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


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

通过Sizzle.compile方法内部的,

matcherFromTokensmatcherFromGroupMatchers

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

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

重点就是

cached = matcherFromTokens(group[i]);

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

matcherFromTokens的分解是有规律的:

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

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

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

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

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

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

matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches);

"TAG": function(nodeNameSelector) {var nodeName =nodeNameSelector.replace(runescape, funescape).toLowerCase();return nodeNameSelector === "*" ?function() {return true;} :function(elem) {return elem.nodeName && elem.nodeName.toLowerCase() ===nodeName;};},

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

继续往下分解

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

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

通过elementMatcher生成一个终极匹配器

functionelementMatcher(matchers) {//生成一个终极匹配器return matchers.length > 1 ?//如果是多个匹配器的情况,那么就需要elem符合全部匹配器规则function(elem, context, xml) {var i =matchers.length;//从右到左开始匹配while (i--) {//如果有一个没匹配中,那就说明该节点elem不符合规则if (!matchers[i](elem, context, xml)) {return false;}}return true;} ://单个匹配器的话就返回自己即可matchers[0];}

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

//addCombinator方法就是为了生成有位置词素的匹配器。functionaddCombinator(matcher, combinator, base) {var dir =combinator.dir,checkNonElements= base && dir === "parentNode",doneName= done++; //第几个关系选择器return combinator.first ?//Check against closest ancestor/preceding element//检查最靠近的祖先元素//如果是紧密关系的位置词素function(elem, context, xml) {while ((elem =elem[dir])) {if (elem.nodeType === 1 ||checkNonElements) {//找到第一个亲密的节点,立马就用终极匹配器判断这个节点是否符合前面的规则returnmatcher(elem, context, xml);}}} ://Check against all ancestor/preceding elements//检查最靠近的祖先元素或兄弟元素(概据>、~、+还有空格检查)//如果是不紧密关系的位置词素function(elem, context, xml) {vardata, cache, outerCache,dirkey= dirruns + " " +doneName;//We can't set arbitrary data on XML nodes, so they don't benefit from dir caching//我们不可以在xml节点上设置任意数据,所以它们不会从dir缓存中受益if(xml) {while ((elem =elem[dir])) {if (elem.nodeType === 1 ||checkNonElements) {if(matcher(elem, context, xml)) {return true;}}}}else{while ((elem =elem[dir])) {//如果是不紧密的位置关系//那么一直匹配到true为止//例如祖宗关系的话,就一直找父亲节点直到有一个祖先节点符合规则为止if (elem.nodeType === 1 ||checkNonElements) {outerCache= elem[expando] || (elem[expando] ={});//如果有缓存且符合下列条件则不用再次调用matcher函数if ((cache = outerCache[dir]) && cache[0] ===dirkey) {if ((data = cache[1]) === true || data ===cachedruns) {return data === true;}}else{cache= outerCache[dir] =[dirkey];cache[1] = matcher(elem, context, xml) || cachedruns; //cachedruns//正在匹配第几个元素if (cache[1] === true) {return true;}}}}}};}

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

combinator为位置词素

根据关系选择器检查

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

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

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

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

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

所以matchers又经过一层包装了

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

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

看看结构


但是组合之后怎么执行?

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

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

哥们,别光看不顶啊!

jQuery 2.0.3 源码分析Sizzle引擎 - 编译函数(大篇幅)相关推荐

  1. jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 先来回答博友的提问: 如何解析 div > p + div.aaron input[type="checkb ...

  2. 菜鸟读jQuery 2.0.3 源码分析系列(1)

    原文链接在这里,作为一个菜鸟,我就一边读一边写 jQuery 2.0.3 源码分析系列 前面看着差不多了,看到下面一条(我是真菜鸟),推荐木有入门或者刚刚JS入门摸不着边的看看,大大们手下留情,想一起 ...

  3. jQuery 2.0.3 源码分析 Deferred(最细的实现剖析,带图)

    Deferred的概念请看第一篇 http://www.cnblogs.com/aaronjs/p/3348569.html ******************构建Deferred对象时候的流程图* ...

  4. 最细的实现剖析:jQuery 2.0.3源码分析Deferred

    Deferred的概念请看第一篇 http://www.cnblogs.com/aaronjs/p/3348569.html **构建Deferred对象时候的流程图** **源码解析** 因为cal ...

  5. jQuery 2.0.3 源码分析 事件体系结构

    那么jQuery事件处理机制能帮我们处理那些问题? 毋容置疑首先要解决浏览器事件兼容问题 可以在一个事件类型上添加多个事件处理函数,可以一次添加多个事件类型的事件处理函数 提供了常用事件的便捷方法 支 ...

  6. jQuery 2.0.3 源码分析core - 整体架构

    拜读一个开源框架,最想学到的就是设计的思想和实现的技巧. 废话不多说,jquery这么多年了分析都写烂了,老早以前就拜读过, 不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery ...

  7. mysql8.0源代码解析_MySQL8.0.11源码分析之mysql关键函数和执行流程

    mysql是命令行客户端程序 ,交互式输入SQL语句或从文件以批处理模式执行它们的命令行工具. 入口函数 int main(int argc, char *argv[]) { if (get_opti ...

  8. Spark2.4.0 SparkEnv 源码分析

    Spark2.4.0 SparkEnv 源码分析 更多资源 github: https://github.com/opensourceteams/spark-scala-maven-2.4.0 时序图 ...

  9. Android 11.0 Settings源码分析 - 主界面加载

    Android 11.0 Settings源码分析 - 主界面加载 本篇主要记录AndroidR Settings源码主界面加载流程,方便后续工作调试其流程. Settings代码路径: packag ...

  10. Android 8.0系统源码分析--Camera processCaptureResult结果回传源码分析

    相机,从上到下概览一下,真是太大了,上面的APP->Framework->CameraServer->CameraHAL,HAL进程中Pipeline.接各种算法的Node.再往下的 ...

最新文章

  1. Android onPause()和onStop()区别
  2. Vue 组件间通信方法汇总
  3. PHP GD库---之商详合成分享图片
  4. Android Studio2.3.3 提示找不到 jni.h
  5. ORA-04028: cannot generate diana for object xxx
  6. Windows下Python包和模块的安装方法(亲测手动安装)
  7. Mybatis判断int类型是否为空
  8. 面向对象(final/抽象类/接口/内部类)
  9. bzoj 1704: [Usaco2007 Mar]Face The Right Way 自动转身机
  10. BZOJ 3359: [Usaco2004 Jan]矩形( dp )
  11. 小布老师Oracle 9i DBA Fundamentals I视频讲座
  12. 各品牌路由器默认账号密码
  13. win10edge启用html5,edge浏览器如何启用flash?win10 Edge浏览器禁用flash方法
  14. Table is marked as crashed and should be repaire 解决方法
  15. win10隐藏图标按钮消失的解决办法
  16. Hive建外表操作以及其它修改表操作 hive外表与内表区别
  17. 面临困难不知道如何抉择怎么办,《大话西游之大圣娶亲》观后感
  18. 经典语录-2014330
  19. Ubuntu 18.04安装坚果云后打开出现白板
  20. liunx启动时:initramfs unpacking failed:write error

热门文章

  1. arm中用c语言编写的程序 出现数组的最后一行调用不到,C语言函数指针数组在ARM中断中的应用...
  2. html5中的错误怎么调试,页面中css调试和问题 解决的一些经验总结
  3. 如何将文件地址转为url_如何快速替换WordPress站点新旧URL地址?
  4. OKHttp源码解析(6)----拦截器CallServerInterceptor
  5. 明晰当下洞见未来 做区块链你还需要这四项技能
  6. 浅谈php的抽象类和接口类
  7. MonoCSharp Evaluator Extension
  8. Google GPS在Android上的一些应用
  9. java——题型和考点大纲
  10. 华为主题锁屏壁纸换不掉_华为手机中的这些默认设置一定要改,不然会导致手机卡顿加快耗电...