JS事件详解
栏目: JavaScript · 发布时间: 6年前
内容简介:JS和HTML之间的交互是通过事件来实现的,在我们的页面加载完毕,所有的由于DOM结构是层层嵌套的,所以监听程序会遇到一个问题,就是当我们点击一个位置的时候,这个位置有多个DOM节点嵌套在一起,那么我们到底点击的谁呢?即使你没深入学习过事件,也应该听过事件捕获和事件冒泡,它们也只不过是浏览器大战中IE和网景对事件流的不同理解而产生的不同实现,IE选择了事件冒泡流,而网景选择了事件捕获流。
前言
JS和HTML之间的交互是通过事件来实现的,在我们的页面加载完毕,所有的 html,css,js 文件都已经load的情况下,我们如何在文档或浏览器窗口发生变化时通过js来进行交互,这就是事件产生的原因。我们在js中预定一个事件的处理程序,然后浏览器的监听程序监听各种事件的发生,当事件发生的时候调用预定的事件处理程序来执行。这种观察员模式的模型让我们实现了 js 和 html,css 之间的松散耦合。
事件流
由于DOM结构是层层嵌套的,所以监听程序会遇到一个问题,就是当我们点击一个位置的时候,这个位置有多个DOM节点嵌套在一起,那么我们到底点击的谁呢? Javascript高级程序设计 给出了一个更形象的比喻,在纸上画一组同心圆,然后把手指指向圆心,此时你指向的不仅仅是一个圆,而是一串同心圆。同理当你点击一个DOM元素,你不仅点击了这个元素,也点击了所有父元素,包括页面本身。由于这个问题,才产生了事件流,也就是一个事件发生了,这个事件会在嵌套的DOM结构里面传播,而事件流就描述了这个传播的具体规则,嵌套的DOM元素是按照什么顺序接收事件的。
即使你没深入学习过事件,也应该听过事件捕获和事件冒泡,它们也只不过是浏览器大战中IE和网景对事件流的不同理解而产生的不同实现,IE选择了事件冒泡流,而网景选择了事件捕获流。
事件冒泡流
先来说一说IE的事件冒泡流,事件冒泡流的设计思路是事件最开始是由事件的目标事件(当前点击的位置所嵌套的DOM结构的嵌套最深的那个节点)接收,然后沿着 DOM 树逐级向上传播一直到根节点也就是 document 节点。一下面的 HTML 文档为例:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
</head>
<body>
<div class="btn">Click me!</div>
</body>
</html>
如果我们点击了Click这个按钮以后按事件冒泡流的规则,事件会按照如下顺序传播
1.<div>
2.
<body>
3.
<html>
4. document
也就是click事件首先发生在 div 上,也就是我们单击的元素,然后 click 事件沿着 DOM 树向上传播,在每一级的DOM节点上都会发生,直到传播到 DOM 树的根节点 document 。
所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。IE5.5 及更早版本中的事件冒 泡会跳过 <html> 元素(从 <body> 直接跳到 document )。IE9、Firefox、Chrome 和 Safari 则将事件一直 冒泡到 window 对象。
目标事件,也就是我们后面提到的 DOM2 里面 event 对象的 target 属性,我看网上的很多解释都不清晰,其实直白的说就是事件触发的坐标所在位置 DOM 结构嵌套最深的那个节点,也即是在 DOM 树上最深的节点,这个节点也就是所谓的 target 。
事件捕获
网景对于事件流的实现和IE则是截然相反的,也就是目标节点最后接收事件,DOM树上的上层节点则更早地接收事件。还以上面的 HTML 代码作为例子,在事件捕获流中,事件的传播顺序如下:
document <html> <body> <div>
在事件捕获流中, document 对象首先接收到事件,然后沿着 DOM 树逐级向下传播,一直传播到目标节点 div 元素,如下图:
DOM事件流
事件捕获只能在事件传播到目标元素之前截获,而事件冒泡只能在事件已经传播到目标元素之后进行截获,所以 DOM二级事件 把两者结合了起来, DOM二级事件 规定了事件传播的三个阶段:
1. 事件捕获阶段
2. 处于目标事件阶段
3. 事件冒泡阶段
还用上面的 HTML 例子来解释,在 DOM 事件流中,实际的目标( <div> 元素)在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从 document 到 <html> 再到 <body> 后就停止了。下一个阶段是“处于目标”阶段,于是事件在 <div> 上发生,并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。然后,冒泡阶段发生, 事件又传播回文档。传播的顺序如下图:
在 DOM Level 3 Events draft 中有一个更清晰的图表示三个阶段:
事件处理程序
也可以叫做事件侦听器,事件就是用户或浏览器自身执行的某种动作。诸如 click、load 和 mouseover ,都是事件的名字。 而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以 on 开头,因此 click 事件的事件处理程序就是 onclick ,load 事件的事件处理程序就是 onload 。为事件指定处理 程序的方式有好几种。
HTML事件处理程序
当一个HTML元素支持某种事件,我们可以通过该元素的属性来指定事件处理程序,比如 click 事件就可以用 onclick 属性来指定事件处理程序,属性值应该是可执行的 javascript 代码,比如点击按钮弹出警告框:
<input type="button" value="Click Me" onclick="alert('Clicked')" />
在 html 中定义的事件处理程序也可以调用别的地方定义的脚本,比如调用你在别的地方定义的函数,如下:
<script type="text/javascript">
function showMessage() {
alert("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()" />
调用的函数可以是在当前html文件中的script标签中,也可以是在页面引用的其他js文件中,事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
HTML事件处理程序的特点:
1. 创建了一个封装着元素属性值的函数,通过函数中的局部变量event直接访问事件对象(后面会介绍),不需要定义这个参数,也不需要从参数列表中读取,可以直接使用。
<script type="text/javascript">
function showMessage() {
console.log(event.type);
console.log(this.value);
}
</script>
<input type="button" value="Click Me" onclick="showMessage()" /> /* 输出 click*/
- this的指向:如果是直接在
onclick属性中执行的javascript代码,那么this指向当前的元素;如果是引用自其他标签或文件中的函数,那么this指向window对象。
<script type="text/javascript">
function showMessage() {
console.log(event.type);
console.log(this);
}
</script>
<!-- 指向当前元素 -->
<input type="button" value="Click Me" onclick="console.log(this)" />
<!-- 指向window对象 -->
<input type="button" value="Click Me" onclick="showMessage()" />
- 在函数内部可以像访问局部变量一样访问document以及该元素本身的成员,需要注意的是引用的函数同样不可以(函数的作用域链取决于函数定义的位置,而不是执行的位置),函数的内部实现类似如下:
function(){
with(document){
with(this){ //元素属性值
}
}
}
如果当前元素是一个表单输入元素,则作用域中还会包含访问表单元素(父元素)的入口,这样扩展作用域的方式,无非就是想让事件处理程序无需引用表单元素就能访问其他表单 字段。
<form method="post">
<input type="text" name="username" value="">
<!-- 可以直接访问username的value -->
<input type="button" value="Echo Username" onclick="alert(username.value)">
</form>
- 如果属性值采取的引用函数的方式,当元素已经渲染好,而js还没有加载完成,可能会造成触发事件而事件处理程序并没有执行,这样会报错,防止报错可以使用
try-catch:
<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}">
- 用HTML指定的事件处理程序造成html和javascript耦合,我们也无法同时给多个元素绑定事件,也无法给同一个事件绑定多个函数等等,由于这种方式的缺点非常明显,所以几乎已经消失了。
由于属性值是javascript代码,因此不能在语句中使用未经转义的HTML语法字符,比如和号 & ,双引号 " ,大于号 > ,小于号 < 等,并且在HTML中转义不能使用反斜杠 \ ,而要使用html实体(entity),比如双引号是 " ,如果你要查询某个字符的实体,在 w3.org 查询。
DOM0级事件处理程序
页面上的每一个元素都有一个事件处理程序属性(包括window和document对象),这些属性都是小写,比如代表click事件的 onclick ,将需要监听事件的元素的该属性的值设置为一个函数,就可以指定事件处理程序,当事件在元素上触发的时候会调用事件处理程序。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert("Clicked");
};
DOM0级事件处理程序中的 this 指向绑定事件的元素,在事件处理程序中可以访问元素的所有属性和方法。这种方法绑定的事件处理程序会在事件流的冒泡阶段被执行。
想要删除绑定的DOM0级事件处理程序,只要将元素的 onclick 属性设置为 null 即可,在HTML标签中绑定的事件处理函数也可以用这个方法来删除绑定的事件处理程序,需要注意的是删除绑定的代码要在需要删除的标签之后。
DOM2级事件处理程序
DOM2级事件处理程序是我们目前最多使用的绑定事件处理程序的方法,包含了两个主要的方法用来绑定和删除事件处理函数 addEventListener() 和 removeEventListener() 。所有DOM节点都包含这两个方法。这两个方法都接受三个参数,第一个参数是要处理的事件类型(和DOM0级事件中的对象属性不同,这里的事件类型不需要加 on ),第二个参数是事件处理程序对应的函数,第三个参数是一个布尔值,如果是 false 表示在捕获阶段调用事件处理程序,如果是 true 表示在冒泡阶段调用事件处理程序,默认为 false 。具体细节可以看 MDN 。如果我们要在一个元素上添加click事件的事件处理程序,就可以使用如下代码:
var btn = document.querySelector(".btn");
btn.addEventListener("click", function () {
console.log(this);
}, false);
其中的 click 就是要处理的事件类型,匿名函数就是我们指定的事件处理函数,最后的 false 就是指定事件触发是在冒泡阶段。当事件监听程序监听到符合要求的事件发生时,就会调用事件处理程序来执行。
DOM2级事件处理程序的特点:
1. 可以为同一个元素的同一类型的事件绑定多个事件处理程序,他们会按照添加顺序执行。如:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
console.log(this.id);
}, false);
btn.addEventListener("click", function(){
console.log("Hello world!");
}, false);
这段代码为btn的click类型的事件指定了两个事件处理程序,当我们触发btn的click事件的时候,这两个事件处理程序会按顺序执行,也就是先输出 this.id 然后输出 Hello world! 。
- 由于可以指定事件触发的阶段以及event对象的存在我们可以用DOM2级事件进行事件委托,达到对性能的提升和同类型元素绑定事件的简化,后面会详细讨论。
-
因为可以为同一元素和同一类型的事件绑定多个事件处理程序,所以要删除这些事件只能通过
removeEventListener()来删除,并且移除时传入的参数必须与绑定时传入的参数相同,如果在绑定的时候如果用的是匿名函数,那么这个事件处理程序将无法删除,因为两个不同的匿名函数指向的是不同的空间,如:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
btn.removeEventListener("click", function(){ //无效
alert(this.id);
}, false);
如果要实现事件处理函数的删除需要将第二个参数换成函数的引用:
var btn = document.getElementById("myBtn");
var handler = function(){
alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!
IE9、Firefox、Safari、Chrome 和 Opera 支持 DOM2 级事件处理程序。如果不是特别的事件如 mouseenter 不支持事件冒泡,一般就默认在冒泡阶段触发即可,事件捕获可以当我们需要在事件传播到目标之前截获的时候使用。
IE事件处理程序
在IE9之前的版本使用的是IE独特的事件处理程序,它只支持冒泡,有两个方法 attachEvent() 和 detachEvent() 两个方法,接收两个参数:事件类型(和DOM0一样需要加上 on )和事件处理程序函数,由于现在很少需要兼容IE9之前的版本,就不过多讨论了,放上一个跨浏览器的事件处理程序:
/* 若支持DOM2级事件处理程序则用DOM2级,若支持IE的事件处理程序则用IE,否则用DOM0级事件处理程序 */
var EventUtil = {
addHandler: function(element, type, handler){
if (element.addEventListener){
element.addEventListener(type, handler, false);
} else if (element.attachEvent){
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
removeHandler: function(element, type, handler){
if (element.removeEventListener){
element.removeEventListener(type, handler, false);
} else if (element.detachEvent){
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
}
};
事件对象
在上面的事件处理程序中多次提到了 event 对象,我们在上面的代码中输出过 event 对象的 type 属性,这个属性表示当前指定的事件处理成熟的事件类型。事实上,每当某个DOM元素触发了某个事件,都会产生一个 event 事件对象,这个对象中包含着所有与事件有关的信息。包括触发事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件 对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有 浏览器都支持 event 对象,但支持方式不同。
在DOM0级和DOM2级事件处理程序中,浏览器会将一个 event 对象传入我们定义的事件处理程序的函数中,即使我们没有在函数的参数列表中加入 event 形参,我们也可以在函数内部使用,应该是浏览器替我加上了参数 event :
<button id="myBtn">btn</button>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function () {
console.log(event.type);//可以输出
}
</script>
一般为了代码便于理解,我们在事件处理程序的回调函数中给出参数 event .
即使我们通过HTML内联的方式执行事件处理程序,在其中我们也可以使用一个指向 event 对象的变量 event :
<input type="button" value="Click Me" onclick="alert(event.type)"/>
event 对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都会有下表列出的成员。
| 属性/方法 | 类型 | 读/写 | 说明 |
|---|---|---|---|
| bubbles | Boolean | 只读 | 事件是否冒泡 |
| cancelable | Boolean | 只读 | 是否可以取消事件的默认行为 |
| currentTarget | Element | 只读 | 事件处理程序当前处理元素 |
| defaultPrevented | Boolean | 只读 | 为 true表示已经调用了preventDefault()(DOM3级事件中新增) |
| detail | Integer | 只读 | 与事件相关细节信息 |
| eventPhase | Integer | 只读 | 事件处理程序阶段:1 捕获阶段,2 处于目标阶段,3 冒泡阶段 |
| preventDefault() | Function | 只读 | 取消事件默认行为 |
| stopPropagation() | Function | 只读 | 取消事件进一步捕获或冒泡 |
| target | Element | 只读 | 事件的目标元素 |
| trusted | Boolean | 只读 | 为true表示事件是浏览器生成的。为false表 示事件是由开发人员通过JavaScript创建的(DOM3级事件中新增) |
| type | String | 只读 | 被触发的事件类型 |
| view | AbstractView | 只读 | 与事件关联的抽象视图,等同于发生事件的window对象 |
关于 currentTarget 和 target 只要记住一点,不管事件传播处于什么阶段, target 都是不变的,指向目标元素,所以我们在实现事件委托的时候会用到 target 。而 currentTarget 则是随着事件传播处于不同的阶段而指向不同的元素。具体细节点击实现页面,打开控制台点击不同的元素查看细节。
在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则 this、currentTarget 和 target 包含相同 的值。来看下面的例子。
我们可以利用 event 对象的 type 属性来用一个函数处理多种事件:
var handler = function(event){
switch(event.type){
case "click":
alert("Clicked");
break;
case "mouseover":
event.target.style.backgroundColor = "red";
break;
case "mouseout":
event.target.style.backgroundColor = "";
break;
}
};
有时我们会需要阻止某些事件的默认行为,比如点击表单的 submit 跳转,以及点击 a标签 的跳转,如果我们不希望这些行为发生,那么我们可以利用 event对象 的 preventDefault() 方法,只有 cancelable 属性设置为 true 的事件,才可以使用 preventDefault()来取消其默认行为。
event对象 还有一个重要的方法是 stopPropagation() ,这个方法用来停止事件在 DOM 树上的传播,不管在哪个传播阶段,都会停止事件的传播。比如我们在按钮和body上都注册了一个事件,当用户点击按钮,我们不希望注册在body上的事件被触发,此时我们就需要用到这个方法:
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
alert("Clicked");
event.stopPropagation();
};
document.body.onclick = function(event){
alert("Body clicked");
};
事件对象的 eventPhase 属性,可以用来确定事件当前正位于事件流的哪个阶段。如果是在捕获阶 段调用的事件处理程序,那么 eventPhase 等于 1 ;如果事件处理程序处于目标对象上,则 event- Phase 等于 2 ;如果是在冒泡阶段调用的事件处理程序, eventPhase 等于 3 。这里要注意的是,尽管“处于目标”发生在冒泡阶段,但 eventPhase 仍然一直等于 2。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
alert(event.eventPhase); //2
};
document.body.addEventListener("click", function(event){
alert(event.eventPhase); //1
}, true);
document.body.onclick = function(event){
alert(event.eventPhase); //3
};
还有一个要注意的点是,很多同学学习事件传播顺序的知识会容易混淆到单个元素的事件处理程序的执行顺序上,对于单个元素,无论你是绑定在捕获阶段还是冒泡阶段,都是先绑定的事件处理程序先执行,事件传播只在嵌套中的不同DOM元素之间有效,比如如下代码,就是先执行冒泡在执行捕获。
<div id="el">element</div>
<script type="text/javascript">
var el = document.getElementById('el');
//冒泡
el.addEventListener('click',function () {
console.log("el冒泡");
},false);
//捕获
el.addEventListener('click',function () {
console.log("el捕获");
},true);
</script>
event对象 在事件触发的时候生成,当事件处理程序执行结束后, event对象 即被销毁,也就是说只有在事件处理程序执行期间, event对象 才会存在。
事件类型
浏览器中发生的事件类型很多,详情查询 MDN
内存和性能
在JS中,添加到页面上的事件处理程序的数量会影响的页面的整体性能,因为每一个事件处理函数也都是对象,都保存在内存中,事件处理程序多了自然对内存的开销会增大。其次,在JS的渲染过程中,指定事件处理程序需要访问DOM,没绑定一个事件处理程序都需要访问一次DOM,如果事件处理程序过多,会影响页面渲染完成的时间。如果我们能够更好地处理事件,对提升页面的性能是有一定的帮助的。
事件委托
事件委托其实很好理解,利用事件流传播的特性,利用事件冒泡,我们可以对多个需要绑定事件的同类型元素的上级DOM节点绑定一个事件处理程序,用这个上层节点的事件处理程序来同一管理那些同一类型的事件。举个例子:
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
如果我们要实现点击每个 li 都输出其中的文本,那么按照传统的做法,我们会为每个 li 绑定一个事件,如果同样类型的元素特别多,那么我们一个一个绑定事件显然是不现实的,而且这样页面的性能也不佳。如果我们事件委托,我们就可以把事件处理程序绑定到 ul 上,利用事件冒泡的特性来统一管理点击 li 的事件。
var list = document.querySelector("myLinks");
ul.addEventListener("click", function (e) {
if (e.target.tagName.toLowerCase() === "li") {
console.log(e.target.innerText);
}
})
当我们点击 li 的时候,由于事件冒泡传播,所以当事件传播到 ul 的时候,会被我们绑定在 ul 上的事件处理程序捕获,然后在函数内部我们利用 event.target 会指向目标元素的特点来判断用户点击的是否是 li ,然后在执行对应的逻辑需求。
使用事件委托还有一个优点就是当我们动态向我们绑定了事件处理程序的上册元素中添加新的元素时,事件处理程序对这个新的元素也会生效,而传统的绑定事件方法则不行。
最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。 虽然 mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。(因为当鼠标从一个元素移到其子节点时,或者当鼠标移出该元素时,都会触发 mouseout 事件。)
移除事件处理程序
移除事件处理程序更像一个 程序员 来维护的垃圾回收方式。每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就 会建立一个连接。这种连接越多,页面执行起来就越慢。采用事件委托的方式能有效的减少连接的数量,但是一些残留在内存中的未被回收的空事件处理程序也是影响页面性能的一个原因。
如果我们绑定了事件处理程序的元素被 removeChild(), replaceChild()或者innerHTML 方法删除或替换的时候,原来添加到元素中的事件处理程序很可能没有被当作垃圾回收,这时候用 removeEventListener() 或者 element.onclick = null 来手动移除事件处理程序是个不错的选择。
在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。
自定义事件
自定义事件与浏览器定义的事件并没有什么不同,一样能够传播,能够指定事件处理程序。有了自定义事件以后,我们可以在任意时刻触发特定的事件。对于复杂页面不同功能模块之间的解耦有很大的帮助。同时自定义事件能够实现对全局的广播,这在复杂的应用中有很大的作用。
创建自定义事件
Events 可以使用 Event 构造函数创建如下:
var event = new Event('build');
// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);
// Dispatch the event.
elem.dispatchEvent(event);
添加自定义数据
要向事件对象添加更多数据,可以使用 CustomEvent,detail 属性可用于传递自定义数据
CustomEvent 接口可以为 event 对象添加更多的数据。例如,event 可以创建如下:
var event = new CustomEvent('build', { 'detail': elem.dataset.time });
访问自定义数据,在事件处理程序中的回调函数中使用:
function eventHandler(e) {
log('The time is: ' + e.detail);
}
元素可以侦听尚未创建的事件:
<form>
<textarea></textarea>
</form>
<script>
const form = document.querySelector('form');
const textarea = document.querySelector('textarea');
form.addEventListener('awesome', e => console.log(e.detail.text()));
textarea.addEventListener('input', function() {
// Create and dispatch/trigger an event on the fly
// Note: Optionally, we've also leveraged the "function expression" (instead of the "arrow function expression") so "this" will represent the element
this.dispatchEvent(new CustomEvent('awesome', { bubbles: true, detail: { text: () => textarea.value } }))
});
</script>
javascript高级程序设计中的模拟事件章节中的方法都已经废弃,想要了解自定义事件的查看 MDN
以上所述就是小编给大家介绍的《JS事件详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 详解JS事件 - 事件模型/事件流/事件代理/事件对象/自定义事件
- Oracle等待事件内容详解
- nginx事件模块结构体详解
- 【WEB系列】SSE服务器发送事件详解
- SpringBoot内置生命周期事件详解 SpringBoot源码(十)
- NodeJS中的事件(EventEmitter) API详解(附源码)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
实战移动互联网营销
江礼坤 / 机械工业出版社 / 2016-1 / 79.00
移动互联网的兴起,又为企业带来了新的挑战与机遇!越来越多的人,看到了移动互联网的价值与前景,但是在具体操作时,移动互联网具体如何玩?企业如何向移动互联网转型?如何通过移动互联网做营销?等等一系列问题,接踵而至。虽然目前相关的资料和文章很多,但是都过于零散,让人看完后,还是无从下手。而本书旨在成为移动互联网营销领域标准的工具书、参考书,为大家呈现一个系统、全面的移动互联网营销体系。让大家从思维模式到......一起来看看 《实战移动互联网营销》 这本书的介绍吧!