前端【VUE】06-vue【组件组成】【组件通信【父传子、子传父、非父子传递】】【进阶语法【.sync修饰符、v-model原理、$refs、自定义指令、Vue异步更新、$nextTick】】【子组件修改父组件的值】
一、组件的三大组成部分(结构/样式/逻辑)
组件的三大组成部分
组件的样式冲突scoped
1、components目录下
components/BaseOne.vue
1 <template> 2 <div class="base-one"> 3 BaseOne 4 </div> 5 </template> 6 7 <script> 8 export default { 9 10 } 11 </script> 12 13 <style scoped> 14 /* 15 1.style中的样式 默认是作用到全局的 16 2.加上scoped可以让样式变成局部样式 17 18 组件都应该有独立的样式,推荐加scoped(原理) 19 ----------------------------------------------------- 20 scoped原理: 21 1.给当前组件模板的所有元素,都会添加上一个自定义属性 22 data-v-hash值 23 data-v-5f6a9d56 用于区分开不通的组件 24 2.css选择器后面,被自动处理,添加上了属性选择器 25 div[data-v-5f6a9d56] 26 */ 27 div{ 28 border: 3px solid blue; 29 margin: 30px; 30 } 31 </style>
components/BaseTwo.vue
1 <template> 2 <div class="base-one"> 3 BaseTwo 4 </div> 5 </template> 6 7 <script> 8 export default { 9 10 } 11 </script> 12 13 <style scoped> 14 div{ 15 border: 3px solid red; 16 margin: 30px; 17 } 18 </style>
2、根组件App.vue
1 <template> 2 <div id="app"> 3 <BaseOne></BaseOne> 4 <BaseTwo></BaseTwo> 5 </div> 6 </template> 7 8 <script> 9 import BaseOne from './components/BaseOne' 10 import BaseTwo from './components/BaseTwo' 11 export default { 12 name: 'App', 13 components: { 14 BaseOne, 15 BaseTwo 16 } 17 } 18 </script>
data 是一个函数
1、components/BaseCount.vue
1 <template> 2 <div class="base-count"> 3 <button @click="count--">-</button> 4 <span>{{ count }}</span> 5 <button @click="count++">+</button> 6 </div> 7 </template> 8 9 <script> 10 export default { 11 data: function () { 12 return { 13 count: 100, 14 } 15 }, 16 } 17 </script> 18 19 <style> 20 .base-count { 21 margin: 20px; 22 } 23 </style>
2、根组件App.vue
1 <template> 2 <div class="app"> 3 <!-- 使用导入的组件, 每个组件都维护了自己的data,加减都是只影响自己组件内的data数据 --> 4 <baseCount></baseCount> 5 <baseCount></baseCount> 6 <baseCount></baseCount> 7 </div> 8 </template> 9 10 <script> 11 import baseCount from './components/BaseCount' 12 export default { 13 components: { 14 baseCount, 15 }, 16 } 17 </script> 18 19 <style> 20 </style>
组件三大组成部分的注意点
1. 结构:有且只能一个根元素2. 样式:默认全局样式,加上scoped局部样式
3. 逻辑:data是一个函数,保证数据独立。
二、组件通信
什么是组件通信
不同的组件关系和组件通信方案分类
组件通信解决方案
父子通信流程图
父组件传递数据到子组件
父组件通过props将数据传递给子组件
1、components/Son.vue
1 <template> 2 <div class="son" style="border:3px solid #000;margin:10px"> 3 <!-- 3.通过插值表达式 直接使用props的值 --> 4 我是Son组件 {{title}} 5 </div> 6 </template> 7 8 <script> 9 export default { 10 name: 'Son-Child', 11 // 2.通过props来接受 12 props:['title'] 13 } 14 </script> 15 <style> 16 </style>
2、根组件App.vue
1 <template> 2 <div class="app" style="border: 3px solid #000; margin: 10px"> 3 我是APP组件 4 <!-- 1.给组件标签,添加属性方式 赋值 :title 中的title是自定义的属性名称, 需要与子组件中的props中保持一致 而:title="myTitle"中的myTitle是data中的变量 --> 5 <Son :title="myTitle"></Son> 6 </div> 7 </template> 8 9 <script> 10 import Son from './components/Son.vue' 11 export default { 12 name: 'App', 13 data() { 14 return { 15 myTitle: '学前端,就来黑马程序员', 16 } 17 }, 18 components: { // 使用导入的组件 19 Son, 20 }, 21 } 22 </script> 23 24 <style> 25 </style>
子组件传递数据给父组件
子组件利用$emit通知父组件,进行修改更新
1、components/Son.vue
1 <template> 2 <div class="son" style="border: 3px solid #000; margin: 10px"> 3 我是Son组件 {{ title }} 4 <!-- 方式1 --> 5 <button @click="changeFn">修改title</button> 6 <!-- 方式2:行内式 --> 7 <button @click="$emit('changTitle','我是传递的参数')">修改title</button> 8 </div> 9 </template> 10 11 <script> 12 export default { 13 name: 'Son-Child', 14 props: ['title'], // 接收父组件传的值 15 methods: { 16 changeFn() { 17 // 通过this.$emit() 向父组件发送通知 18 // changTitle通知父组件的函数, 19 this.$emit('changTitle','我是传递的参数') 20 }, 21 }, 22 } 23 </script> 24 <style> 25 </style>
2、根组件App.vue
1 <template> 2 <div class="app" style="border: 3px solid #000; margin: 10px"> 3 我是APP组件 4 <!-- 2.父组件对子组件的消息进行监听 --> 5 <!-- @changTitle 此处的changTitle必须与子组件调用的$emit()第一个参数保持一致 监听事件 --> 6 <Son :title="myTitle" @changTitle="handleChange"></Son> 7 </div> 8 </template> 9 10 <script> 11 import Son from './components/Son.vue' 12 export default { 13 name: 'App', 14 data() { 15 return { 16 myTitle: '学前端', 17 } 18 }, 19 components: { 20 Son, 21 }, 22 methods: { 23 // 3.提供处理函数,提供逻辑 24 handleChange(newTitle) { 25 this.myTitle = newTitle 26 }, 27 }, 28 } 29 </script> 30 <style> 31 </style>
什么是prop
props传值
1、components/UserInfo.vue
1 <template> 2 <div class="userinfo"> 3 <h3>我是个人信息组件</h3> 4 <div>姓名:{{username}}</div> 5 <div>年龄:{{age}}</div> 6 <div>是否单身:{{isSingle}}</div> 7 <div>座驾:{{car.brand}}</div> 8 <div>兴趣爱好:{{hobby.join('、')}}</div> 9 </div> 10 </template> 11 12 <script> 13 export default { 14 props:['username','age','isSingle','car','hobby'] 15 } 16 </script> 17 18 <style> 19 .userinfo { 20 width: 300px; 21 border: 3px solid #000; 22 padding: 20px; 23 } 24 .userinfo > div { 25 margin: 20px 10px; 26 } 27 </style>
2、根组件App.vue
1 <template> 2 <div class="app"> 3 <UserInfo 4 :username="username" 5 :age="age" 6 :isSingle="isSingle" 7 :car="car" 8 :hobby="hobby" 9 ></UserInfo> 10 </div> 11 </template> 12 13 <script> 14 import UserInfo from './components/UserInfo.vue' 15 export default { 16 data() { 17 return { 18 username: '小帅', 19 age: 28, 20 isSingle: true, 21 car: { 22 brand: '宝马', 23 }, 24 hobby: ['篮球', '足球', '羽毛球'], 25 } 26 }, 27 components: { 28 UserInfo, 29 }, 30 } 31 </script> 32 <style> 33 </style>
props校验
1、components/BaseProgress.vue
1 <template> 2 <div class="base-progress"> 3 <div class="inner" :style="{ width: w + '%' }"> 4 <span>{{ w }}%</span> 5 </div> 6 </div> 7 </template> 8 9 <script> 10 export default { 11 // 1.基础写法(类型校验) 12 // props: { 13 // w: Number, 14 // }, 15 16 // 2.完整写法(类型、默认值、非空、自定义校验) 17 props: { 18 w: { 19 type: Number, 20 required: true, 21 default: 0, 22 validator(val) { 23 // console.log(val) 24 if (val >= 100 || val <= 0) { 25 console.error('传入的范围必须是0-100之间') 26 return false 27 } else { 28 return true 29 } 30 }, 31 }, 32 }, 33 } 34 </script> 35 36 <style scoped> 37 .base-progress { 38 height: 26px; 39 width: 400px; 40 border-radius: 15px; 41 background-color: #272425; 42 border: 3px solid #272425; 43 box-sizing: border-box; 44 margin-bottom: 30px; 45 } 46 .inner { 47 position: relative; 48 background: #379bff; 49 border-radius: 15px; 50 height: 25px; 51 box-sizing: border-box; 52 left: -3px; 53 top: -2px; 54 } 55 .inner span { 56 position: absolute; 57 right: 0; 58 top: 26px; 59 } 60 </style>
2、根组件App.vue
1 <template> 2 <div class="app"> 3 <BaseProgress :w="width"></BaseProgress> 4 </div> 5 </template> 6 7 <script> 8 import BaseProgress from './components/BaseProgress.vue' 9 export default { 10 data() { 11 return { 12 width: 30, 13 } 14 }, 15 components: { 16 BaseProgress, 17 }, 18 } 19 </script> 20 <style> 21 </style>
prop & data、单向数据流
1、components/BaseCount.vue
1 <template> 2 <div class="base-count"> 3 <button @click="handleSub">-</button> 4 <span>{{ count }}</span> 5 <button @click="handleAdd">+</button> 6 </div> 7 </template> 8 9 <script> 10 export default { 11 // 1.自己的数据随便修改 (谁的数据 谁负责) 12 // data () { 13 // return { 14 // count: 100, 15 // } 16 // }, 17 // 2.外部传过来的数据 不能随便修改 18 props: { 19 count: { 20 type: Number, 21 }, 22 }, 23 methods: { 24 handleSub() { 25 // 通知父组件数据变化 26 this.$emit('changeCount', this.count - 1) 27 }, 28 handleAdd() { 29 this.$emit('changeCount', this.count + 1) 30 }, 31 }, 32 } 33 </script> 34 35 <style> 36 .base-count { 37 margin: 20px; 38 } 39 </style>
2、根组件App.vue
1 <template> 2 <div class="app"> 3 <BaseCount :count="count" @changeCount="handleChange"></BaseCount> 4 </div> 5 </template> 6 7 <script> 8 import BaseCount from './components/BaseCount.vue' 9 export default { 10 components:{ 11 BaseCount 12 }, 13 data(){ 14 return { 15 count:100 16 } 17 }, 18 methods:{ 19 handleChange(newVal){ 20 // console.log(newVal); 21 this.count = newVal 22 } 23 } 24 } 25 </script> 27 <style> 29 </style>
子组件修改父组件的值的三种方式
如果父组件传给子组件的数据,子组件想修改,有以下三种方式:1、子传父,在子组件通知父组件修改
2、在子组件通过this.$parent.父组件中的属性 = xxxx
3、通过在父组件中传值的时候,属性名加上.sync,放权给子组件修改props中的数据,然后通过this.emit('update:props传过来的属性名', 要修改成什么值)
组件通信案例:小黑记事本-组件版
1、组件目录下
components/TodoFooter.vue
1 <template> 2 <!-- 统计和清空 --> 3 <footer class="footer"> 4 <!-- 统计 --> 5 <span class="todo-count" 6 >合 计:<strong> {{ list.length }} </strong></span 7 > 8 <!-- 清空 --> 9 <button class="clear-completed" @click="clear">清空任务</button> 10 </footer> 11 </template> 12 13 <script> 14 export default { 15 props: { 16 list: { 17 type: Array, 18 }, 19 }, 20 methods:{ 21 clear(){ 22 this.$emit('clear') 23 } 24 } 25 } 26 </script> 27 28 <style> 29 </style>
components/TodoHeader.vue
1 <template> 2 <!-- 输入框 --> 3 <header class="header"> 4 <h1>小黑记事本</h1> 5 <input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/> 6 <button class="add" @click="handleAdd">添加任务</button> 7 </header> 8 </template> 9 10 <script> 11 export default { 12 data(){ 13 return { 14 todoName:'' 15 } 16 }, 17 methods:{ 18 handleAdd(){ 19 // console.log(this.todoName) 20 this.$emit('add',this.todoName) 21 this.todoName = '' 22 } 23 } 24 } 25 </script> 26 27 <style> 28 29 </style>
components/TodoMain.vue
1 <template> 2 <!-- 列表区域 --> 3 <section class="main"> 4 <ul class="todo-list"> 5 <li class="todo" v-for="(item, index) in list" :key="item.id"> 6 <div class="view"> 7 <span class="index">{{ index + 1 }}.</span> 8 <label>{{ item.name }}</label> 9 <button class="destroy" @click="handleDel(item.id)"></button> 10 </div> 11 </li> 12 </ul> 13 </section> 14 </template> 15 16 <script> 17 export default { 18 props: { 19 list: { 20 type: Array, 21 }, 22 }, 23 methods: { 24 handleDel(id) { 25 this.$emit('del', id) 26 }, 27 }, 28 } 29 </script> 30 31 <style> 32 </style>
2、样式style/index.css
1 html, 2 body { 3 margin: 0; 4 padding: 0; 5 } 6 body { 7 background: #fff; 8 } 9 button { 10 margin: 0; 11 padding: 0; 12 border: 0; 13 background: none; 14 font-size: 100%; 15 vertical-align: baseline; 16 font-family: inherit; 17 font-weight: inherit; 18 color: inherit; 19 -webkit-appearance: none; 20 appearance: none; 21 -webkit-font-smoothing: antialiased; 22 -moz-osx-font-smoothing: grayscale; 23 } 24 25 body { 26 font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 27 line-height: 1.4em; 28 background: #f5f5f5; 29 color: #4d4d4d; 30 min-width: 230px; 31 max-width: 550px; 32 margin: 0 auto; 33 -webkit-font-smoothing: antialiased; 34 -moz-osx-font-smoothing: grayscale; 35 font-weight: 300; 36 } 37 38 :focus { 39 outline: 0; 40 } 41 42 .hidden { 43 display: none; 44 } 45 46 #app { 47 background: #fff; 48 margin: 180px 0 40px 0; 49 padding: 15px; 50 position: relative; 51 box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); 52 } 53 #app .header input { 54 border: 2px solid rgba(175, 47, 47, 0.8); 55 border-radius: 10px; 56 } 57 #app .add { 58 position: absolute; 59 right: 15px; 60 top: 15px; 61 height: 68px; 62 width: 140px; 63 text-align: center; 64 background-color: rgba(175, 47, 47, 0.8); 65 color: #fff; 66 cursor: pointer; 67 font-size: 18px; 68 border-radius: 0 10px 10px 0; 69 } 70 71 #app input::-webkit-input-placeholder { 72 font-style: italic; 73 font-weight: 300; 74 color: #e6e6e6; 75 } 76 77 #app input::-moz-placeholder { 78 font-style: italic; 79 font-weight: 300; 80 color: #e6e6e6; 81 } 82 83 #app input::input-placeholder { 84 font-style: italic; 85 font-weight: 300; 86 color: gray; 87 } 88 89 #app h1 { 90 position: absolute; 91 top: -120px; 92 width: 100%; 93 left: 50%; 94 transform: translateX(-50%); 95 font-size: 60px; 96 font-weight: 100; 97 text-align: center; 98 color: rgba(175, 47, 47, 0.8); 99 -webkit-text-rendering: optimizeLegibility; 100 -moz-text-rendering: optimizeLegibility; 101 text-rendering: optimizeLegibility; 102 } 103 104 .new-todo, 105 .edit { 106 position: relative; 107 margin: 0; 108 width: 100%; 109 font-size: 24px; 110 font-family: inherit; 111 font-weight: inherit; 112 line-height: 1.4em; 113 border: 0; 114 color: inherit; 115 padding: 6px; 116 box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 117 box-sizing: border-box; 118 -webkit-font-smoothing: antialiased; 119 -moz-osx-font-smoothing: grayscale; 120 } 121 122 .new-todo { 123 padding: 16px; 124 border: none; 125 background: rgba(0, 0, 0, 0.003); 126 box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); 127 } 128 129 .main { 130 position: relative; 131 z-index: 2; 132 } 133 134 .todo-list { 135 margin: 0; 136 padding: 0; 137 list-style: none; 138 overflow: hidden; 139 } 140 141 .todo-list li { 142 position: relative; 143 font-size: 24px; 144 height: 60px; 145 box-sizing: border-box; 146 border-bottom: 1px solid #e6e6e6; 147 } 148 149 .todo-list li:last-child { 150 border-bottom: none; 151 } 152 153 .todo-list .view .index { 154 position: absolute; 155 color: gray; 156 left: 10px; 157 top: 20px; 158 font-size: 22px; 159 } 160 161 .todo-list li .toggle { 162 text-align: center; 163 width: 40px; 164 /* auto, since non-WebKit browsers doesn't support input styling */ 165 height: auto; 166 position: absolute; 167 top: 0; 168 bottom: 0; 169 margin: auto 0; 170 border: none; /* Mobile Safari */ 171 -webkit-appearance: none; 172 appearance: none; 173 } 174 175 .todo-list li .toggle { 176 opacity: 0; 177 } 178 179 .todo-list li .toggle + label { 180 /* 181 Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 182 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 183 */ 184 background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 185 background-repeat: no-repeat; 186 background-position: center left; 187 } 188 189 .todo-list li .toggle:checked + label { 190 background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 191 } 192 193 .todo-list li label { 194 word-break: break-all; 195 padding: 15px 15px 15px 60px; 196 display: block; 197 line-height: 1.2; 198 transition: color 0.4s; 199 } 200 201 .todo-list li.completed label { 202 color: #d9d9d9; 203 text-decoration: line-through; 204 } 205 206 .todo-list li .destroy { 207 display: none; 208 position: absolute; 209 top: 0; 210 right: 10px; 211 bottom: 0; 212 width: 40px; 213 height: 40px; 214 margin: auto 0; 215 font-size: 30px; 216 color: #cc9a9a; 217 margin-bottom: 11px; 218 transition: color 0.2s ease-out; 219 } 220 221 .todo-list li .destroy:hover { 222 color: #af5b5e; 223 } 224 225 .todo-list li .destroy:after { 226 content: '×'; 227 } 228 229 .todo-list li:hover .destroy { 230 display: block; 231 } 232 233 .todo-list li .edit { 234 display: none; 235 } 236 237 .todo-list li.editing:last-child { 238 margin-bottom: -1px; 239 } 240 241 .footer { 242 color: #777; 243 padding: 10px 15px; 244 height: 20px; 245 text-align: center; 246 border-top: 1px solid #e6e6e6; 247 } 248 249 .footer:before { 250 content: ''; 251 position: absolute; 252 right: 0; 253 bottom: 0; 254 left: 0; 255 height: 50px; 256 overflow: hidden; 257 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 258 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 259 0 17px 2px -6px rgba(0, 0, 0, 0.2); 260 } 261 262 .todo-count { 263 float: left; 264 text-align: left; 265 } 266 267 .todo-count strong { 268 font-weight: 300; 269 } 270 271 .filters { 272 margin: 0; 273 padding: 0; 274 list-style: none; 275 position: absolute; 276 right: 0; 277 left: 0; 278 } 279 280 .filters li { 281 display: inline; 282 } 283 284 .filters li a { 285 color: inherit; 286 margin: 3px; 287 padding: 3px 7px; 288 text-decoration: none; 289 border: 1px solid transparent; 290 border-radius: 3px; 291 } 292 293 .filters li a:hover { 294 border-color: rgba(175, 47, 47, 0.1); 295 } 296 297 .filters li a.selected { 298 border-color: rgba(175, 47, 47, 0.2); 299 } 300 301 .clear-completed, 302 html .clear-completed:active { 303 float: right; 304 position: relative; 305 line-height: 20px; 306 text-decoration: none; 307 cursor: pointer; 308 } 309 310 .clear-completed:hover { 311 text-decoration: underline; 312 } 313 314 .info { 315 margin: 50px auto 0; 316 color: #bfbfbf; 317 font-size: 15px; 318 text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 319 text-align: center; 320 } 321 322 .info p { 323 line-height: 1; 324 } 325 326 .info a { 327 color: inherit; 328 text-decoration: none; 329 font-weight: 400; 330 } 331 332 .info a:hover { 333 text-decoration: underline; 334 } 335 336 /* 337 Hack to remove background from Mobile Safari. 338 Can't use it globally since it destroys checkboxes in Firefox 339 */ 340 @media screen and (-webkit-min-device-pixel-ratio: 0) { 341 .toggle-all, 342 .todo-list li .toggle { 343 background: none; 344 } 345 346 .todo-list li .toggle { 347 height: 40px; 348 } 349 } 350 351 @media (max-width: 430px) { 352 .footer { 353 height: 50px; 354 } 355 356 .filters { 357 bottom: 10px; 358 } 359 }
3、根组件App.vue
1 <template> 2 <!-- 主体区域 --> 3 <section id="app"> 4 <TodoHeader @add="handleAdd"></TodoHeader> 5 <TodoMain :list="list" @del="handelDel"></TodoMain> 6 <TodoFooter :list="list" @clear="clear"></TodoFooter> 7 </section> 8 </template> 9 10 <script> 11 import TodoHeader from './components/TodoHeader.vue' 12 import TodoMain from './components/TodoMain.vue' 13 import TodoFooter from './components/TodoFooter.vue' 14 15 // 渲染功能: 16 // 1.提供数据: 提供在公共的父组件 App.vue 17 // 2.通过父传子,将数据传递给TodoMain 18 // 3.利用 v-for渲染 19 20 // 添加功能: 21 // 1.手机表单数据 v-model 22 // 2.监听事件(回车+点击都要添加) 23 // 3.子传父,讲任务名称传递给父组件 App.vue 24 // 4.进行添加 unshift(自己的数据自己负责) 25 // 5.清空文本框输入的内容 26 // 6.对输入的空数据 进行判断 27 28 // 删除功能 29 // 1.监听事件(监听删除的点击) 携带id 30 // 2.子传父,讲删除的id传递给父组件的App.vue 31 // 3.进行删除filter(自己的数据 自己负责) 32 33 // 底部合计:父传子 传list 渲染 34 // 清空功能:子传父 通知父组件 → 父组件进行更新 35 // 持久化存储:watch深度监视list的变化 -> 往本地存储 ->进入页面优先读取本地数据 36 export default { 37 data() { 38 return { 39 list: JSON.parse(localStorage.getItem('list')) || [ 40 { id: 1, name: '打篮球' }, 41 { id: 2, name: '看电影' }, 42 { id: 3, name: '逛街' }, 43 ], 44 } 45 }, 46 components: { 47 TodoHeader, 48 TodoMain, 49 TodoFooter, 50 }, 51 watch: { 52 list: { 53 deep: true, 54 handler(newVal) { 55 localStorage.setItem('list', JSON.stringify(newVal)) 56 }, 57 }, 58 }, 59 methods: { 60 handleAdd(todoName) { 61 // console.log(todoName) 62 this.list.unshift({ 63 id: +new Date(), 64 name: todoName, 65 }) 66 }, 67 handelDel(id) { 68 // console.log(id); 69 this.list = this.list.filter((item) => item.id !== id) 70 }, 71 clear() { 72 this.list = [] 73 }, 74 }, 75 } 76 </script> 77 78 <style> 79 </style>
4、main.js 需要引入css
1 import Vue from 'vue' 2 import App from './App.vue' 3 import './style/index.css' 4 5 Vue.config.productionTip = false 6 7 new Vue({ 8 render: h => h(App), 9 }).$mount('#app')
非父子通信(拓展) -event bus 事件总线
1、utils/EventBus.js
1 import Vue from 'vue' 2 3 // 定义事件总线 4 const Bus = new Vue() 5 // 导出 6 export default Bus
2、组件目录下
components/BaseA.vue
1 <template> 2 <div class="base-a"> 3 我是A组件(接受方) 4 <p>{{msg}}</p> 5 </div> 6 </template> 7 8 <script> 9 import Bus from '../utils/EventBus' 10 export default { 11 data() { 12 return { 13 msg: '', 14 } 15 }, 16 created() { 17 Bus.$on('sendMsg', (msg) => { 18 // console.log(msg) 19 this.msg = msg 20 }) 21 }, 22 } 23 </script> 24 25 <style scoped> 26 .base-a { 27 width: 200px; 28 height: 200px; 29 border: 3px solid #000; 30 border-radius: 3px; 31 margin: 10px; 32 } 33 </style>
components/BaseB.vue
1 <template> 2 <div class="base-b"> 3 <div>我是B组件(发布方)</div> 4 <button @click="sendMsgFn">发送消息</button> 5 </div> 6 </template> 7 8 <script> 9 import Bus from '../utils/EventBus' 10 export default { 11 methods: { 12 sendMsgFn() { 13 Bus.$emit('sendMsg', '今天天气不错,适合旅游') // 触发事件 14 }, 15 }, 16 } 17 </script> 18 19 <style scoped> 20 .base-b { 21 width: 200px; 22 height: 200px; 23 border: 3px solid #000; 24 border-radius: 3px; 25 margin: 10px; 26 } 27 </style>
components/BaseC.vue
1 <template> 2 <div class="base-c"> 3 我是C组件(接受方) 4 <p>{{msg}}</p> 5 </div> 6 </template> 7 8 <script> 9 import Bus from '../utils/EventBus' 10 export default { 11 data() { 12 return { 13 msg: '', 14 } 15 }, 16 created() { 17 Bus.$on('sendMsg', (msg) => { // 监听事件 18 // console.log(msg) 19 this.msg = msg 20 }) 21 }, 22 } 23 </script> 24 25 <style scoped> 26 .base-c { 27 width: 200px; 28 height: 200px; 29 border: 3px solid #000; 30 border-radius: 3px; 31 margin: 10px; 32 } 33 </style>
3、根组件App.vue
1 <template> 2 <div class="app"> 3 <BaseA></BaseA> 4 <BaseB></BaseB> 5 <BaseC></BaseC> 6 </div> 7 </template> 8 9 <script> 10 import BaseA from './components/BaseA.vue' 11 import BaseB from './components/BaseB.vue' 12 import BaseC from './components/BaseC.vue' 13 export default { 14 components:{ 15 BaseA, 16 BaseB, 17 BaseC 18 } 19 } 20 </script> 21 <style> 22 </style>
非父子通信(拓展) -provide & inject
1、组件目录下
components/GrandSon.vue
1 <template> 2 <div class="grandSon"> 3 我是GrandSon 4 {{ color }} -{{ userInfo.name }} -{{ userInfo.age }} 5 </div> 6 </template> 7 8 <script> 9 export default { 10 inject: ['color', 'userInfo'], 11 } 12 </script> 13 14 <style> 15 .grandSon { 16 border: 3px solid #000; 17 border-radius: 6px; 18 margin: 10px; 19 height: 100px; 20 } 21 </style>
components/SonA.vue
1 <template> 2 <div class="SonA">我是SonA组件 3 <GrandSon></GrandSon> 4 </div> 5 </template> 6 7 <script> 8 import GrandSon from '../components/GrandSon.vue' 9 export default { 10 components:{ 11 GrandSon 12 } 13 } 14 </script> 15 16 <style> 17 .SonA { 18 border: 3px solid #000; 19 border-radius: 6px; 20 margin: 10px; 21 height: 200px; 22 } 23 </style>
components/SonB.vue
1 <template> 2 <div class="SonB"> 3 我是SonB组件 4 </div> 5 </template> 6 7 <script> 8 export default { 9 10 } 11 </script> 12 13 <style> 14 .SonB { 15 border: 3px solid #000; 16 border-radius: 6px; 17 margin: 10px; 18 height: 200px; 19 } 20 </style>
2、根组件App.vue
1 <template> 2 <div class="app"> 3 我是APP组件 4 <button @click="change">修改数据</button> 5 <SonA></SonA> 6 <SonB></SonB> 7 </div> 8 </template> 9 10 <script> 11 import SonA from './components/SonA.vue' 12 import SonB from './components/SonB.vue' 13 export default { 14 provide() { 15 return { 16 // 简单类型 是非响应式的 17 color: this.color, 18 // 复杂类型 是响应式的 19 userInfo: this.userInfo, 20 } 21 }, 22 data() { 23 return { 24 color: 'pink', 25 userInfo: { 26 name: 'zs', 27 age: 18, 28 }, 29 } 30 }, 31 methods: { 32 change() { 33 this.color = 'red' 34 this.userInfo.name = 'ls' 35 }, 36 }, 37 components: { 38 SonA, 39 SonB, 40 }, 41 } 42 </script> 43 44 <style> 45 .app { 46 border: 3px solid #000; 47 border-radius: 6px; 48 margin: 10px; 49 } 50 </style>
进阶语法
v-model 原理
1、组件目录下
components/MyInput.vue
1 <template> 2 <input 3 type="text" 4 :value="value" 5 @input="$emit('input', $event.target.value)" 6 /> 7 </template> 8 9 <script> 10 export default { 11 name: 'MyInput', 12 props: ['value'] 13 } 14 </script> 15 <style lang="less" scoped></style>
components/MySelect.vue
1 <template> 2 <select :value="value" @change="$emit('input', $event.target.value)"> 3 <option value="北京">北京市</option> 4 <option value="上海">上海市</option> 5 <option value="广州">广州市</option> 6 <option value="深圳">深圳市</option> 7 </select> 8 </template> 9 10 <script> 11 export default { 12 name: 'MySelect', 13 props: ['value'] 14 } 15 </script> 16 <style lang="less" scoped></style>
2、根组件App.vue
1 <template> 2 <div> 3 <h3>需求1:不用v-model实现双向绑定</h3> 4 <input type="text" :value="uname" @input="changeValue" /> <!-- 等同于 -> <input type="text" v-model="uname"> --> 5 <p>结论:v-model 可以拆分成 :value + @input</p> 6 <hr /> 7 8 <h3>需求2:实现输入框组件的v-model</h3> 9 <!-- <MyInput v-model="age"></MyInput> --> 10 <!-- <MyInput :value="age" @input="changeAge"></MyInput> --> 11 <MyInput v-model="age"></MyInput> 12 <p>结论:组件的v-model需要子组件的配合。</p> 13 <p>结论:子组件需要接收value属性,并触发input事件</p> 14 <hr /> 15 16 <h3>需求3:实现下拉框组件的v-model</h3> 17 <MySelect v-model="address"></MySelect> 18 <!-- <MySelect :value="address" @input="changeSelect"></MySelect> --> 19 <p>下拉框的v-model相当于 :value + @input</p> 20 21 <hr /> 22 <p>总结:v-model ==== :value + @input</p> 23 <p>总结:父传子,为了把数据传给子组件,设置表单元素的默认值</p> 24 <p>总结:子传父,为了把表单元素的值,传给父组件,修改data数据</p> 25 </div> 26 </template> 27 28 <script> 29 import MyInput from './components/MyInput.vue' 30 import MySelect from './components/MySelect.vue' 31 export default { 32 data () { 33 return { 34 address: '上海', 35 age: 20, 36 uname: 'zhangsan' 37 } 38 }, 39 components: { 40 MyInput, 41 MySelect 42 }, 43 methods: { 44 changeValue (e) { 45 // 把输入框的值,赋值给uname 46 this.uname = e.target.value 47 }, 48 changeAge (val) { 49 this.age = val 50 }, 51 changeSelect (val) { 52 this.address = val 53 } 54 } 55 } 56 </script> 57 58 <style lang="less" scoped> 59 p { 60 background-color: gold; 61 } 62 </style>
.sync 修饰符
1、组件目录下
components/MyDialog.vue
1 <template> 2 <div class="dialog" v-show="abc"> 3 <div class="dialog-header"> 4 <h3>友情提示</h3> 5 <span class="close" @click="$emit('update:abc', false)">✖️</span> 6 </div> 7 <div class="dialog-content">我是文本内容</div> 8 <div class="dialog-footer"> 9 <button>取消</button> 10 <button>确认</button> 11 </div> 12 </div> 13 </template> 14 15 <script> 16 export default { 17 props: ['abc'] 18 } 19 </script> 20 21 <style scoped> 22 * { 23 margin: 0; 24 padding: 0; 25 } 26 27 .dialog { 28 width: 470px; 29 height: 230px; 30 padding: 0 25px; 31 background-color: #ffffff; 32 margin: 40px; 33 border-radius: 5px; 34 } 35 36 .dialog-header { 37 height: 70px; 38 line-height: 70px; 39 font-size: 20px; 40 border-bottom: 1px solid #ccc; 41 position: relative; 42 } 43 44 .dialog-header .close { 45 position: absolute; 46 right: 0px; 47 top: 0px; 48 cursor: pointer; 49 } 50 51 .dialog-content { 52 height: 80px; 53 font-size: 18px; 54 padding: 15px 0; 55 } 56 57 .dialog-footer { 58 display: flex; 59 justify-content: flex-end; 60 } 61 62 .dialog-footer button { 63 width: 65px; 64 height: 35px; 65 background-color: #ffffff; 66 border: 1px solid #e1e3e9; 67 cursor: pointer; 68 outline: none; 69 margin-left: 10px; 70 border-radius: 3px; 71 } 72 73 .dialog-footer button:last-child { 74 background-color: #007acc; 75 color: #fff; 76 } 77 </style>
2、根组件App.vue
1 <template> 2 <div> 3 <button @click="abc = true">删除</button> 4 5 <!-- 等同于 => <MyDialog :abc="abc" @update:abc="handle"></MyDialog> --> 6 <!-- :abc="abc" 表示向子组件传值 --> 7 <!-- @update:abc="handle" 表示监听子组件的通知, 子组件通知必须是 update:abc 这个函数名, update:固定的 abc为自定义属性名, 随便改啥都行,保持一致即可 --> 8 9 <!-- abc 是自定义的属性名 --> 10 <MyDialog :abc.sync="abc"></MyDialog> 11 12 <!-- 13 :属性.sync="变量" 14 等同于 :属性="变量" + @update:属性="xxx" 15 --> 16 </div> 17 </template> 18 19 <script> 20 import MyDialog from './components/MyDialog.vue' 21 export default { 22 data() { 23 return { 24 abc: false // 控制弹层显示和隐藏 25 } 26 }, 27 // methods: { 28 // handle(val) { 29 // this.abc = val 30 // }, 31 // }, 32 components: { 33 MyDialog 34 } 35 } 36 </script> 37 38 <style> 39 body { 40 background-color: #b3b3b3; 41 } 42 </style>
ref 和 $refs
1、组件目录下
components/MyForm.vue
1 <template> 2 <div> 3 <h3>Form组件需求:点击按钮能够重置表单</h3> 4 用户名:<input type="text" v-model="uname" /><br /> 5 手机号:<input type="text" v-model="phone" /><br /> 6 </div> 7 </template> 8 9 <script> 10 export default { 11 data () { 12 return { 13 uname: 'zhangsan', 14 phone: '13266668888' 15 } 16 }, 17 methods: { 18 // 清空方法 19 resetForm () { 20 this.uname = this.phone = '' 21 } 22 } 23 } 24 </script> 25 26 <style lang="less" scoped></style>
components/MyTest.vue
1 <template> 2 <div> 3 <h3>Test组件需求:找到下面的p标签,设置样式</h3> 4 <p ref="abc">这是Test组件中的p标签</p> 5 </div> 6 </template> 7 8 <script> 9 export default { 10 mounted () { 11 // document.querySelector 是在整个页面中找元素 12 // document.querySelector('p').style.color = 'red' 13 // 通过$refs获取dom 14 // this.$refs.abc 就和document.querySelector('p')一个意思,获取到ref值为abc的组件,这样就可以调用组件中的方法 15 this.$refs.abc.style.color = 'blue' 16 // 1. 如果组件中,多个元素有相同的ref值, this.$refs.xxx 只找最后一个 17 // 2. 循环出来之后,多个元素有相同的ref值, this.$refs.xxx 找到全部 18 } 19 } 20 </script> 21 <style lang="less" scoped></style>
2、根组件App.vue
1 <template> 2 <div> 3 <h2>App组件</h2> 4 <p ref="abc">这是App组件中的p标签</p> 5 <hr /> 6 <MyTest></MyTest> 7 <hr /> 8 <MyForm ref="myform"></MyForm> 9 // 通过$refs获取组件 10 <!-- $refs.myform 找到ref值为myform的组件, 就可以调用组件中的方法 --> 11 <button @click="$refs.myform.resetForm()">重置</button> 12 </div> 13 </template> 14 15 <script> 16 import MyTest from './components/MyTest.vue' 17 import MyForm from './components/MyForm.vue' 18 export default { 19 data () { 20 return {} 21 }, 22 components: { 23 MyTest, 24 MyForm 25 } 26 } 27 </script> 28 <style lang="less" scoped></style>
自定义指令focus、loading
1、在main.js中定义
1 import Vue from 'vue' 2 import App from './App.vue' 3 4 Vue.config.productionTip = false 5 6 // 1. 全局注册指令 7 Vue.directive('focus', { 8 // 进入页面,让元素自动获取焦点 9 inserted (ele) { 10 // console.log(ele) // 绑定指令的元素 11 // console.log(binding) // 指令的一些信息(比如包括指令的值 binding.value) 12 ele.focus() // 让元素获取焦点 13 } 14 }) 15 16 new Vue({ 17 render: h => h(App) 18 }).$mount('#app')
2、自定义指令的使用,在根组件中App.vue
1 <template> 2 <!-- <div class="box" @click="$event.target.innerHTML = 'HELLO'"> --> 3 <div class="box"> 4 5 <!-- 使用自定义指令 color --> 6 <h3 v-color="yanse">需求1:让输入框立即获取焦点</h3> 7 8 <div class="focus"> 9 <!-- 使用全局的自定义指令 v-focus --> 10 <input type="text" v-focus /> 11 </div> 12 13 <h3>需求2:Ajax请求数据,并设计loading指令</h3> 14 <!-- list数据为空,加 loading 类,让loading图片显示 --> 15 <!-- list数据不为空,移除 loading 类,让loading图片隐藏 --> 16 <!-- 使用局部定义的指令 loading --> 17 <ul v-loading="list.length"> 18 <li class="news" v-for="item in list" :key="item.id"> 19 <div class="left"> 20 <div class="title">{{ item.title }}</div> 21 <div class="info"> 22 <span>{{ item.source }}</span> 23 <span>{{ item.time }}</span> 24 </div> 25 </div> 26 <div class="right"> 27 <img :src="item.img" alt="" /> 28 </div> 29 </li> 30 </ul> 31 </div> 32 </template> 33 34 <script> 35 // 安装 axios => npm i axios 或者 yarn add axios 36 // 安装如果报错,则加 --legacy-peer-deps 选项 37 // 接口地址:http://hmajax.itheima.net/api/news 38 // 请求方式:get 39 // 请求参数:无 40 41 import axios from 'axios' 42 export default { 43 directives: { 44 loading: { 45 // 刷新页面后,立即判断有没有数据,loading图片要不要显示 46 inserted(ele, obj) { 47 // console.log(ele) // 使用指令的元素 48 // console.log(obj.value) // 指令的值 49 obj.value <= 0 50 ? ele.classList.add('loading') 51 : ele.classList.remove('loading') 52 }, 53 update(ele, obj) { 54 obj.value <= 0 55 ? ele.classList.add('loading') 56 : ele.classList.remove('loading') 57 } 58 }, 59 color: { 60 inserted(ele, obj) { 61 console.log('111111111') 62 // console.log(ele) // element 元素,表示使用指令的那个元素 63 // console.log(obj) // obj.value 就是传递给指令的值 64 ele.style.color = obj.value 65 }, 66 update(ele, obj) { 67 console.log(22222222222222) 68 ele.style.color = obj.value 69 } 70 }, 71 }, 72 data() { 73 return { 74 yanse: 'blue', 75 list: [] 76 } 77 }, 78 async created() { 79 const { data: res } = await axios.get('http://hmajax.itheima.net/api/news') 80 setTimeout(() => { 81 this.list = res.data 82 }, 2000) 83 }, 84 methods: { 85 // showEle(e) { 86 // console.log(e.target) 87 // } 88 } 89 } 90 </script> 91 92 <style> 93 .loading::before { 94 position: absolute; 95 left: 0; 96 top: 0; 97 width: 100%; 98 height: 100%; 99 background: #fff url('./loading.gif') no-repeat; 100 content: ''; 101 } 102 103 .box { 104 width: 800px; 105 min-height: 500px; 106 position: relative; 107 } 108 109 .focus, 110 ul { 111 border: 3px solid orange; 112 border-radius: 5px; 113 padding: 10px; 114 margin: 10px; 115 } 116 117 .news { 118 display: flex; 119 height: 120px; 120 width: 600px; 121 margin: 0 auto; 122 padding: 20px 0; 123 cursor: pointer; 124 } 125 126 .news .left { 127 flex: 1; 128 display: flex; 129 flex-direction: column; 130 justify-content: space-between; 131 padding-right: 10px; 132 } 133 134 .news .left .title { 135 font-size: 20px; 136 } 137 138 .news .left .info { 139 color: #999999; 140 } 141 142 .news .left .info span { 143 margin-right: 20px; 144 } 145 146 .news .right { 147 width: 160px; 148 height: 120px; 149 } 150 151 .news .right img { 152 width: 100%; 153 height: 100%; 154 object-fit: cover; 155 } 156 </style>
Vue异步更新、$nextTick
使用场景:如果想在修改数据后立刻得到更新后的DOM结构,可以使用this.$nextTick()
在created生命周期中进行DOM操作
1 <template> 2 <div> 3 <!-- 需求:输入框和确认按钮默认隐藏;点击编辑显示;显示后立即获取焦点 --> 4 <div class="title"> 5 <h2>大标题</h2> 6 <button @click="showIpt">编辑</button> 7 </div> 8 <div class="form" v-show="flag"> 9 <input type="text" ref="refIpt" /> 10 <button>确认</button> 11 </div> 12 </div> 13 </template> 14 15 <script> 16 export default { 17 data () { 18 return { 19 flag: false 20 } 21 }, 22 methods: { 23 showIpt () { 24 this.flag = true // 修改flag,让输入框显示 25 // this.flag = false 26 // this.flag = true 27 // this.flag = false 28 // this.flag = true 29 // 上述,数据更新了,页面不会立即更新;会在下次循环的时候更新页面。 30 31 this.$nextTick(() => { 32 // 数据更新后,页面也更新后。这个函数执行 33 this.$refs.refIpt.focus() // 找到输入框,让其获得焦点 34 }) 35 } 36 } 37 } 38 </script> 39 40 <style lang="less" scoped> 41 .title { 42 width: 300px; 43 display: flex; 44 justify-content: flex-start; 45 align-items: center; 46 button { 47 margin-left: 5px; 48 } 49 } 50 </style>