前端【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>

 

posted @ 2024-04-07 21:20  为你编程  阅读(34)  评论(0编辑  收藏  举报