考点一:生命周期
在new Vue()
之后。 Vue 会调用_init
函数进行初始化,也就是这里的init
过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过Object.defineProperty
设置setter
与getter
函数,用来实现「响应式」以及「依赖收集」。
vue生命周期总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后。
- beforeCreate (创建前)在beforeCreat阶段,vue实例的挂载元el还没有。vue实例的挂载元素
$el
和数据对象data
都是undefined
, 还未初始化; - created (创建后) 完成了
data
数据初始化,el
还未初始化; - beforeMount (载入前) vue实例的
$el
和data
都初始化了, 相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上; - mounted (载入后) 在
el
被新创建的vm.$el
替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html
内容替换el
属性指向的DOM
对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。 - beforeUpdate (更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
- updated (更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。当data变化时,会触发beforeUpdate和updated方法;
- beforeDestroy (销毁前) 在实例销毁之前调用。实例仍然完全可用。
- destroyed (销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在。
考点二:双向绑定
把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty() 把这些属性全部转为 getter/setter。
vue.js是采用数据劫持,并结合发布者-订阅者的模式:通过Object.defineProperty()来劫持vue中各个属性的setter、getter,在数据变动的时候再发布消息给订阅者,触发相应的监听回调。
- 当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。 - MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
考点三:计算属性和侦听器
1.计算属性
1.1计算属性例子:
<body>
<div id="root">
姓:<input v-model="firstName" type="text">
名:<input v-model="lastName" type="text">
<div>{{fullName}}</div>
</div>
<script>
new Vue({
el: "#root",
data: {
firstName: '',
lastName: ''
},
computed: {
fullName: function() {
return this.firstName + "" + this.lastName
}
}
})
</script>
</body>
![c1e4eea5f1ca556f5afdd68c95c48479.png](https://i-blog.csdnimg.cn/blog_migrate/b03e928ed2ca235c5c617f80e9c500bd.jpeg)
https://jsbin.com/lixuwujaqe/edit?html,output
1.2 关于计算属性是否会影响原值的面试题:
输入框b的值是由输入框a的值+1计算得到的,若认为更改b 的值后a的值会相应变化吗?
<body>
<div id="vue_det">
<input type="text" v-model="a">
<input type="text" v-model="b" @input="handleInput">
<button @click="handlePrint">打印a、 b的值a:{{a}} b: {{b}}</button>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#vue_det',
data: {
a: 1
},
computed: {
b: function() {
return (this.a + 1)
}
},
methods: {
handleInput() {
console.log(this.a, 'a')
console.log(this.b, 'b')
},
handlePrint() {
console.log(this.a, 1)
console.log(this.b, 2)
}
}
})
</script>
</body>
运行结果为不会。
![ff55b3e28d7227557bc383fa30afd2f6.png](https://i-blog.csdnimg.cn/blog_migrate/fb19f7f62e5cac24ac186c89903bd693.jpeg)
http://js.jirengu.com/falubuyefu/1/edit
2.侦听器
侦听器例子:
<body>
<div id="root">
姓:<input v-model="firstName" type="text">
名:<input v-model="lastName" type="text">
<div>{{fullName}}</div>
<span>{{count}}</span>
</div>
<script>
new Vue({
el: "#root",
data: {
firstName: '',
lastName: '',
count: 0
},
computed: {
fullName: function() {
return this.firstName + "" + this.lastName
}
},
watch: {
firstName: function() {
this.count++
},
lastName: function() {
this.count++
}
}
})
</script>
</body>
![f7f015e2af953a5b897558dc21cbdeef.png](https://i-blog.csdnimg.cn/blog_migrate/9c231ccbdc091a92b0ca3273d62b2ac2.jpeg)
当然,也可以侦听计算属性,效果是一样的:https://jsbin.com/pusuhuviba/edit?html,output
![f6f326273ecbf9045d014b41905a1168.png](https://i-blog.csdnimg.cn/blog_migrate/b0ccc5c44aca3fbddf0d5439aae631c0.jpeg)
考点四:组件通信
先由“组件”开始讲,庞大的大型项目可以细分拆成很多部分,每一部分就是一个小型的组件,这样维护每个组件就相对简单很多。实际上,每一个组件又是一个new Vue()产生的实例。任何一个Vue项目都是由千千万万个 Vue的实例组成的。
例子1:一个vue实现的Todo: https://jsbin.com/maxuziwaka/edit?html,output
![56dd8101eb5ca7f182baf998b3332575.png](https://i-blog.csdnimg.cn/blog_migrate/fc0edf4c46228f85df45a37c64c6da5e.jpeg)
<body>
<div id="root">
<div>
<input v-model="inputValue" type="text">
<button @click="handleSubmit">提交</button>
</div>
<ul>
<todo-item
v-for="(item, index) of list"
:key="index"
:content="item"
>
</todo-item>
</ul>
</div>
<script>
// 这种方式定义的是全局组件
Vue.component('todo-item', {
props: ['content'],
template: '<li>{{content}}</li>'
})
new Vue({
el: "#root",
data: {
inputValue: '',
list:[]
},
methods: {
handleSubmit: function() {
this.list.push(this.inputValue)
}
}
})
</script>
</body>
或者也可以定义成局部组件:
<body>
<div id="root">
<div>
<input v-model="inputValue" type="text">
<button @click="handleSubmit">提交</button>
</div>
<ul>
<todo-item
v-for="(item, index) of list"
:key="index"
:content="item"
>
</todo-item>
</ul>
</div>
<script>
// 这种方式定义的是局部组件
var TodoItem = {
props: ['content'],
template: '<li>{{content}}</li>'
}
new Vue({
el: "#root",
components: {
'todo-item': TodoItem
},
data: {
inputValue: '',
list: []
},
methods: {
handleSubmit: function() {
this.list.push(this.inputValue)
}
}
})
</script>
</body>
如图:https://jsbin.com/revatudoho/edit?html,output
![c9eda3480166d0921d54d60d436c36d1.png](https://i-blog.csdnimg.cn/blog_migrate/c0e36b68ade76bc0d10160c2dd345200.jpeg)
一、父子组件之间通信 1.子组件通过props获取父组件的传值,父组件通过v-on监听子组件使用$emit触发的自定义事件。
1.1 父组件通过属性的方式向子组件传递值,如上文中例子1的:content="item"的写法。
1.2 子组件要向父组件通信的话需要使用发布-订阅模式:
例如:子组件里的每一项li添加一项点击就删除该行的功能,则需要传递被点击行的序号信息给父组件。https://jsbin.com/munodusaji/edit?html,console,output
<body>
<div id="root">
<div>
<input v-model="inputValue" type="text">
<button @click="handleSubmit">提交</button>
</div>
<ul>
<todo-item
v-for="(item, index) of list"
:key="index"
:content="item"
:index="index"
@delete="handleDelete"
>
</todo-item>
</ul>
</div>
<script>
// 这种方式定义的是全局组件
Vue.component('todo-item', {
props: ['content', 'index'],
template: '<li @click="handleClick">{{content}}</li>',
methods: {
handleClick: function() {
this.$emit('delete', this.index)
}
}
})
new Vue({
el: "#root",
data: {
inputValue: '',
list:[]
},
methods: {
handleSubmit: function() {
this.list.push(this.inputValue)
},
handleDelete: function(index) {
console.log(index)
this.list.splice(index, 1)
}
}
})
</script>
</body>
![9ce54aa620420b89fb44cbc029375155.png](https://i-blog.csdnimg.cn/blog_migrate/20688b1c173ff3611a5ce7560f67a0a8.jpeg)
写在vue-cli搭建的项目里看起来这种父子通信的结构可能更为直观一些:
![b33bca32194bcdc6442089d622088f20.png](https://i-blog.csdnimg.cn/blog_migrate/9717f2bcce324ec83ad250cb3669f12b.jpeg)
“TodoList“是父组件(即左编辑栏内),其内部用到了“<todo-item></todo-item>”的子组件,子组件是“TodoItem”(即右编辑栏内)。子组件通过props获取父组件的传值content和index,父组件通过v-on事件监听子组件使用$emit自定义的方法delete。
![9f1ca88f275aac43164cf5e8f883a9a2.png](https://i-blog.csdnimg.cn/blog_migrate/89e168dca89ba2044620dd8f74232ff8.jpeg)
2. 父子组件采用$parent/$children通信。
![dd320386ed3a21a9b5acbbf46feb39ea.png](https://i-blog.csdnimg.cn/blog_migrate/bf15b6a84a9d03e01f23f516072befc8.jpeg)
见Vue文档
例子2.1
父组件想要拿到某个子组件内的值realIndex(即在0开头的index的基础上自己加工+1后的子组件的值)要通过$children选取子组件实例。
![90192e5a4699f15a12cdb164d26713ee.png](https://i-blog.csdnimg.cn/blog_migrate/312de934aceddc89abe1b8f020ed5656.jpeg)
![891811a4769284a5624a0597a5a894b7.png](https://i-blog.csdnimg.cn/blog_migrate/4a12b0461d089923124e57bc862307cb.jpeg)
而某个子组件想要获取其父组件的值需要使用$parent得到其父组件实例。
![ddce8bc0b77cf10ed547fda5ff035ce1.png](https://i-blog.csdnimg.cn/blog_migrate/9c2a618572d926948b7cdf61d944dc9b.jpeg)
![a24b2fb0ad3432de2c5f022defdaab22.png](https://i-blog.csdnimg.cn/blog_migrate/52b0edc0ad70b98b1ce84d90ca46d4db.jpeg)
3.ref方法
官方文档API和应用如下:
![7af0d7118e465cd7336fef4f75d11b8a.png](https://i-blog.csdnimg.cn/blog_migrate/664f4bdb4580f86febc42205414e7c2b.jpeg)
![6fab69cadcc466b18542dba1f7c5dc5b.png](https://i-blog.csdnimg.cn/blog_migrate/916ab062da5cc7a7155f9d34b6034b10.jpeg)
例子3.1 比如父组件TodoList.vue想要得到子组件<todo-item>的相关信息可以通过ref特性为这个子组件赋予一个ID引用,然后可以使用 this.$refs.ID来访问这个子组件实例。
注:当ref
和v-for
一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组。
![46d246c23734686d3070ace53afffa85.png](https://i-blog.csdnimg.cn/blog_migrate/62cdb57a3701702ec1b38ed6e5303ba1.jpeg)
![02861b6c4dbc2e78b1d5aea6011bf692.png](https://i-blog.csdnimg.cn/blog_migrate/99a2b7b5cb1c05294f6664926b4481a5.jpeg)
4. slot方法
<slot>插槽可用于父组件向子组件传递HTML等,子组件内的数据想要传递到父组件作用域内使用则需要借助“作用域插槽”来在子组件文件内的<slot>元素上绑定一个特性即“插槽prop”。
详见本文第五节“slot插槽”
二、 跨级通信——爷孙组件典型
例子二 如下图,我们在刚才的项目里主App文件(实际上我们改叫做TodoList.vue了)里新插入里一个子组件<Advertise>,其内部又引入一个可以选择广告服务类型的<JoinKind>子组件(它相对于<TodoList>来说是其孙组件)。
// advertise.vue
<template>
<div class="footer">
<p>广告位长期招租哦 </p>
<p>{{contact}}</p>
<join-kind></join-kind>
</div>
</template>
<script>
import joinKind from "./kind.vue";
export default {
name: 'Advertise',
components: {
'join-kind': joinKind
},
data() {
return {
contact: '800-***-8820'
}
},
methods: {
}
}
</script>
![21ee5fb4b745b19cfd2a16a05fa36661.png](https://i-blog.csdnimg.cn/blog_migrate/7911ab58f058d75a87d16096bce78a4b.jpeg)
1. provide/inject 方法
依赖注入不论子组件嵌套得有多深,只要调用了inject那么就能注入provide中的数据,而不局限于只能从当前父组件的props属性中得数据。provide
和inject
主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。并且这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
![614157631ce8eecfc16e08e0cce2219d.png](https://i-blog.csdnimg.cn/blog_migrate/b593a36178df666bc9cf108790ff1d90.jpeg)
![549e9d969875ae1333475bbe9e8846e3.png](https://i-blog.csdnimg.cn/blog_migrate/3252a62c2634fbbeddb118d37b688588.jpeg)
"依赖注入"简单来说就是:父组件中通过 provide来提供变量,然后在子组件或者孙组件等想要获取的地方中通过inject来注入变量 。
![3c4aba5fc931c83260932f02d5d9aded.png](https://i-blog.csdnimg.cn/blog_migrate/ff6945de4743006a439671de20baec32.jpeg)
![2999aa66f34441b85ecd93aed04e349b.png](https://i-blog.csdnimg.cn/blog_migrate/4b9c4042ef5f0852830f78c207808f3e.jpeg)
2. $attrs/$listeners 方法
![8453b7463b9660474084305c983dd9a9.png](https://i-blog.csdnimg.cn/blog_migrate/7063cac054cdef8e59df58ab958b1e30.jpeg)
$attrs
和$listeners
属性像两个收纳箱,一个负责收纳属性,一个负责收纳事件。可以使用v-bind="$attrs"
将父组件中不被认为props
特性绑定的属性传递给子组件(这样孙组件就能在子组件得到的属性上拿到父组件数据)。
![4d5b8dfb29dc34223124f82ee6ed9317.png](https://i-blog.csdnimg.cn/blog_migrate/105c0b494faacbede574708b0bb1c46a.jpeg)
![80b4c93b28c043d7de375de4f818545f.png](https://i-blog.csdnimg.cn/blog_migrate/399add85b3d4b165dcf5db13c3791bc4.jpeg)
![53e9eacc51d1a4667e6969f3f3b11e63.png](https://i-blog.csdnimg.cn/blog_migrate/b3760403bd2996539c46eb5c49f0be88.jpeg)
还可以使用v-on="$listeners"将父组件中非.native的子组件绑定事件传递给子组件(这样孙组件就能在子组件得到的属性上调用父文件里的事件)。
![6b328a964bad709a8a72c18e0467bdcb.png](https://i-blog.csdnimg.cn/blog_migrate/c29300d323fdd93e086f0631159e33cc.jpeg)
![bb4d30ff6dd3c8fb63368dbfddc657a2.png](https://i-blog.csdnimg.cn/blog_migrate/3c5d8ba6dfc99f38f9f723eb1d2628a8.jpeg)
![a569a563900d977d5b244099e0378713.png](https://i-blog.csdnimg.cn/blog_migrate/78c4f8a08a3d3bf3e03e845b63ae8e0c.jpeg)
附注:$attrs通常配合 inheritAttrs 选项一起使用,当我们在组件上赋予了一个非Prop 声明时,编译之后的代码会把这些个属性都当成原始属性对待,添加到 html 原生标签上,看上面的代码编译之后的样子:(子组件文件里我们写里inheritAttr:false了故子组件在DOM里没编译出那些属性,但孙组件里编译出来了)
![df087b326035a7306db55f0efc0fc2fa.png](https://i-blog.csdnimg.cn/blog_migrate/836b46329dc17e472720f96c7d6cdb53.jpeg)
我们现在在孙组件里也加上inheritAttrs: false则孙组件也不会露出那些属性了:
![1558422956474430e9e5b3829a3d7515.png](https://i-blog.csdnimg.cn/blog_migrate/329a99984db63345bacc6cc1111f5bee.jpeg)
![84ec97550b806c1e55b21a925b039967.png](https://i-blog.csdnimg.cn/blog_migrate/06c88130081b6854cd1e3a40ad902aff.jpeg)
![e62512d516a74664ba97b6819e344c3d.png](https://i-blog.csdnimg.cn/blog_migrate/0904e6a06cb6634975ad343b6588e8b2.jpeg)
3. 事件总线eventbus 方法
在官网上有个藏得很深的使用例子:
![1f0c99bd945b942ff008a40a8f577f63.png](https://i-blog.csdnimg.cn/blog_migrate/c664dc411f12ed169bfab8fdd52ee285.jpeg)
![7fcf3b3b74f719eabe1833cafd71907e.png](https://i-blog.csdnimg.cn/blog_migrate/7b095f2277a9e761c4beef57bcb7edc6.jpeg)
(1)首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它.
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)假设你有两个兄弟组件:additionNum
和showNum
, addtionNum.vue 中通过EventBus.$emit()发送事件,showNum.vue 中通过EventBus.$on()接收事件。这样就实现了在组件additionNum.vue
中点击相加按钮, 在showNum.vue
中利用传递来的num
展示求和的结果。
![3143938697db6919d629cc29d50b74e2.png](https://i-blog.csdnimg.cn/blog_migrate/c60d253aaf80031a9d2dc725d6a4cd97.jpeg)
![ee13b39e743bd714aac4b8145432d810.png](https://i-blog.csdnimg.cn/blog_migrate/78ee4d128732d55d22bf53eba0d8fe87.jpeg)
若需要移除事件的监听:EventBus.$off('addition', {})即可。
4. Vuex全局状态管理
在main.js引入store,注入,新建一个目录store,….. export 等
相关使用实例请见下一篇文章:Vuex使用解析
考点五:slot 插槽
![e2582e53f67d0b8df01760d937327db9.png](https://i-blog.csdnimg.cn/blog_migrate/6b826310c7265de00f868e19122d3dbb.jpeg)
想象这样的场景: 正常情况下<Child><span>HelloWorld</span></Child>写在组件 Child标签里的span标签会被组件的<template></template>里内容替换覆盖掉,当你想要在父文件的<Child>组件标签内传进内容时就会用到插槽。
例如:<advertise></advertise>是在父组件TodoList.vue里调用的子组件,想要在该文件内往子组件标签插进文字或者HTML是无法直接实现的,
![6c4814f26d89c47487d1195a59317fd9.png](https://i-blog.csdnimg.cn/blog_migrate/176ec38c7a3e503ad3a413f7d58b1252.jpeg)
![e0e5030f16f3df74aa527f6694549f78.png](https://i-blog.csdnimg.cn/blog_migrate/98308a00bb51cfa6ec3962f1feaf2c3b.jpeg)
而写成这样:在子组件的文件里用slot标签“占位”一下,就能起到替换你想要内容的作用。
![872f30e057762e49438930ce313f3630.png](https://i-blog.csdnimg.cn/blog_migrate/7a3e44f1b7997af153ed8d8712dc5e5c.jpeg)
![45efe7fb1e981bc69469ba66ef609071.png](https://i-blog.csdnimg.cn/blog_migrate/f26006e57c8792a1da90fcc6bfe0b975.jpeg)
插槽可通俗理解成“占坑”,即在组件模板中提前占座,待使用组件标签时其内的内容就会自动落座替换坑位(即对应<slot></slot>的位置)。可以通过slot插槽向组件内部指定位置传递内容。父组件向子组件传递“标签”可以通过slot完成。
在slot标签添加样式无效。拥有命名的插槽不能被不含slot属性的标签内容替换,会显示slot的默认值(具名slot具有对应性)。例如:
![4a7b7e4c6d36ecd2cf4b0f2fe7d7489a.png](https://i-blog.csdnimg.cn/blog_migrate/fc82774fbead245d50534f3d56ca64a9.jpeg)
![99dea114fd669f0bcf637ae840e7d4ab.png](https://i-blog.csdnimg.cn/blog_migrate/2e8de0729383403015125e59758b35f5.jpeg)
(一)关于具名插槽:有时我们需要多个插槽,对于这样的情况,<slot>
元素有一个特殊的特性:name
。一个不带name
的<slot>
出口会带有隐含的名字“default”。在向具名插槽提供内容的时候,我们可以在一个<template>
元素上使用v-slot
指令,并以v-slot
的参数的形式提供其名称,现在<template>
元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有v-slot
的<template>
中的内容都会被视为默认插槽的内容。组件中有多个命名的slot插槽时,可以实现父组件对子组件的指定位置显示内容或传参。
例如:下面的2和4均属于“任何没有被包裹在带有v-slot的<template>中的内容都会被视为默认插槽(<slot></slot>)的内容。”
![05f8881e4370516143c5aa4f38856a64.png](https://i-blog.csdnimg.cn/blog_migrate/913aca1f77c0612d31408b353f0a6b83.jpeg)
![d51693c4fccc7c96f9af57dd91cad41b.png](https://i-blog.csdnimg.cn/blog_migrate/41439de64a39852be71c5e4a215d29e6.jpeg)
(二)关于作用域插槽:
![5d5d5b1f2c9e29d1cb676f994cb0ba62.png](https://i-blog.csdnimg.cn/blog_migrate/a717adcfd346352edb76fbe0c21f0e8a.jpeg)
简单说就是子组件模版内的数据在父文件里是获取不到的,因为作用域不同。现在想要在父级作用域中使用子组件作用域的数据就必须借助“作用域插槽”——比如将子组件内某个量maneger作为<slot></slot>的一个特性绑定上去:<slot name="name" v-bind:person="maneger">{{person}}</slot>,在父级作用域里我们可以给v-slot带一个值来定义我们提供的插槽prop的名字:
<advertise>
<template v-slot:name="childProps">
<i>{{childProps.person}}</i>
</template>
</advertise>
例如:
![da321bf8c74244e9651e3bcdba88ec40.png](https://i-blog.csdnimg.cn/blog_migrate/f2bfc7861bc6ed109f4bb1567988568c.jpeg)
![39764d0c890178cd76b7f78001cfb1b0.png](https://i-blog.csdnimg.cn/blog_migrate/42d9451d1d22ac35a19e8aea19dd4a4b.jpeg)
总结:<slot>插槽可用于父组件向子组件传递HTML等,子组件内的数据想要传递到父组件作用域内使用则需要借助“作用域插槽”来在子组件文件内的<slot>元素上绑定一个特性即“插槽prop”。
本文参考:
如何在Vue面试环节,证明自己值月薪15K?_慕课手记www.imooc.com![b974faa6acca974693ec2904a96be1f5.png](https://i-blog.csdnimg.cn/blog_migrate/229dfa0e6f48b293b245b3ee575e5c04.jpeg)
![b974faa6acca974693ec2904a96be1f5.png](https://i-blog.csdnimg.cn/blog_migrate/229dfa0e6f48b293b245b3ee575e5c04.jpeg)
![b974faa6acca974693ec2904a96be1f5.png](https://i-blog.csdnimg.cn/blog_migrate/229dfa0e6f48b293b245b3ee575e5c04.jpeg)
![b974faa6acca974693ec2904a96be1f5.png](https://i-blog.csdnimg.cn/blog_migrate/229dfa0e6f48b293b245b3ee575e5c04.jpeg)