Vue-进阶知识

2020/7/12 Vue
// 组件化MVVM
1. 传统组件,只是静态渲染,更新还要依赖于操作DOM
2. 数据驱动视图MVVM
----------------------------------------------------------------------------------------------
// vue响应式
1. 实现数据驱动视图的第一步
2. 组件data的数据一旦变化,立刻触发视图的更新
3. 核心API Object.defineProperty // vue3.0启用proxy
    const data = {}
    let name = 'cj'
    Object.defineProperty(data, 'userName', {
      get: function() {
        return name
      },
      set: function(val) {
        name = val
      }
    })
4. 利用Object.defineProperty()监听data的数据
    先对data的数据进行过滤,如果是值类型则直接返回
    如果是obj则用Object.defineProperty()方法监听
    如果是数组则要特殊处理,修改原型方法
    在监听方法中会递归使用过滤方法实现深度监听
5. Object.defineProperty()的缺点
    对于data下面的嵌套的对象需要深度监听,需要递归到底,一次性计算量大 // vue3.0优化
    无法监听新增属性和删除属性的变化,所以需要使用Vue.set()和Vue.delete() // 对data中的数据新增或删除时需要set和delete
    vue3.0将使用proxy进行监听,原生可以监听对象和数组,会在使用时进行监听,而不是一上来就全部递归到底
6. 对于对象采用递归方式深度监听,无法原生监听数组,需要特殊处理
7. 对于数组则利用Object.create('监听的数组')这个方法改写原型下所有的方法加入了更新视图等操作
   而改写后原型上的方法名和正常的方法名一样,这样就实现了监听数组,其实就是重新定义数组原型然后
   把每一个数组的原型都变成改写后的原型 // Object.create()的作用是创建新对象,原型指向参数,再扩展新的方法不会影响到原型
    const oldArrayProperty = Array.prototype
    const newArr = Object.create(oldArrayProperty)
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(item => {
      newArr[item] = function() {
        // 调用原数组的原型方法
        oldArrayProperty[item].call(this, ...arguments) // 如push方法: Array.prototype.push.call(this, ...arguments)
        updateView() // 更新视图
      }
    })
8. Vue Array 没有经过 Object.defineProperty,虽然这个 API 可以监听数组的变化,但是监听不了 length 改变
   但是由于性能原因,Vue 没有采用此 API 监听数组,而只是修改了几个操作数组的方法加入更新视图的操作,替换了原型上的方法
----------------------------------------------------------------------------------------------
// 虚拟DOM/VDOM (Virtual DOM)
1. vdom存在的价值: 数据驱动视图,控制DOM操作
2. DOM操作非常耗费性能,有了一定的复杂度,想减少计算次数比较难,但可以把计算转移为js计算,因为js执行速度很快
3. vdom就是用js模拟DOM结构,计算出最小的变更,操作DOM
    <div id="container" class="box"><span style="font-size: 20px">123</span></div>
    上面的DOM结构可以用js这样表示: // vnode
    {
      tag: 'div', // 标签名
      props: { // 属性,也会包含事件
        id: 'container',
        className: 'box'
      },
      children: [ // 内容
        {
          tag: 'span',
          props: {
            style: 'font-size: 20px'
          },
          children: '123'
        }
      ]
    }
4. snabbdom 是一个简洁强大的vdom库,vue就是参考它实现的vdom和diff
    h函数传入的参数和3的vnode差不多,会返回一个vnode对象里面包括sel标签、data属性、children子元素和子文本不共存、
    text子文本和子元素不共存、elm挂载的DOM标签、key
    patch函数传入的第一个参数可以是DOM元素也可以是vnode,第二个参数也是vnode,如果是DOM元素则是第一次渲染,如果是vnode则是更新
5. 如果不用vdom,改变了某个DOM元素的子元素会导致整个DOM元素重新渲染,而vdom则是只让子元素重新渲染
    新旧vnode对比,得出最小的更新范围,最后更新DOM
6. vdom和vnode的区别: vdom好比是一棵树,vnode则是vdom中一个一个的树节点
7. diff翻译过来就是对比,是一个比较广泛的概念,如linux diff命令、git diff命令等,两棵树做diff,就是这里的vdom diff
8. diff算法是比较两个vnode,计算出最小的变更,以便减少DOM操作次数,提高性能
9. 两棵树做diff的时间复杂度O(n^3),第一遍历tree1,第二遍历tree2,第三排序比较,当数量大时,此算法不可用
10. vue vdom diff算法把时间复杂度优化到O(n):
    只比较同一层级,不跨级比较
    tag不相同,则直接删掉重建,不再深度比较
    tag和key两者都相同,则认为是相同节点,不再深度比较 // sameVnode
11. 它的流程是:
    首先,它会判断是否是首次渲染,因为如果是首次渲染,没有旧的vnode,不需要比较,直接渲染就可以了
    在非首次渲染,首先比较两个节点是否一样如果不一样,直接删除重建,如果一样,就需要进行vnode比较,就是比较children
    如果新节点没有文本节点,删除旧节点的文本节点,如果有文本节点,替换掉旧的文本节点
    如果只有新节点有子节点,直接插入,如果只有旧节点有子节点,直接删除
    如果新旧节点都有子节点,这时候会遍历新节点的children,每个新的子节点都需要在旧的children里面进行寻找,找一个一样的节点
    如果没有找到,新的子节点直接插入,如果找到了,这两个节点再进行vnode比较
    也可以简单的理解为,如果没有是重新渲染,如果有的话,直接把旧的子节点挪过来用就可以了
12. snabbdom库的核心函数
      patch // 渲染DOM
      patchVnode // 替换节点
      addVnode // 新增节点
      removeVnode // 删除节点
      updateChildren // 对比children, key的重要性
----------------------------------------------------------------------------------------------
// 模板编译, 在vue的tamplate中的写标签是模板而不是html, 有指令、插值、js表达式、能判断、循环等
1. with语法会改变{}内自由变量的查找规则,当做obj属性来查找,如果找不到就会报错,不要乱用,打破了作用域规则,易读性变差
    const obj = { a: 1, b: 2 }
    with(obj) {
      console.log(a) // 1
      console.log(b) // 2
      console.log(c) // 会报错, 如果不用with, obj.c会打印undefined
    }
2. html是标签语言,只有js才能实现判断、循环 // 图灵完备的, 就是能实现逻辑、算法、判断、循环
3. 因此模板一定是转换为某种js代码,即模板编译
4. 通过vue-template-compiler进行模板编译生成render函数 // 函数内部使用了 with 语法
5. 执行render函数返回vnode // 与snabbdom需要的vnode类似
6. 基于vnode再执行patch和diff,最后渲染和更新视图
7. 使用webpack vue-loader, 会在开发环境下编译模板
8. 示例:
    template: <p>{{name}}</p>
    所对应的render函数是:
    with(this) {
      return _c('p', [_v(_s(name))])
    }
    _c: createElement相当于snabbdom的h函数
    _v: createTextVnode
    _s: toString
9. 在vue组件中可以直接写 render 函数代替 template 和 JSX
    template: <comp-one ref="comp"><span ref="span">{{ val }}</span></comp-one>
    所对应的render函数是:
    render(h) { // h === this.$createElement() === createVNode
      return h(
        'comp-one',
        { ref: 'comp'},
        [h('span', {ref: 'span'}, this.val)]
      )
    }
----------------------------------------------------------------------------------------------
// 组件渲染过程
1. 响应式: 监听data属性getter、setter // 包括数组
2. 模板编译: 模板到render函数,再到vnode
3. vdom: patch(ele, vnode)和patch(oldVnode, newVnode)
// 初次渲染
1. 解析模板为render函数 // 或在开发环境已完成, vue-loader
2. 触发响应式, 监听data属性getter、setter
3. 执行render函数, 生成vnode, 进行patch(ele, vnode)
4. 注意: 执行render函数会触发getter
// 更新过程
1. 修改data, 触发setter // 此前在getter中已被监听
2. 重新执行render函数, 生成newVnode
3. patch(vnode, newVnode)
----------------------------------------------------------------------------------------------
// 组件渲染过程详细, 有依赖收集
// 初次渲染
1. initState ->进行双向绑定
2. $mount->将template编译成render函数
3. 执行渲染 触发属性get函数,将渲染watcher 收集到dep中
4. 调用render 函数 生成vnode
5. patch(elm, vnode)
// 更新过程
1. 修改数据 触发属性set
2. 然后dep.notify() ->watch.update 派发更新
3. 触发render watcher 的render回调
4. 生成新的vnode
5. patch(oldVnode, newVnode)
// 异步渲染
1. vue是异步渲染的,data改变和后dom不会立即渲染
2. 页面渲染时会将data的修改经行整合一次性渲染
3. 在js中dom是在主线程清空了的才会去渲染
4. $nextTick()DOM渲染完成后才会执行回调函数
5. vue里面渲染也是一样,$nextTicket 其实就是个类似于settimeout这样的异步函数,把回调操作推到异步队列
   所以即使我不用$netTicket,用个setTimeout也是能实时获取到最新的dom
----------------------------------------------------------------------------------------------
// vuex
1. const store = new vuex.store({
    state: { count: 0 },
    mutations: { updateCount(state, num) { state.count = num }},
    action: { updateCountSync(state, num) { state.count = num }}
  })
2. 在组件中的computed中使用,获取值时写在computed中
   computed: {
     count() {
       return this.$store.state.count
     }
   }
3. 在组件中的methods中使用,通过this.$store.commit('updateCount', 1) 调用mutations操作修改state
4. getter相当于组件内的computed
   getter: {
     sum(state) {
       return state.count + 1
     }
   }
   在组件中使用this.$store.getter.sum
5. 在组件中的computed中使用简写
   computed: {
     ...mapState(['count']) // 直接使用this.count === this.$store.state.count
     ...mapState({ counter: 'count' }) // 改变名称
     ...mapState({ counter: state => state.count }) // this.counter === this.$store.state.count
     ...mapGetter(['sum']) // 直接使用this.sum === this.$store.getter.sum
     ...mapState({ summit: 'sum' }) // 改变名称
   }
6. 修改state中的数据通过mutations(同步)或actions(异步)去修改
7. actions或组件通过this.$store.commit('mutations中的函数名', '传递的参数,多个值使用对象')
8. 组件通过this.$store.dispatch('actions中的函数名', '传递的参数,多个值使用对象')
9. 在组件中的methods中使用简写
   methods: {
     ...mapMutations(['mutations中的函数名']) === this.函数名(参数) === 7
     ...mapActions(['actions中的函数名']) === this.函数名(参数) === 8
   }
10. 模块
   const store = new vuex.store({
      modules: {
        g1: {
          state: {
            count: 1
          }
        },
        g2: {
          state: {
            count: 2
          }
        }
      }
   })
11. 在组件中的computed中使用
   computed: {
     count() {
       return this.$store.g1.state.count
     }
   }
12. 在组件中的computed中简写
   computed: {
     ...mapState([g1Count: state => state.g1.count])
   }
13. 默认模块下mutations会变成全局,所以使用时和79一样,也可以给module: { g1: { namespaced: true }}
14. 加配置,变成模块下的,则方法需要...mapmutations(['g1/函数名']),this['g1/函数名'](参数)调用
15. 在模块中的getter有3个参数,2个一样,3个是全局的state
16. 在模块中的actions可以调用commit触发当前模块下的mutations
17. 如果要触发全局下的mutations则需要在第三个参数中加上 { root: true },因为加了13的配置
18. 可以通过在入口文件时的store.registerModules('c', { state: { count: 1 }})动态增加模块
19. store.unrefisterModule('c') // 解除模块
20. store.watch // 监听state变化
21. store.subcribe((mutation, state) => { '哪个mutation', 'mutation的参数' }) // 获取mutations的变化
22. store.subcribeAction((action, state) => { '哪个action', 'action的参数' }) // 获取action的变化
23. 可以实现热更新
24. 全部配置
    export default () => {
      return new Vuex.store({
        strict: true, // 只用于生产环境,不能直接修改state的值
        state: { count: 1 },
        mutations: { updateCount(state, num) { state.count = num }},
        action: { updateCountSync(state, num) { state.count = num }}, // 也可直接commit调用mutations
        getter: { sum(state) {  return state.count + 1 } },
        modules: {
          g1: {
            state: {
              count: 1
            }
          },
          g2: {
            state: {
              count: 2
            }
          }
        }
      })
    }
25. vuex刷新会重置丢失数据,可以在数据中加入缓存机制解决 // 注意
26. 在actions中 return commit 可以在调用dispatch时使用.then获取修改后的值
27. vue-devtools可以在chrome商店中下载或使用远程的devtools,使用远程时需要在index.html中添加一段js,具体看github
28. getters使用时是通过函数return state的方式,要在new store时传入
29. 使用扩展运算符(...)在computed中相当是往computed添加对象属性 // 合并computed和store的值,就可以通过this.值获取
30. mutations相当于是原子操作,不能再细分
31. actions可以包含多个mutations
32. 默认情况下,可以直接通过 this.$store.state.变量 = xxx 或 dispatch action 修改 state 的值
    如果配置了 strivt: true 的话,表示开启严格默认,state 的修改只能通过 mutation,其他方式会报错
32. vuex工作原理:vuex中的store本质就是没有template的隐藏着的vue组件
----------------------------------------------------------------------------------------------
// vue-router的原理之hash路由
1. 核心API: window.onhashchange // 监听url的hash变化
2. hash变化会触发网页跳转,即浏览器的前进、后退
3. hash变化不会刷新页面,SPA必需的特点
4. hash永远不会提交到server端 // 前端自生自灭
5. 触发hash变化的方法:
    js修改url
    手动修改url的hash
    浏览器的前进、后退
// vue-router的原理之H5 history路由, 不会刷新页面
1. 核心API: window.onpopstate // 监听浏览器前进、后退
            history.pushState // 使用这个方法跳转url的path浏览器不会刷新页面
2. 需要后端支持,匹配返回html文件
3. to B的系统推荐用hash,简单易用,对url规范不敏感
4. to C的系统可以考虑选择H5 history,需要服务端支持
----------------------------------------------------------------------------------------------
// vue-router
1. vue-router默认的url加上#/为哈希,最好用history
2. route它是指一条路由
3. routes它是指一组路由
4. router它是一个机制,用来管理路由
5. 使用this.$route.query.参数名获取到的url参数,第一次获取到的值的类型时传递过来的类型
6. 刷新后获取到的值的类型则是String,因为路由没有变,直接取url上的
7. const routes = [
     { path: '/home', component: home },
     { path: '/login', component: login },
     { path: '/', redirect: '/home' } // 重定向,默认进入home页面
   ]
   var router = new Router( { routes })
   在html中使用<router-view></router-view>
8. export default () => {
      return new Router({
        routes, // 一组路由
        mode: 'histoty', // 把路由中默认加的#去掉
        base: '/base/', // 在使用跳转时在路径前面加上/base/
        linkActiveClass: 'active-link', // 当当前路由一部分和to="..."一样时会给元素加上这个类名,给<router-link>增加样式
        linkExactActiveClass: 'exact-active-link', // 当当前路由完全和to="..."一样时会给元素加上这个类名,给<router-link>增加样式
        scrollBehavior(to, from, savePosition) {
          // 进入页面时滚动到上次浏览的位置
          // to去哪个页面,from从哪里来,savePosition历史记录上次的位置
          if (savePosition) {
            return savePosition
          } else {
            return { x: 0, y: 0 }
          }
        },
        parseQuery(query) {
          // 字符串查询转为对象
        },
        stringifyQuery(obj) {
          // 对象查询转为字符串
        },
        fallback: true // 有的浏览器不能使用history会自动转为hash模式
      })
   }
9. 在routes中可以在组件中用this.$route获取url上的内容
   const routes = [{
     path: '/home/':id, // this.$route.params.id获取
     component: () => import('路径') // 进行懒加载,在组件注册时也可用
     name: 'home', // 在<router-link :to="{ name: 'home' }">
     meta: {
       title: 'this is home',
       description: ''
     },
     children: [{ // 嵌套路由,在用children中需在所对应的组件中用<router-view>占位
       path: '/homeChildren,
       component: homeChildren,
     }],
     props: true,
     props: route => {
       id: route.query.id // 在组件中直接使用this.id,而不用使用this.$route.query.id
     }
   }]
10.<transition name="fade"><router-view /></transition> // 可以用过渡动画
11. 一个页面两个<router-view>对应不同的组件
    <router-view />
    <router-view name="g1" />
    const routes = [{
      path: '/home',
      components: {
        default: home, // 没name
        g1: homeg1 // 有name="g1"
      }
    }]
----------------------------------------------------------------------------------------------
// 导航守卫
1. 导航守卫: 类型中间件,进入路由前先通过守卫,进行操作,进入页面
2. 全局守卫: 在定义router的地方定义,main.js
   router.beforeEach((to, from, next) => { console.log('beforeEach') next() }) // 前置守卫
   router.beforResolve((to, from, next) => { console.log('beforResolve') next() }) // 解析守卫
   router.afterEach((to, from) => { console.log('afterEach') }) // 后置守卫
3. 路由独享守卫: 在路由内定义
   const routes = [{
     path: '/home',
     components: home,
     beforeEnter: (to, from, next) => { console.log('beforeEnter') next() }
   }]
4. 组件内的守卫: 在组件内定义
   beforeRouteEnter(to, from, next) {
     // 在渲染该组件的对应路由被confirm(弹框)前调用
     // 不能获取组件实例this,因为在守卫执行前,组件实例还没创建
     // 可用这个守卫给next传值
     console.log('beforeRouteEnter')
     next()
   }
   beforeRouteUptate(to, from, next) {
     // 同一个组件但是id不同: abc/123到abc/456,生命周期不会重新执行,而这个可以,重用的组件
     console.log('beforeRouteUptate')
     next()
   }
   beforeRouteLeave(to, from, next) { console.log('beforeRouteLeave') next() }
----------------------------------------------------------------------------------------------
// 完整的导航解析流程
1. 导航被触发
2. 在失活的组件里调用beforeRouteLeave守卫
3. 调用全局的beforeEach守卫
4. 在重用的组件里调用beforeRouteUpdate守卫
5. 在路由配置里调用beforeEnter守卫
6. 解析异步路由组件
7. 在被激活的组件中调用beforeRouteEnter守卫
8. 调用全局的beforeResolve守卫
9. 导航被确认调用全局的afterEach守卫
10. 触发DOM更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
Last Updated: 2024/6/11 14:20:38
    飘向北方
    那吾克热-NW,尤长靖