文章目录
- 1. v-for 为什么需要绑定 key ?(0:00)
- 2.组件的渲染与更新过程(12:20)
- 3. 组件的 data 为什么必须是一个函数?(22:25)
- 4. Vue 事件绑定原理(30:25)
- 5. v-model 的原理与实现(41:45)
- 6. v-html 的使用可能出现的问题(0:00)
- 7. 父子组件的生命周期顺序(03:50)
- 8. Vue 组件如何通信?(09:30)
- 9. Vue 中相同逻辑如何抽离?(18:00)
- 10. 为什么要异步加载组件?(23:50)
- 11. 插槽与作用域插槽(30:10)
- 12. 对 keep-alive 的理解(38:20)
- 13. Vue 性能优化(43:20)
- 14. Vue3.0 有哪些改进(54:20)
- 15. hash 路由与 history 路由(56:00)
- ~~16. vue-router 路由守卫~~
- 17. Vuex 工作原理 (58:00)
- 参考资料
1. v-for 为什么需要绑定 key ?(0:00)
理解:
- 绑定唯一 key,避免暴力比对,可以调整顺序
- key 默认是索引,会就地复用
- 图解
2.组件的渲染与更新过程(12:20)
理解:
-
组件渲染时,会通过
Vue.extend
方法构建子组件构造函数(原型继承的方法实现) -
installComponentHooks
安装组件的钩子函数init:初始化,prepatch:预补丁、insert:插入、destroy :销毁
-
用 new VNode 实例化(组件的 vNode 没有 children)
-
手动调用
$mount( )
进行挂载 -
更新组件时 patchVnode 流程,其核心是 diff 算法
3. 组件的 data 为什么必须是一个函数?(22:25)
理解:
- 保证组件的相对独立
- 若是 data 是对象,则所有组件共享同一个对象,数据会相互感染
- data 用函数返回一个对象,复用组件会创建多个实例
- 根组件的 data 可以是一个对象,因为它不会被复用
原理
- Vue.extend 中会执行 mergeOptions 方法来 合并 Vue 构造函数和子组件的 options到 vm.$options组件
4. Vue 事件绑定原理(30:25)
理解:
Vue 事件绑定分为两种:一是原生事件的绑定;二是组件的事件绑定。
- 原生dom事件绑定用
addEventListener
实现(普通元素的@click 与组件的@click.native) - 组件自定义事件绑定采用
$on
方法实现 (组件的@click 单独处理)
原理
5. v-model 的原理与实现(41:45)
理解
-
v-model 即可以作用在普通表单元素上,又可以作用在组件上,它其实是一个语法糖
-
<input v-model="message"> // 以上代码等价于如下代码 <input v-bind:value="message" v-on:input="message=$event.target.value">
-
let Child = { template: '<div>' + '<input :value="value" @input="updateValue" placeholder="edit me">' + '</div>', props: ['value'], methods: { updateValue(e) { this.$emit('input', e.target.value) } } } let vm = new Vue({ el: '#app', template: '<div>' + '<child v-model="message"></child>' + '<p>Message is: {{ message }}</p>' + '</div>', data() { return { message: '' } }, components: { Child } }) // 以上代码等价于如下代码 let vm = new Vue({ el: '#app', template: '<div>' + '<child :value="message" @input="message=arguments[0]"></child>' + '<p>Message is: {{ message }}</p>' + '</div>', data() { return { message: '' } }, components: { Child } })
原理
- v-model 的 directive 函数:根据 AST 元素节点的不同情况去执行不同的逻辑
6. v-html 的使用可能出现的问题(0:00)
-
导致 xss 攻击
<input v-model='msg'>
<div v-html='msg'></div>
用户的输入内容是不可靠的,比如输入:
<img src='' onerror='alert(1)'>
会弹出对话框-
会替换标签内部内容
-
解决方法:①输入框增加验证规则②使用 Unicode 编码转移字符
-
-
会替换子元素的内容
7. 父子组件的生命周期顺序(03:50)
- 父 => 子 => 子 => 父
8. Vue 组件如何通信?(09:30)
-
父子组件:父 => 子 使用 props ,子 => 父 使用 $on 与 $emit (简易的发布订阅模式)
-
获取父子组件实例:$prarent 、$children (组件初始化时获取)
-
在父组件中提供数据子组件消费:Provide 与 Inject
1.Provide :将provide数据写到当前组件实例上
2.Inject:遍历父组件查找 provide 数据(响应式数据)
3.多用于写插件 -
Ref (非响应式)获取组件实例,调用属性与方法
-
event bus 事件总线实现跨组件通信 ,
Vue.prototype.$bus = new Vue( )
-
Vuex 状态管理实现
9. Vue 中相同逻辑如何抽离?(18:00)
- 实现:使用
Vue.mixin
,给组件每个生命周期,注入一些公共逻辑 - 核心是 mergeOption
- 语法:
Vue.mixin( data() { return { //... } }, beforeCreate() { // ... } )
10. 为什么要异步加载组件?(23:50)
理解
- 使用场景:为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。路由的组件就是异步组件。
- 组件功能多时,打包结果会很大,异步加载组件可以实现文件的分割加载
- import( url) 语法或 require 语法
- 可以自定义 loading、resolve、reject、timeout 4 种状态
11. 插槽与作用域插槽(30:10)
1. 普通插槽
-
创建组件 vnode 时,会将子元素的 vnode保存,初始化组件时,通过 slot 属性将子元素分类
-
渲染子组件时,会替换 slot 属性的对应节点
-
作用域为父组件
-
示例代码:
let AppLayout = { template: '<div class="container">' + '<header><slot name="header"></slot></header>' + '<main><slot>默认内容</slot></main>' + '<footer><slot name="footer"></slot></footer>' + '</div>' } let vm = new Vue({ el: '#app', template: '<div>' + '<app-layout>' + '<h1 slot="header">{{title}}</h1>' + '<p>{{msg}}</p>' + '<p slot="footer">{{desc}}</p>' + '</app-layout>' + '</div>', data() { return { title: '我是标题', msg: '我是内容', desc: '其它信息' } }, components: { AppLayout } })
父子组件生成的代码
// 子组件生成的代码: with(this) { return _c('div',{ staticClass:"container" },[ _c('header',[_t("header")],2), _c('main',[_t("default",[_v("默认内容")])],2), _c('footer',[_t("footer")],2) ] ) } // 父组件生成的代码: with(this){ return _c('div', [_c('app-layout', [_c('h1',{attrs:{"slot":"header"},slot:"header"}, [_v(_s(title))]), _c('p',[_v(_s(msg))]), _c('p',{attrs:{"slot":"footer"},slot:"footer"}, [_v(_s(desc))] ) ]) ], 1)}
最终生成的DOM 如下
<!-- 最终生成的DOm --> <div> <div class="container"> <header><h1>我是标题</h1></header> <main><p>我是内容</p></main> <footer><p>其它信息</p></footer> </div> </div>
2. 作用域插槽
-
作用域插槽解析时不会作为组件的子节点,而是解析为函数
-
子组件渲染时,调用该函数渲染
-
作用域为子组件
-
示例代码:
let Child = { template: '<div class="child">' + '<slot text="Hello " :msg="msg"></slot>' + '</div>', data() { return { msg: 'Vue' } } } let vm = new Vue({ el: '#app', template: '<div>' + '<child>' + '<template slot-scope="props">' + '<p>Hello from parent</p>' + '<p>{{ props.text + props.msg}}</p>' + '</template>' + '</child>' + '</div>', components: { Child } })
父子组件生成的代码
// 子组件生成的代码: with(this){ return _c('div', {staticClass:"child"}, [_t("default",null, {text:"Hello ",msg:msg} )], 2)} // 父组件生成的代码: with(this){ return _c('div', [_c('child', {scopedSlots:_u([ { key: "default", fn: function(props) { return [ _c('p',[_v("Hello from parent")]), _c('p',[_v(_s(props.text + props.msg))]) ] } }]) } )], 1) }
最终生成的DOM 如下
<!-- 最终生成的DOm --> <div> <div class="child"> <p>Hello from parent</p> <p>Hello Vue</p> </div> </div>
原理
12. 对 keep-alive 的理解(38:20)
一般为了组件的缓存优化而使用 <keep-alive>
组件。它有两个常用属性:include和 exclude,两个生命周期钩子函数:activated 和 deactivated。
1. 内置组件实现
-
它有一个属性 abstract 为 true,是一个抽象组件
-
在 created 钩子里定义了 this.cache 和 this.keys,本质上它就是去缓存已经创建过的 vnode,当缓存数超限了,会采用LRU 算法移除缓存节点
LRU(Least Recently Used) 算法:LRU 是一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!它的主要衡量指标是使用的时间,附加指标是使用的次数。
-
<keep-alive>
只处理第一个子元素,所以一般和它搭配使用的有 component 动态组件或者是 router-view
2. 组件渲染
-
首次渲染,initComponent 函数缓存 vnode 创建生成的 DOM 节点,除建立缓存外,与普通组件渲染无差别
-
缓存渲染
- 若包裹的第一个组件 vnode 命中缓存,则直接返回缓存中的 vnode.componentInstance
- 接着又会执行 patch 过程,再次执行到 createComponent 方法
- createComponent 里会执行reactivateComponent 方法,过执行 insert(parentElm, vnode.elm, refElm) 就把缓存的 DOM 对象直接插入到目标元素中
- 命中缓存则不会在执行组件的 created、mounted 等钩子函数
问题:对于组件 vnode 而言,是没有 children 的,那么对于
<keep-alive>
组件而言,如何更新它包裹的内容呢?
答:原来 patchVnode 在做各种 diff 之前,会先执行 prepatch 的钩子函数,其核心逻辑就是执行 updateChildComponent 方法。updateChildComponent 主要是去更新组件实例的一些属性,重点关注slot
,prepatch 会重新解析<keep-alive>
组件的 slots,从而实现更新它包裹的内容。
3. 生命周期
-
activated 钩子函数:它的执行时机是
<keep-alive>
包裹的组件渲染的时候 -
deactivated 钩子函数:它是发生在 vnode 的 destory 钩子函数
13. Vue 性能优化(43:20)
-
编码优化
- 不必要的数据不要放在 data 中(因为 Vue 的响应式数据处理会遍历数据,内存开销不小)
- v-for 的事件使用事件代理
- SPA(单页应用)采用
keep-alive
缓存组件 - 拆分组件(提高复用性、代码可维护性,减少不必要的渲染<数据变化只会重新渲染当前组件的数据>)
- 尽量使用
v-if
,仅当元素需要频繁切换显示或隐藏时采用v-show
- 保证 key 的唯一性(vue 默认就地复用的策略,会导致问题)
- 对于data 中静态数据,可使用
Object.freeze
进行属性冻结(禁止改变对象属性) - 合理使用路由懒加载与异步组件
- 持久化数据问题(防抖、节流)
-
加载性能优化
- 第三方模块按需加载(babel-plugin-component)
- 滚动可视区域动态加载(vue-virtual-scroll-list))
- 图片懒加载(vue-lazyload)
参考博客:解析vue-lazyload的设计思想
-
用户体验
- 首页加载骨架屏(vue-skeleton-webpack-plugin)
- PWA(progressive-web-application 渐进式网页应用)(使用较少,兼容性差)
-
SEO 优化
- 预渲染插件(prerender-spa-plugin)
- SSR(服务端渲染)
-
打包
- CDN 加载第三方模块
- 多线程打包(happypack???)
- suurcemap 使用 (source-map)
参考博客:sourceMap是个啥
-
缓存与压缩
- 客户端缓存、服务端缓存
- 服务端 gzip 压缩
14. Vue3.0 有哪些改进(54:20)
- 使用 ts 做静态数据监测
- 使用 composition API(解决 mixin 的缺陷,代码更加条理清晰,降低耦合性)
- 响应式数据用 proxy 实现
- vdom算法优化,只更新绑定动态数据部分
15. hash 路由与 history 路由(56:00)
路由:通过改变 URL 实现不重新请求页面而刷新视图
-
hash路由
- 通过 onhashchange 事件实现
- hash 改变时触发
- 特点(缺点) :URL 带
#
-
history 路由
-
history.pushState、history.replaceState(HTML5 history 的 api)实现,前者增加历史记录,后者是替换当前历史记录
-
使用 back()(等价于go(-1)), forward()等价于go(1)和 go() 方法可实现在用户历史记录中向后和向前的跳转
-
注意 pushState() 绝对不会触发 hashchange 事件(即时只改变hash),也不会触发 popstate 事件
popstate 事件能监听除 history.pushState() 和 history.replaceState() 外 url 的变化
-
不支持跨域,否则会抛出异常
-
优点:美观,甩掉了丑陋的
#
-
缺陷,对于服务器没有的资源,会出现 404
解决方法:服务端设置匹配不到静态资源则返回 index.html。
但这样会出现服务端不再返回 404 错误页面的问题,解决方法:在Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面,如
const router = new VueRouter({ mode: 'history', routes: [ { path: '*', component: NotFoundComponent } ] })
-
16. vue-router 路由守卫
17. Vuex 工作原理 (58:00)
-
状态共享管理,可以理解为为一个商店(store),各个组件可以获取商店的物品(state)
-
getter(可以认为是 store 的计算属性):可以对 state 中的数据进行计算,并根据其依赖缓存,仅当依赖的值变化时菜重新计算
-
mutation(可以认为是 store 的method属性)可以改变 state 中的数据的状态
-
使用方法:
mutations: { changeState (state [, obj]) { //handler } }
,changeState 是mutation 事件类型,多使用常量替代。如:// mutations.js export default const SOME_MUTATION = 'SOME_MUTATION' // store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutations' const store = new Vuex.Store({ state: { ... }, mutations: { // 计算属性命名功能来使用一个常量作为函数名(ES2015 风格) [SOME_MUTATION] (state) { // mutate state } } }) })
-
调用(提交)方法:
1.main.js中提交:store.commit( 'changeState' [, obj])
,可以通过 obj 进行跨组件传参
2.组件中提交:使用this.$store.commit('xxx')
-
注意: mutation 必须是同步函数
若是在 mutation 中混合异步调用会导致你的程序很难调试
-
-
action:action 类似于 mutation,
- 区别
1.action 提交的是 mutation,而不是直接变更状态。
2.action 可以包含任意异步操作。 - 示例
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
- 分发 action:
1.载荷形式:store.dispatch('xxx', { //... })
2.对象形式:store.dispatch( { type: 'xxx', key: value } )
3.组件中分发:this.$store.dispatch('xxx')
或使用 mapAction辅助函数...mapAction([ 'xxx' ])
(将 this.xxx() 映射为this.$store.dispatch('xxx')
)
- 区别
-
module:将 store 模块化,使得逻辑更加清晰