vue相关基础
# VUE
# 核心重要
# 几个核心思想
- 数据驱动
- 组件化
- 虚拟dom、diff局部最优更新
# 核心知识点
# 数据驱动分析
- Vue 实例化
- Vue 实例挂载
- render
- Virtual DOM
- createElement
- update
# 组件化分析
- createComponent
- patch
- 合并配置
- 生命周期
- 组件注册
- 异步组件
# 响应式原理分析
- 响应式对象 / 依赖收集
- 派发更新 / nextTick
- 检测变化的注意事项
- 计算属性 VS 侦听属性
- 组件更新
# 总体把握
- template风格、对象配置风格
- 虚拟dom思想(js对象操作代替dom操作)
- diff算法思想(同层比较,添加、移动、删除)
- 组件化思想(组件编译、组件通信)
- 数据响应式(依赖收集、派发更新,发布订阅)
图示分析
instance/index.js
真正的Vue的构造函数,并在Vue的原型上扩展方法core/index.js
增加全局API
方法runtime/index.js
扩展$mount
方法及平台对应的代码
# 特性
# SPA/SSR
# 单页面应用和多页面应用区别及优缺点
- SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一 个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。 多页应用跳转,需要整页资源刷新。
区别:
# 如何在单页 Vue 应用(SPA)中实现路由
可以通过官方的 vue-router 库在用 Vue 构建的 SPA 中进行路由。该库提供了全面的功能集,其中包括嵌套路线、路线参数和通配符、过渡、HTML5 历史与哈希模式和自定义滚动行为等功能。 Vue 还支持某些第三方路由器包。
# MVVM
图示比较:
# 区别
mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到View 。
# VUE示例
通过一个 Vue 实例来说明 MVVM 的具体实现
(1)View 层
<div id="app">
<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>
</div>
2
3
4
(2)ViewModel 层
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
ajax({// Ajax 获取 Model 层的数据
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(3) Model 层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "samy",
"domain": "www.baidu.com"
}
}
2
3
4
5
6
7
8
# 渐进式框架
使用渐进式框架的代价很小,从而使现有项目(使用其他技术构建的项目)更容易采用并迁移到新框架。 Vue.js 是一个渐进式框架
,因为你可以逐步将其引入现有应用,而不必从头开始重写整个程序。
Vue 的最基本和核心的部分涉及“视图”层,因此可以通过逐步将 Vue 引入程序并替换“视图”实现来开始你的旅程。
由于其不断发展的性质,Vue 与其他库配合使用非常好,并且非常容易上手。这与 Angular.js 之类的框架相反,后者要求将现有程序完全重构并在该框架中实现。
# 响应原理/双向数据绑定
vue 实现数据双向绑定主要是
Vue 采用 数据劫持 结合 发布者-订阅者 模式的方式,
通过
Object.defineProperty()
来劫持各个属性的 setter 以及 getter,在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。
在数据变动时发布消息给订阅者,触发相应的监听回调。【要点】
具体就是【要点】:Vue实现数据双向绑定的效果,需要三大模块;
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者(OCWD),
- 通过Observe来监听model的变化,
- 通过Compile来解析编译模版指令,
- 最终利用Watcher搭起Observer和Compile之前的通信桥梁,
从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。【要点】
# defineProperty
- Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
- Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、unshift()、shift()、splice()、sort()、reverse() 这7种方法,其他数组的属性检测不到。【PPUSSSR】
有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。
# proxy
# Proxy与Object.defineProperty的优劣对比?
Proxy的优势如下:
- Proxy可以直接监听对象而非属性
- Proxy可以直接监听数组的变化
- Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的
- Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
- Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
Object.defineProperty的优势如下:
- 兼容性好,支持IE9
Object.defineProperty(user,'name',{
set:function(key,value){
}
})
var user = new Proxy({},{
set:function(target,key,value,receiver){}
})
let obj = {name:'',age:'',sex:'' }
let handler = {
get(target, key, receiver) {
console.log("get", key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
}
};
let proxy = new Proxy(obj, handler);
proxy.name = "李四";
proxy.age = 24;
// set name 李四
// set age 24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 总体流程
# 1. 响应式对象
响应式对象,核心就是利用 Object.defineProperty
给数据递归添加了 getter
和 setter
,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集
,setter 做的事情是派发更新
。本质上是发布订阅模式(观察者模式)
。
# 2. 依赖收集
所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。
在定义相应式对象的的getter函数里,触发dep.depend
做依赖收集,将获取属性的地方全部加入订阅者列表中,当数据发生变化时,通过遍历订阅者列表实现变更发布。
再次render时会先做依赖清除,再次进行新的依赖收集,这样做是为了处理v-if条件渲染的数据不用再派发更新了。
# 3. 派发更新
实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。
通过setter来触发变量的更新,这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发更新,而是先添加到一个队列里,然后在 nextTick 后执行更新,可以理解为等一段时间一起更新。
队列排序 queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:
- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
- 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
- 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
# 虚拟DOM
虚拟DOM的实现就是普通对象包含tag、data、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存)
减少了对真实DOM的操作;
- 由于dom操作耗时十分长,且dom对象的体积很大,单个div的dom属性就有294个之多;
- Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。
- VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
- Virtual DOM到真实的dom需要经过以下过程:VNode 的 create、diff、patch;
# 实现原理步骤
虚拟 DOM 的实现原理主要包括以下 3 部分:
- create: 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 — 比较两棵虚拟 DOM 树的差异;
- pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
# vue2diff
原理
Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
Vue3
中采用最长递增子序列实现diff
算法;
diff主要流程:
先同级比较,再比较子节点;
- 先比较是否是相同节点;
- 相同节点比较属性,并复用老节点;
- 先判断一方有儿子,一方没有儿子的情况;
- 如果老有,新没有,移除老节点的信息,
- 如果老没有,新有,把新节点的信息替换到老节点上;
- 比较都有儿子的情况;【最复杂的,子流程】
- 考虑老节点和新节点儿子的情况;
- 优化比较:头头、尾尾、头尾、尾头;
- 比对查找进行复用;
- 递归比较子节点;
# 模板编译原理
总体流程:会将template => ast树 => codegen => 转化为render函数 => 内部调用的就是_c方法 => 虚拟dom
- vue的模板编译过程如下:template - ast - render函数; 将
template
转行成render
函数; - template=> ast => codegen => with+function 实现生成render方法;
核心步骤:
- 1.将template模板转换成
ast
语法树 -parserHTML
; - 2.对静态语法做静态标记 -
markUp
; - 3.重新生成代码 -
codeGen
;
# 对比vue3/react
# 与vue3比较
- 对
TypeScript
支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型)vue3
中响应式数据原理改成proxy
; - 大量的
API
挂载在Vue对象的原型上,难以实现TreeShaking
。 - 支持
Composition API
; 受ReactHook
启发 - 架构层面对跨平台
dom
渲染开发支持不友好 vdom
的对比算法更新,只更新vdom
的绑定了动态数据的部分;- 对虚拟DOM进行了重写、对模板的编译进行了优化操作...
# 与react比较
react整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,所以在react中,是单向数据流;
vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。
- vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
props
是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;- 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
- 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
- vue使用
插槽
分发内容,使得可以混合父组件的内容与子组件自己的模板; - vue多了
指令系统
,让模版可以实现更丰富的功能,而React只能使用JSX语法; - Vue增加的语法糖
computed
和watch
,而在React中需要自己写一套逻辑来实现; - react的思路是
all in js
,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。 - react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些,比如
redux
的combineReducer
就对应vuex
的modules
, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。 - react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合
redux-form
,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin
# 和其它框架(jquery)的区别是什么?哪些场景适合?
区别:vue数据驱动,通过数据来显示视图层而不是节点操作。
场景:数据操作比较多的场景,更加便捷
# Vue与AngularJS的区别
- Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
- AngularJS依赖对数据做脏检查,所以
Watcher
越多越慢;Vue.js使用基于依赖追踪
的观察并且使用异步队列
更新,所有的数据都是独立触发的。 - AngularJS社区完善, Vue的学习成本较小
# 生命周期
# 各个生命周期及描述 (8+2)
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前;vue实例的挂载元素$el和数据对象 data都是undefined, 还未初始化 |
created | 组件实例已经完全创建,属性也绑定,完成了 data数据初始化;但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用;vue实例的$el和data都初始化了 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子;在 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
# 生命周期总括【要点】
8 个生命周期,创建前/创建后、挂载前/挂载后、更新前/更新后、销毁前/销毁后。Vue 生命周期的作用
是方便我们通过它的生命周期,在业务代码中更好地操作数据,实现相关功能。这些函数称为生命周期 hook
8个生命周期过程:Vue 实例(组件)从其初始化到销毁和删除都经历生命周期;
- 创建前/后:
- 在 beforeCreated 阶段,Vue 实例的挂载元素
$el
和数据对象 data 以及事件还未初始化。 - 在 created 阶段,Vue 实例的数据对象 data 以及方法的运算有了,
$el
还没有。
- 在 beforeCreated 阶段,Vue 实例的挂载元素
- 载入前/后:
- 在 beforeMount 阶段,
render
函数首次被调用,Vue 实例的 $el 和 data 都初始化了,但还是挂载在虚拟的 DOM 节点上。 - 在 mounted 阶段,Vue 实例挂载到实际的 DOM 操作完成,一般在该过程进行 Ajax 交互。
- 在 beforeMount 阶段,
- 更新前/后:
- 在数据更新之前调用,即发生在虚拟 DOM 重新渲染和打补丁之前,调用 beforeUpdate。
- 在虚拟 DOM 重新渲染和打补丁之后,会触发 updated 方法。
- 销毁前/后:
- 在执行实例销毁之前调用 beforeDestory,此时实例仍然可以调用。
- 在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 Vue 实例已经解除了事件监听以及和 DOM 的绑定,但是 DOM 结构依然存在
# 补充
created
实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。(服务端渲染支持created方法);mounted
实例已经挂载完成,可以进行一些DOM操作;beforeUpdate
可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程;updated
可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用;destroyed
可以执行一些优化操作,清空定时器,解除绑定事件;
# watch、computed与methods的联系和区别【要点】
computed 就是计算属性,其可以当成一个data数据来使用。直接使用,不用像方法那样调用();
watch 就是监听的意思,其专门用来观察和响应Vue实例上的数据的变动。
能使用watch
属性的场景基本上都可以使用computed
属性,而且computed
属性开销小,性能高,因此能使用computed
就尽量使用computed
属性
想要执行异步或昂贵的操作以响应不断变化的数据时,这时watch
就派上了大用场。
其应用场景一般都是搜索框之类的,需要不断的响应数据的变化;如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择。
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值;
# watch中的deep:true
是如何实现的
当用户指定了watch
中的deep
属性为true
时,如果当前监控的值是数组类型,会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新;
ps: computed时不用设置的,默认获取对象中全部数据,再JSON.stringify({})
到渲染页面上;
# keep-alive
了解及原理使用?
#
keep-alive
平时在哪使用?原理是?
keep-alive
可以实现组件的缓存,当组件切换时不会对单曲最近进行卸载。
常用的2个属性include/exclude
,2个生命周期activated/deactivated
;
# 更新检测
# 数组更新检测 vm.$set
数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,pop,unshift,shift,splice,sort,reverse)7【PPUSSSR】个方法进行重写。
由于 JavaScript 的限制,Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
解决办法:
解决上面1问题: Vue 提供了以下操作方法:
Vue.set(vm.items, indexOfItem, newValue)// Vue.set
vm.$set(vm.items, indexOfItem, newValue)// vm.$set,Vue.set的一个别名
vm.items.splice(indexOfItem, 1, newValue)// Array.prototype.splice; 底层实现方式;
2
3
解决上面2问题:Vue 提供了以下操作方法:
vm.items.splice(newLength)// Array.prototype.splice
示例:通过socket监听设备在线状态
sockets: {
sDevices: function (value) {
const index = this.data.findIndex(item => value.id === item.id)
if (index >= 0) {
this.data.splice(index, 1, value)
}
}
},
2
3
4
5
6
7
8
# 对象更新检测
还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除:
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
来实现为对象添加响应式属性
# 给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?
<template>
<div>
<ul>
<li v-for="value in obj" :key="value"> {{value}} </li>
</ul>
<button @click="addObjB">添加 obj.b</button>
</div>
</template>
<script>
export default {
data () {
return {
obj: {
a: 'obj.a'
}
}
},
methods: {
addObjB () {
this.obj.b = 'obj.b'
console.log(this.obj)
}
}
}
</script>
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
点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局 api $set():
addObjB () (
this.$set(this.obj, 'b', 'obj.b')
console.log(this.obj)
}
2
3
4
$set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
# data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?nextick
不会立即同步执行重新渲染。 Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。 Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。 然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际(已去重的)工作。
# Vue.set/delete
方法是如何实现的?
为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组
vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判断属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
# vue中常用的命令
# 常用指令
v-once
渲染一次 (可用作优化,但是使用频率极少)v-html
将字符串转化成dom
插入到标签中 (会导致xss攻击问题,并且覆盖子元素)v-if/v-else/v-else-if
不满足时dom
不存在(可以使用template标签)v-show
不满足时dom
隐藏 (不能使用template标签)v-for
循环字符串、对象、数字、数组 (循环时必须加key,尽量不采用索引)v-bind
可以简写成: 属性(style、class...)绑定v-on
可以简写成@ 给元素绑定事件 (常用修饰符 .stop、.prevent、.self、.once、.passive)v-model
双向绑定 (支持.trim、.number修饰符)
# 事件 & 按键修饰符
对于 .passive
、.capture
和 .once
这些事件修饰符, Vue 提供了相应的前缀可以用于 on
:
修饰符 | 前缀 |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once 或 .once.capture | ~! |
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键: .enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 (opens new window)) |
修饰键: .ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey 、shiftKey 或者 metaKey ) |
示例:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
//这里是一个使用所有修饰符的例子:
on: {
keyup: function (event) {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果按下去的不是 enter 键或者
// 没有同时按下 shift 键
// 则返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 常见的事件修饰符及其作用
事件修饰符有:.capture、.once、.passive 、.stop、.self、.prevent、
.stop
:等同于 JavaScript 中的event.stopPropagation()
,防止事件冒泡;.prevent
:等同于 JavaScript 中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture
:与事件冒泡的方向相反,事件捕获由外到内;.self
:只会触发自己范围内的事件,不包含子元素;.once
:只会触发一次。
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
<!--处理函数只会在 $event.key 等于 PageDown 时被调用-->
<input v-on:keyup.page-down="onPageDown">
<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
//希望使用户能够按下键盘上的Enter键,来将内容提交给名为 “storeComment” 的方法
<textarea @keyup.enter="storeComment"></textarea>
new Vue({
el: '#app',
methods: {
storeComment(event) {
//using event.target.value or use v-model to bind to a data property
}
}
});
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
# v-if
和v-show
指令
# 显示比较
- 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
- 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
- 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
# 原理比较
- v-if在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
- v-show会被编译成指令,条件不满足时控制样式将对应节点隐藏 (内部其他指令依旧会继续执行)
扩展回答: 频繁控制显示隐藏尽量不使用v-if,v-if和v-for尽量不要连用,要处理的话,使用在外一层控制或者用计算属性处理;
const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`
<div v-if="true"><span v-for="i in 3">hello</span></div>`
);
/** with(this) {
* return (true) ? _c('div', _l((3), function (i) { return _c('span', [_v("hello")]) }), 0) : _e()
* }
**/
const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`<div v-show="true"</div>`);
// with(this) {
// return _c('div', {
// directives:[{
// name:"show",
// rawName:"v-show",
// value:(true),
// expression:"true",
// }]
// })
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# v-if、v-show、v-html 的原理
- v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
- v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;
- v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。
# v-html
会导致哪些问题?
- 可能会导致
xss
攻击; v-html
会替换掉标签内部的子元素;
# v-if
和 v-for
指令
不推荐同时使用 v-if
和 v-for
; 当 v-if
与 v-for
一起使用时,v-for
具有比 v-if
更高的优先级。这意味着 v-if
将分别重复运行于每个 v-for
循环中。
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
优化方案一:
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>
<!-- 上面的代码将只渲染未完成的 todo -->
<!-- 优化后 -->
<!--而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 <template>)上。-->
<ul v-if="todos.length">
<li v-for="todo in todos">
{{ todo }}
</li>
</ul>
<p v-else>No todos left!</p>
2
3
4
5
6
7
8
9
10
11
12
13
优化方案二:
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
//不推荐:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# v-for为什么要加key
而且key, 不能是index或者随机数;如果是的话,在操作添加或者删除时会错位;
为了在比对过程中进行复用 ;
# v-model
的实现原理及如何自定义
**组件:**会将组件的
v-model
默认转化成value+input
方法的语法糖;**原生:**原生的
v-model
,会根据标签的不同生成不同的事件和属性input
,checkbox
,select
- text 和 textarea 元素:使用
value
属性和input
事件; - checkbox 和 radio: 使用
checked
属性和change
事件; - select :将
value
作为 prop 并将change
作为事件;
- text 和 textarea 元素:使用
上面是具体实现,内部实现还是:vue数据双向绑定实现原理解析
<input v-model="searchText">
<!--等价于:-->
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
<input type="text" v-model="nameInput">
<input type="text" :value="nameInput" @keyup="nameInput = $event.target.value">
2
3
4
5
6
7
8
9
组件的v-model
就是value+input
方法的语法糖;
// 父组件
<aa-input v-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
// 子组件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:{value:aa,}
methods:{
onmessage(e){
$emit('input',e.target.value)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
<custom-input v-model="searchText"></custom-input>
//相当于
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 如何理解自定义指令?
指令的实现原理,可以从编译原理=>代码生成=>指令钩子实现进行概述
实现步骤:
- 1.在生成
ast
语法树时,遇到指令会给当前元素添加directives属性; - 2.通过
genDirectives
生成指令代码; - 3.在patch前将指令的钩子提取到
cbs
中, 在patch过程中调用对应的钩子; - 4.当执行指令对应钩子函数时,调用对应指令定义的方法;
// <i-button v-action:add >添加用户</a-button>
// <a-button v-action:delete>删除用户</a-button>
// <a v-action:edit @click="edit(record)">修改</a>
const action = Vue.directive('action', {
inserted: function (el, binding, vnode) {
const actionName = binding.arg
const roles = store.getters.roles
const elVal = vnode.context.$route.meta.permission
const permissionId = elVal instanceof String && [elVal] || elVal
roles.permissions.forEach(p => {
if (!permissionId.includes(p.permissionId)) {
return
}
if (p.actionList && !p.actionList.includes(actionName)) {
el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
// 因为 v-show 的话,dom其实没有隐藏,用户可以改变display就看到,v-if呢
}
})
}
})
export default action
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//注册
Vue.directive('enterToNext', {
inserted: function (el) {
console.log('enterToNext...')
// let frm = el.querySelector('.el-form');
const inputs = el.querySelectorAll('input')
console.log(inputs)
for (var i = 0; i < inputs.length; i++) {
inputs[i].setAttribute('keyFocusIndex', i)
inputs[i].addEventListener('keyup', (ev) => {
if (ev.keyCode === 13) {
const targetTo = ev.srcElement.getAttribute('keyFocusTo')
if (targetTo) {
this.$refs[targetTo].$el.focus()
} else {
var attrIndex = ev.srcElement.getAttribute('keyFocusIndex')
var ctlI = parseInt(attrIndex)
if (ctlI < inputs.length - 1) { inputs[ctlI + 1].focus() }
}
}
})
}
}
})
//使用
<el-form
ref="postForm"
v-enterToNext="true"
:model="postForm"
:rules="rules"
label-position="left"
label-width="110px"
size="mini"
status-icon
>
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
# Vue.directive
源码实现
把定义的内容进行格式化挂载到Vue.options
属性上;
Vue.directive('demo', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
el.style.top = binding.value + 'px'
}
})
2
3
4
5
6
# 组件事件
# Vue中事件绑定的原理
Vue
的事件绑定分为两种:
- 原生Dom的事件绑定,采用的
addEnventListener
实现; - 组件的事件绑定,采用的是
$on
实现; 组件中的nativeOn等价于普通元素on, 组件的on会单独处理;
# 事件的编译
let compiler = require("vue-template-compiler");
let r1 = compiler.compile("<div @click='fn()' > xxx </div>");
let r2 = compiler.compile(
"<my-component @click.native='fn' @onclick='fn1'>xxx </my-component>"
);
console.log(r1.render);
//with(this){return _c('div',{on:{"click":function($event){return fn()}}},[_v(" xxx ")])}
console.log(r2.render);
// with(this){return _c('my-component',{on:{"onclick":fn1},nativeOn:{"click":function($event){return fn($event)}}},[_v("xxx ")])}
2
3
4
5
6
7
8
9
# 将原生事件绑定到组件
想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on
的 .native
修饰符:
<base-input v-on:focus.native="onFocus"></base-input>
再升级处理:子组件套了一层;
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
2
3
4
5
6
7
8
这时,父级的 .native
监听器将静默失败。它不会产生任何报错,但是 onFocus
处理函数不会如你预期地被调用。为了解决这个问题,Vue 提供了一个 $listeners
属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
this.$listeners,// 我们从父级添加所有的监听器 【处理的核心】
{// 然后我们添加自定义监听器,或覆写一些监听器的行为
input: function (event) { // 这里确保组件配合 `v-model` 的工作
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
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
现在 <base-input>
组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 元素一样使用了:所有跟它相同的 attribute 和监听器的都可以工作。
# 组件通信【要点】
常见使用场景可以分为三类:
- 父子通信:props / $emit;$parent / $children/ ref;$attrs/$listeners;provide / inject API;
- 兄弟通信:eventBus; Vuex
- 跨级通信:
$emit
/$on
,Vuex;$attrs/$listeners;provide / inject API
总括:常用的三种: props / $emit
, EventBus ($emit / $on)
, Vuex ;
单向数据流;
- props和$emit :父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的;
- $parent,$children: 获取当前组件的父组件和当前组件的子组件;
$refs
获取实例的方式调用组件的属性或者方法;- 父组件中通过
provide
来提供变量,然后在子组件中通过inject
来注入变量; $attrs
和$listeners
:A->B->C。Vue 2.4 开始提供了$attrs
和$listeners
来解决这个问题;envetBus
平级组件数据传递;这种情况下可以使用中央事件总线的方式;vuex
状态管理;
#
$attrs
是为了解决什么问题出现的?应用场景有哪些?provide/inject 不能解决它能解决的问题吗?
- $attrs主要的作用就是实现批量传递数据。
- provide/inject更适合应用在插件中,主要是实现跨级数据传递;
# 异步渲染/组件
因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能考虑。Vue
会在本轮数据更新后,再去异步更新视图;
# nextTick
在哪里使用?原理是?
大家所熟悉的 Vue API Vue.nextTick
全局方法和 vm.$nextTick
实例方法的内部都是调用 nextTick
函数,该函数的作用可以理解为异步执行传入的函数。
nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。原理就是异步方法(promise,mutationObserver,setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务)
延迟调用优先级如下:Promise > MutationObserver > setImmediate > setTimeout【PMSS】
nextTick
方法主要使用了宏任务和微任务,定义了一个异步方法。多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。
nextTick主要是通过js eventLoop的执行机制原理,将回调通过(promise)添加到microTask上面,来实现,在下一次DOM周期后执行回调函数。
const callback = []
let pendding = false
function nextTick(cb){
if(cb){
callback.push(()=>{cb()})
}
if(!pendding){
pendding = true
promise.resolve().then(()=>{
padding =false
const copies = callback.slice(0)
callback.length = 0
for(let i = 0;i < copies.length;i++){
copies[i]()
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在
nextTick()
的回调函数中。 - 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在
nextTick()
的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()
的回调函数中。
new Vue({ // 示例
methods: {
example: function () {
this.message = 'changed'// 修改数据
this.$nextTick(function () {// DOM 还没有更新
this.doSomethingElse() // DOM 现在更新了// `this` 绑定到当前实例
})
}
}
})
2
3
4
5
6
7
8
9
10
# 为什么要使用异步组件?
如果组件功能多打包出的结果会变大,可以采用异步的方式来加载组件。主要依赖import()
这个语法,可以实现文件的分割加载;
component: () => import('@/views/login'), //require([])
# sync
修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
推荐以 update:myPropName
的模式触发事件取而代之; 父组件可以监听那个事件并根据需要更新一个本地的数据属性
this.$emit('update:title', newTitle)
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
<!-- 为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:-->
<text-document v-bind:title.sync="doc.title"></text-document>
2
3
4
5
6
7
8
注意带有 .sync
修饰符的 v-bind
不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model
。
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync
修饰符和 v-bind
配合使用:
<text-document v-bind.sync="doc"></text-document>
这样会把 doc
对象中的每一个属性 (如 title
) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on
监听器。
将 v-bind.sync
用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
# 组件渲染
# 组件中写name选项又哪些好处及作用?
- 可以通过名字找到对应的组件 (递归组件)
- 可用通过name属性实现缓存功能 (keep-alive)
- 可以通过name来识别组件 (跨级组件通信时非常重要)
# Vue.mixin
的使用场景和原理?
Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。
mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题", 这里强调一下mixin的数据是不会被共享的!
/*合并两个option对象到一个新的对象中*/
export function mergeOptions(){}
Vue.mixin = function (obj) {
this.options = mergeOptions(this.options,obj);
}
Vue.mixin({
beforeCreate(){
console.log('before create ok')
}
})
2
3
4
5
6
7
8
9
10
11
vuex的挂载
export default function applyMixin(Vue) {
// const version = Number(Vue.version.split('.')[0])
// 父子组件的beforecreate执行顺序
Vue.mixin({
//TODO:了解mixin的原理
beforeCreate: vuexInit, // 内部会把生命周期函数 拍平成一个数组
// destory: "xxx"
});
}
// 组件渲染时从父=》子
// 给所有的组件增加$store 属性 指向我们创建的store实例
function vuexInit() {
const options = this.$options; // 获取用户所有的选项
if (options.store) {
this.$store = options.store; // 根实例;比如:main.js
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store; // 儿子 或者孙子....
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# component / extend / mixins / extends 的区别
- Vue.component 注册全局组件,为了方便
- Vue.extend 创建组件的构造函数,为了复用
- mixins、extends 为了扩展
如果按照优先级去理解,当你需要继承一个组件时,可以使用Vue.extend().当你需要扩展组件功能的时候,可以使用extends,mixins;
mixins 选项接受一个混合对象的数组。这些混合实例对象可以像正常的实例对象一样包含选项,他们将在 Vue.extend() 里最终选择使用相同的选项合并逻辑合并。
这和 mixins 类似,区别在于,组件自身的选项会比要扩展的源组件具有更高的优先级.
官方文档是这么写的,除了优先级,可能就剩下接受参数的类型吧,mixins接受的是数组.
# 简述 mixin、extends 的覆盖逻辑【要点】
(1)mixin 和 extends mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
- mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
- extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
(2)mergeOptions 的执行过程
- 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
- 对未合并的选项,进行判断
if(!child._base) {
if(child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if(child.mixins) {
for(let i = 0, l = child.mixins.length; i < l; i++){
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
2
3
4
5
6
7
8
9
10
- 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
- 返回合并结果 options。
# extend
Vue.extend使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
内部实现:
Vue.extend = function (extendOptions: Object): Function {
// ...
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.options = mergeOptions( // 子组件的选项和Vue.options进行合并
Super.options,
extendOptions
)
// ...
return Sub;
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)// Vue.component 中使用的是Vue.extend方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用:
// Vue.extend
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'samy'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
2
3
4
5
6
7
8
9
10
11
12
13
14
# mixins
mixins 选项接受一个混入对象的数组。这些混入实例对象可以像正常的实例对象一样包含选项,他们将在 Vue.extend() 里最终选择使用相同的选项合并逻辑合并。举例:如果你的混入包含一个钩子而创建组件本身也有一个,两个函数将被调用。 Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
// mixins示例
var mixin = {
created: function () { console.log(1) }
}
var vm = new Vue({
created: function () { console.log(2) },
mixins: [mixin]
})
// => 1
// => 2
2
3
4
5
6
7
8
9
10
mixins要点
- data混入组件数据优先
- 钩子函数将混合为一个数组,混入对象的钩子将在组件自身钩子之前调用
- 值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。
- 以上合并策略可以通过
Vue.config.optionMergeStrategies
修改
# extends
允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件。这和 mixins 类似。
// extends示例
var CompA = { ... }
// 在没有调用 `Vue.extend` 时候继承 CompA
var CompB = {
extends: CompA,
...
}
2
3
4
5
6
7
8
# 总结
- mixins可以混入多个mixin,extends只能继承一个
- mixins类似于面向切面的编程(AOP),extends类似于面向对象的编程;
- extend用于创建vue实例;
- 优先级Vue.extend>extends>mixins
# Vue
中slot插槽
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
# 普通插槽
普通插槽(模板传入到组件中,数据采用父组件数据)
- 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类;
{a:[vnode],b:[vnode]}
; - 渲染组件时会拿到对应的
slot
属性的节点进行替换操作;【插槽的作用域为父组件】
# 作用域插槽
作用域插槽(在父组件中访问子组件数据)
- 作用域插槽在解析的时候,不会作为组件的孩子节点;
- 会解析成函数,当子组件渲染时,会调用此函数进行渲染;【插槽的作用域为子组件】
# 实现原理
当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
# 组件渲染和更新过程
- 渲染组件时,会通过
Vue.extend
方法构建子组件的构造函数,并进行实例化,最终手动调用$mount()
进行挂载; - 更新组件时,会进行
patchvnode
流程,核心就是diff算法;
父子组件渲染的先后顺序;
组件是如何渲染到页面上的;
①在渲染父组件时会创建父组件的虚拟节点,其中可能包含子组件的标签 ;
②在创建虚拟节点时,获取组件的定义使用
Vue.extend
生成组件的构造函数;③将虚拟节点转化成真实节点时,会创建组件的实例并且调用组件的$mount方法;
④所以组件的创建过程是先父后子;
# 父子组件的生命周期调用顺序
可以归类为以下 4 部分:
加载渲染过程; 这个特别点;后面的mount操作顺序;
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程;
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程;
父 beforeUpdate -> 父 updated
销毁过程;
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
组件的调用顺序都是先父后子,渲染完成的顺序肯定是先子后父;
组件的销毁操作先父后子,销毁完成是先子后父;
# 渲染
# template模版编译
编译的核心是把 template 模板编译成 render 函数,主要分为如下三个步骤:
- 生成AST树
- 优化
- codegen
Vue 中 template 就是先转化成 AST 树,再得到 render 函数返回 VNode(Vue 的虚拟 DOM 节点)。
- 通过
compile 编译器
把 template 编译成 AST 语法树(abstract syntax tree - 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。 - AST 会经过 generate(将 AST 语法树转换成 render function 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有标签名、子节点、文本等待。
# 真实DOM
和其解析流程
流程图:
所有的浏览器渲染引擎工作流程大致分为5步:创建 DOM
树 —> 创建 Style Rules
-> 构建 Render
树 —> 布局 Layout
-—> 绘制 Painting
。 【D R 渲布绘】 【DCJ渲布绘】
- 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
- 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
- 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
- 第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
- 第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。
# Virtual DOM/VNode【要点】
# template 到 render 的过程
vue的模版编译过程主要如下:template -> ast -> render函数
vue 在模版编译版本的码中会执行 compileToFunctions 将template转化为render函数:
// 将模板编译为render函数
const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
2
CompileToFunctions中的主要逻辑如下∶
(1)调用parse方法将template转化为ast(抽象语法树)
const ast = parse(template.trim(), options)
- parse的目标:把tamplate转换为AST树,它是一种用 JavaScript对象的形式来描述整个模板。
- 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造AST树的目的。
AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本 **
(2)对静态节点做优化
optimize(ast,options)
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
const code = generate(ast, options)
generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``)
生成render函数。
# vue有了数据响应式,为何还要diff?
核心原因:粒度
React
通过setState
知道有变化了,但不知道哪里变化了,所以需要通过diff
找出变化的地方并更新dom。Vue
已经可以通过响应式系统知道哪里发生了变化,但是所有变化都通过响应式会创建大量Watcher
,极其消耗性能,因此vue采用的方式是通过响应式系统知道哪个组件发生了变化,然后在组件内部使用diff
。这样的中粒度策略,即不会产生大量的Watcher,也使diff的节点减少了,一举两得。
# 过滤器fillter
# 过滤器的作用,如何实现一个过滤器
根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用filters
来过滤数据,filters
不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed
,方法 methods
都是通过修改数据来处理数据格式的输出显示)。
使用场景:
- 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
- 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用我们的
fliters
过滤器来处理数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在**插值表达式 ** 和
v-bind
表达式中,然后放在操作符“ |
”后面进行指示。
全局安装:或者在创建 Vue 实例之前全局定义过滤器:
import Vue from 'vue'
import * as filter from './filter'
Object.keys(filter).forEach(k => Vue.filter(k, filter[k]))
export function formatDate(dataStr = new Date(), type = 1) {
let pattern;
switch (type) {
case 1:
pattern = "YYYY-MM-DD";
break;
case 2:
pattern = "YYYY-MM-DD HH:mm:ss";
break;
case 3:
pattern = "YYYYMMDD";
break;
case 4:
pattern = "YYYYMM";
break;
case 5:
pattern = "YYYY-MM-DD HH:mm";
break;
case 6: //周
pattern = "YYYYWW";
break;
}
return moment(dataStr).format(pattern);
}
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
html及js中使用
<span class="people">
{{
allEntranceNums.mod | numberFormat
}}
</span>
startTime: this.$options.filters["formatDate"](startTime, 2),
endTime: this.$options.filters["formatDate"](endTime, 2)
2
3
4
5
6
7
局部使用:你可以在一个组件的选项中定义本地的过滤器: 当全局过滤器和局部过滤器重名时,会采用局部过滤器。
<li>商品价格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
2
3
4
5
6
7
# 插件use
# Vue.use
的作用及原理?
Vue.use
是用来使用插件的,可以在插件中扩展全局组件、指令、原型方法等。
源码位置:
core/global-api/use.js:5
Vue.use = function (plugin: Function | Object) {
// 插件不能重复的加载
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this) // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
if (typeof plugin.install === 'function') { // 调用插件的install方法
plugin.install.apply(plugin, args) Vue.install = function(Vue,args){}
} else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
plugin.apply(null, args)
}
installedPlugins.push(plugin) // 缓存插件
return this
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在vuex中的使用;
// Vue.use 方法会调用插件的install方法,此方法中的参数就是Vue的构造函数
// Vue.use = function (plugin) { // vue中类似use原理实现
// plugin.install(this);
// }
// 插件的安装 Vue.use(Vuex)
export const install = (_Vue) => {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== "production") {
console.error(
"[vuex] already installed. Vue.use(Vuex) should be called only once."
);
}
return;
}
Vue = _Vue; // _Vue 是Vue的构造函数;vue缓存起来再暴露出去
applyMixin(Vue); // 需要将根组件中注入的store 分派给每一个组件 (子组件) Vue.mixin
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用插件
安装 Vue.js 插件。**如果插件是一个对象,必须提供 install
方法。如果插件是一个函数,它会被作为 install 方法。**install 方法调用时,会将 Vue 作为参数传入。
该方法需要在调用 new Vue()
之前被调用。
当 install 方法被同一个插件多次调用,插件将只会被安装一次。Vue.use
会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
通过全局方法 Vue.use()
使用插件, 调用 MyPlugin.install(Vue)
。它需要在你调用 new Vue()
启动应用之前完成:
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
//也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
2
3
4
5
6
7
# 调用方式
隐式调用:Vue.js 官方提供的一些插件 (例如
vue-router
)在检测到Vue
是可访问的全局变量时会自动调用Vue.use()
。显示调用:然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用
Vue.use()
:
// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
2
3
4
5
# 步骤
采用ES6的
import ... from ...
语法或CommonJSd的require()
方法引入插件使用全局方法
Vue.use( plugin )
使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })
import router from './router'
import store from './store'
import ElementUi from 'element-ui'
import * as filters from './filters'
Vue.use(ElementUi)//显示调用
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
const app = new Vue({
el: '#app',
router,//隐式调用;
store,
...App
})
export default app
2
3
4
5
6
7
8
9
10
11
12
13
routet/index.js
import Vue from 'vue'
import Router from 'vue-router'
const login = () => import(/* webpackChunkName: "login" */ '../pages/login')
Vue.use(Router)
router.afterEach(transition => {
NProgress.done()
})
export default router
2
3
4
5
6
7
8
9
# 组件优化
# 如何保存页面的当前的状态
既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:
- 前组件会被卸载
- 前组件不会被卸载
那么可以按照这两种情况分别得到以下方法:
组件会被卸载:
(1)将状态存储在LocalStorage / SessionStorage
只需要在组件即将被销毁的生命周期 componentWillUnmount
(react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如我们从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果我们从别的组件跳转到 B 组件的时候,实际上我们是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以我们需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
优点
- 兼容性好,不需要额外库或工具。
- 简单快捷,基本可以满足大部分需求。
缺点
- 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)
- 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象
(2)路由传值 通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点
- 简单快捷,不会污染 LocalStorage / SessionStorage。
- 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)
缺点
- 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。
vuex及存储插件
/**
* plugin要实现的方法有;
* plugins subscribe replaceState
*/
// 自己实现vuex-persist插件
class VuexPersistence {
constructor({ storage }) {
this.storage = storage;
this.localName = "VUEX-MY";
}
plugin = (store) => {
let localState = JSON.parse(this.storage.getItem(this.localName));
if (localState) store.replaceState(localState);
store.subscribe((mutationName, state) => {
// 这里需要做一个节流 throttle lodash
this.storage.setItem(this.localName, JSON.stringify(state));
});
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
组件不会被卸载:
(1)单页面渲染 要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
优点
- 代码量少
- 不需要考虑状态传递过程中的错误
缺点
- 增加 A 组件维护成本
- 需要传入额外的 prop 到 B 组件
- 无法利用路由定位页面
除此之外,在Vue中,我们还可以是用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行 被包裹在keep-alive中的组件的状态将会被保留:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>
2
3
router.js
{
path: '/',
name: 'xxx',
component: ()=>import('../src/views/xxx.vue'),
meta:{
keepAlive: true // 需要被缓存
}
},
2
3
4
5
6
7
8
# keepalive
# 主要流程
- 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
- 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
- key生成规则,cid +"∶∶"+ tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件。
- 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。
- 最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。
# render函数
- 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件;
- keep-alive 只对第一个组件有效,所以获取第一个子组件;
- 和 keep-alive 搭配使用的一般有:动态组件 和router-view;
# keep-alive 本身的创建过程和 patch 过程
缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefned) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,
而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
- 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM;
- 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance
- prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
# LRU 缓存策略
从内存中找出最久未使用的数据并置换新的数据。 LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 链表满的时候,将链表尾部的数据丢弃;
# 设计模式
# vue
中使用了哪些设计模式?
工厂模式 - 传入参数即可创建实例 (
createElement
)根据传入的参数不同返回不同的实例
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { // ... if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { vnode = createComponent(tag, data, context, children) } // .... }
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单例模式
单例模式就是整个程序有且仅有一个实例。
export function install (_Vue) { if (Vue && _Vue === Vue) { if (__DEV__) { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } Vue = _Vue applyMixin(Vue) }
1
2
3
4
5
6
7
8
9
10
11
12发布-订阅模式
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { const vm: Component = this if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } Vue.prototype.$emit = function (event: string): Component { const vm: Component = this let cbs = vm._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) const info = `event handler for "${event}"` for (let i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info) } } return vm }
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观察者模式 :
watcher
&dep
的关系代理模式 (防抖和节流) => 返回替代 (例如:
Vue3
中的proxy)代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
装饰模式: @装饰器的用法
中介者模式 =>
vuex
中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。
策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案。
function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) }
1
2
3
4外观模式、适配器模式、迭代器模式、模板方法模式 .....
# 手写功能原理
# vm.$on
监听当前实例上的自定义事件。事件可以由
vm.$emit
触发。回调函数会接收所有传入事件触发函数的额外参数。
Vue.prototype.$on = function (event, fn) {
const vm = this
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push[fn]
}
return vm
}
2
3
4
5
6
7
8
9
10
11
温馨提示:_events是实现初始化的时候定义的,this.events = Object.create(null),所以不要困惑_events哪里来的。
vm.$on('test', function (msg) {
console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi"
2
3
4
5
# vm.$off
移除自定义事件监听器。
- 如果没有提供参数,则移除所有的事件监听器;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function (event, fn) {
const vm = this
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
vm.$off(event[i], fn)
}
return vm
}
const cbs = vm._events[event]
if (!cbs) return vm
if (arguments.length === 1) {
vm._events[event] = null
return vm
}
if (fn) {
let len = cbs.length
while (len--) {
let cb = cbs[len]
if (cb === fn || cb.fn === fn) {
cbs.splice(len, 1)
break
}
}
}
return vm
}
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
# vm.$once
监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
Vue.prototype.$once = function (event,fn){
const vm = this
function on(){
vm.$off(event,on)
fn.apply(vm,arguments)
}
on.fn = fn
vm.$on(event,on)
return vm
}
2
3
4
5
6
7
8
9
10
# vm.$emit
触发当前实例上的事件。附加参数都会传给监听器回调。
Vue.prototype.$emit = function (event, ...params) {
const vm = this
let cbs = vm._events[event]
if (cbs) {
for (let i = 0; i < cbs.length; i++) {
cbs[i].apply(vm, params)
}
}
return vm
}
2
3
4
5
6
7
8
9
10
# 缓存算法(FIFO/LRU)
# FIFO
最简单的一种缓存算法,设置缓存上限,当达到了缓存上限的时候,按照先进先出的策略进行淘汰,再增加进新的 k-v 。
使用了一个对象作为缓存,一个数组配合着记录添加进对象时的顺序,判断是否到达上限,若到达上限取数组中的第一个元素key,对应删除对象中的键值。
/**
* FIFO队列算法实现缓存
* 需要一个对象和一个数组作为辅助
* 数组记录进入顺序
*/
class FifoCache{
constructor(limit){
this.limit = limit || 10
this.map = {}
this.keys = []
}
set(key,value){
let map = this.map
let keys = this.keys
if (!Object.prototype.hasOwnProperty.call(map,key)) {
if (keys.length === this.limit) {
delete map[keys.shift()]//先进先出,删除队列第一个元素
}
keys.push(key)
}
map[key] = value//无论存在与否都对map中的key赋值
}
get(key){
return this.map[key]
}
}
module.exports = FifoCache
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
# LRU
LRU(Least recently used,最近最少使用)算法。该算法的观点是,最近被访问的数据那么它将来访问的概率就大,缓存满的时候,优先淘汰最无人问津者。
算法实现思路:基于一个双链表的数据结构,在没有满员的情况下,新来的 k-v 放在链表的头部,以后每次获取缓存中的 k-v 时就将该k-v移到最前面,缓存满的时候优先淘汰末尾的。
双向链表的特点,具有头尾指针,每个节点都有 prev(前驱) 和 next(后继) 指针分别指向他的前一个和后一个节点。
**关键点:**在双链表的插入过程中要注意顺序问题,一定是在保持链表不断的情况下先处理指针,最后才将原头指针指向新插入的元素,在代码的实现中请注意看我在注释中说明的顺序注意点!
/**
* LRU队列算法实现缓存
* 基于一个双链表的数据结构
* 需要一个对象和一头一尾
*/
class LruCache {
constructor(limit) {
this.limit = limit || 10
//head 指针指向表头元素,即为最常用的元素
this.head = this.tail = undefined
this.map = {}
this.size = 0
}
get(key, IfreturnNode) {
let node = this.map[key]
// 如果查找不到含有`key`这个属性的缓存对象
if (node === undefined) return
// 如果查找到的缓存对象已经是 tail (最近使用过的)
if (node === this.head) { //判断该节点是不是是第一个节点
// 是的话,皆大欢喜,不用移动元素,直接返回
return returnnode ? node : node.value
}
// 不是头结点,铁定要移动元素了
if (node.prev) { //首先要判断该节点是不是有前驱
if (node === this.tail) { //有前驱,若是尾节点的话多一步,让尾指针指向当前节点的前驱
this.tail = node.prev
}
//把当前节点的后继交接给当前节点的前驱去指向。
node.prev.next = node.next
}
if (node.next) { //判断该节点是不是有后继
//有后继的话直接让后继的前驱指向当前节点的前驱
node.next.prev = node.prev
//整个一个过程就是把当前节点拿出来,并且保证链表不断,下面开始移动当前节点了
}
node.prev = undefined //移动到最前面,所以没了前驱
node.next = this.head //注意!!! 这里要先把之前的排头给接到手!!!!让当前节点的后继指向原排头
if (this.head) {
this.head.prev = node //让之前的排头的前驱指向现在的节点
}
this.head = node //完成了交接,才能执行此步!不然就找不到之前的排头啦!
return IfreturnNode ?
node :
node.value
}
set(key, value) {
// 之前的算法可以直接存k-v但是现在要把简单的 k-v 封装成一个满足双链表的节点
//1.查看是否已经有了该节点
let node = this.get(key, true)
if (!node) {
if (this.size === this.limit) { //判断缓存是否达到上限
//达到了,要删最后一个节点了。
if (this.tail) {
this.tail = this.tail.prev
this.tail.prev.next = undefined
//平滑断链之后,销毁当前节点
this.tail.prev = this.tail.next = undefined
this.map[this.tail.key] = undefined
//当前缓存内存释放一个槽位
this.size--
}
node = {
key: key
}
this.map[key] = node
if(this.head){//判断缓存里面是不是有节点
this.head.prev = node
node.next = this.head
}else{
//缓存里没有值,皆大欢喜,直接让head指向新节点就行了
this.head = node
this.tail = node
}
this.size++//减少一个缓存槽位
}
}
//节点存不存在都要给他重新赋值啊
node.value = value
}
}
module.exports = LruCache
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
具体的思路就是如果所要get的节点不是头结点(即已经是最近使用的节点了,不需要移动节点位置)要先进行平滑的断链操作,处理好指针指向的关系,拿出需要移动到最前面的节点,进行链表的插入操作。
# 最长递增子序列
function lis(arr) {
let len = arr.length,
res = [],
dp = new Array(len).fill(1);
// 存默认index
for (let i = 0; i < len; i++) {
res.push([i])
}
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i],
nextIndex = undefined;
// 如果为-1 直接跳过,因为-1代表的是新节点,不需要进行排序
if (cur === -1) continue
for (let j = i + 1; j < len; j++) {
let next = arr[j]
// 满足递增条件
if (cur < next) {
let max = dp[j] + 1
// 当前长度是否比原本的长度要大
if (max > dp[i]) {
dp[i] = max
nextIndex = j
}
}
}
// 记录满足条件的值,对应在数组中的index
if (nextIndex !== undefined) res[i].push(...res[nextIndex])
}
let index = dp.reduce((prev, cur, i, arr) => cur > arr[prev] ? i : prev, dp.length - 1)
// 返回最长的递增子序列的index
return result[index]
}
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
# 懒加图片载
**原理:**监听页面滚动事件,做防抖处理,计算图片是否在可视区域,如果在则设置图片src
属性,并监听图片加载完成事件,图片加载成功后移除滚动事件监听即可。
- 使用
<!-- 如果滚动的容器是window -->
<LazyLoadImg src="123.png"/>
<!-- 如果滚动的容器是页面内部某个容器 -->
<LazyLoadImg src="123.png" :container="() => $refs.container"/>
2
3
4
- 源码
<template>
<img
ref="image"
v-if="selfSrc"
v-show="loaded"
class="lazy-load-img"
:src="selfSrc"
@load="loadImage"
/>
<div
ref="contentDom"
v-else-if="!loaded"
class="lazy-load-img-container"
>
<div class="lazy-load-img-content"><div class="loading"></div></div>
</div>
</template>
<script>
export default {
name: 'LazyLoadImg',
props: {
src: {
type: String,
default: ''
},
offset: {
type: Number,
default: 50
},
container: {
type: Function,
default: () => (undefined)
}
},
data() {
return {
loaded: false, // 已加载
isVisible: false, // 是否在可视区域
selfSrc: '',
};
},
mounted() {
this.$container = this.container() || document.documentElement || document.body;
this.contentDom = this.$refs.contentDom;
this.target = this.container() || window;
this.target.addEventListener('scroll', this.scrollHandle);
},
beforeDestroy() {
this.removeEvent();
},
methods: {
scrollHandle() {
// console.log(this.isVisible);
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
const top = this.contentDom.offsetTop;
const height = this.contentDom.clientHeight;
const clientHeight = this.$container.clientHeight;
const scrollTop = this.$container.scrollTop;
const viewTop = top - (scrollTop + clientHeight);
this.isVisible = viewTop < this.offset && (top + height + this.offset > scrollTop);
// console.log(viewTop, top + height - scrollTop, this.isVisible);
if (this.isVisible) {
this.selfSrc = this.src;
this.removeEvent();
}
}, 100);
},
removeEvent() {
this.target.removeEventListener('scroll', this.scrollHandle);
},
loadImage() {
this.loaded = true;
}
}
}
</script>
<style>
.lazy-load-img-container{
display: inline-block;
width: 600px;
height: 350px;
background-color: #eee;
color: #aaa;
}
.lazy-load-img-container .lazy-load-img-content{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.lazy-load-img-container .loading{
width: 15px;
height: 15px;
border-radius: 100%;
margin: 2px;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
border: 3px solid #ddd;
border-bottom-color: transparent;
height: 25px;
width: 25px;
background: transparent !important;
display: inline-block;
-webkit-animation: loadingRotate 0.75s 0s linear infinite;
animation: loadingRotate 0.75s 0s linear infinite;
}
@keyframes loadingRotate {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
</style>
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
# 优化【要点】
# 优化角度分类
- 编码优化
- 加载性能优化
- 用户体验优化
SEO
优化- 打包优化
- 缓存压缩优化
# 编码优化
- 不要在模板里面写过多表达式;
- 尽量减少
data
中的数据,data
中的数据都会增加getter
和setter
,会收集对应的watcher
vue
在v-for
时给每项元素绑定事件需要用事件代理;SPA
页面采用keep-alive
缓存组件;- 拆分组件(提高复用性、增加代码的可维护性、减少不必要的渲染);
v-if
当值为false
时内部指令不会执行,具有阻断功能,很多情况下使用v-if
替代v-show
;key
确保唯一性(默认vue
采用就地复用策略);- 长列表只显示的话,
Object.freeze
冻结数据; - 合理使用路由懒加载,异步组件;
- 数据持久化的问题(防抖,节流);
- 尽量采用runtime运行时版本;
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
# 加载性能优化
- 第三方模块按需导入(
babel-plugin-component
); - 滚动到可视区域动态加载(
vue-virtual-scroll-list
);始终加载上中下屏; - 图片懒加载(
vue-lazyload
);
# 用户体验优化
- 骨架屏
app-skeleton
; - app壳
app-shell
; - service worker
pwa
;渐进式Web应用,使用多种技术来增强web app的功能,让网页应用呈现和原生应用相似的体验。
# SEO
优化
- 预渲染插件
prerender-spa-plugin
; - 服务器渲染
ssr
;
# 打包优化
- 使用
cdn
的方式加载第三方模块; - 多线程打包
happypack
; splitChunks
抽离公共文件;sourceMap
生成;优化 SourceMap;Tree Shaking/Scope Hoisting
;- 减少 ES6 转为 ES5 的冗余代码;
- Webpack 对图片进行压缩
- 模板预编译
- 提取组件的 CSS
- 构建结果输出分析
- Vue 项目的编译优化
# 缓存压缩优化
- 客户端缓存、服务端缓存;
- 服务端
gzip
压缩; - CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
# Vuex
# 实现的原理
vuex核心就是借用了vue的实例 因为vue的实例数据变化 会刷新视图;
# install注入挂载
// Vue.use 方法会调用插件的install方法,此方法中的参数就是Vue的构造函数
// Vue.use = function (plugin) { // vue中类似use原理实现
// plugin.install(this);
// }
// 插件的安装 Vue.use(Vuex)
export const install = (_Vue) => {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== "production") {
console.error(
"[vuex] already installed. Vue.use(Vuex) should be called only once."
);
}
return;
}
Vue = _Vue; // _Vue 是Vue的构造函数;vue缓存起来再暴露出去
applyMixin(Vue); // 需要将根组件中注入的store 分派给每一个组件 (子组件) Vue.mixin
};
export default function applyMixin(Vue) {
// const version = Number(Vue.version.split('.')[0])
// 父子组件的beforecreate执行顺序
Vue.mixin({
//TODO:了解mixin的原理
beforeCreate: vuexInit, // 内部会把生命周期函数 拍平成一个数组
// destory: "xxx"
});
}
// 组件渲染时从父=》子
// 给所有的组件增加$store 属性 指向我们创建的store实例
function vuexInit() {
const options = this.$options; // 获取用户所有的选项
if (options.store) {
this.$store = options.store; // 根实例;比如:main.js
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store; // 儿子 或者孙子....
}
}
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
# Vuex几个核心模块【要点】
vuex主要由五部分组成:state、 getters、mutations、actions、module组成。
主要包括以下几个模块:(sgmam) + mapG amsg m
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,
mapGetters 辅助函数
仅仅是将 store 中的 getter 映射到局部计算属性
。 - Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
使用流程是: 组件中可以直接调用上面五个部分除了module; Vuex 适用于 父子、隔代、兄弟组件通信
组件不允许直接修改属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,我们最终达成了 Flux (opens new window) 架构。这样约定的好处是,我们能够记录所有 store 中发生的 state 改变,同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。
应用级的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事务; 异步逻辑应该封装在action中。
# action和mutation的区别
vuex的流向:
- view——>commit——>mutations——>state变化——>view变化(同步操作)
- view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)
// 保证通过mutation修改store的数据
_withCommiting(fn) {
/* 调用withCommit修改state的值时会将store的committing值置为true,内部会有断言检查该值,在严格模式下只允许使用mutation来修改store中的值,而不允许直接修改store的数值 */
const committing = this._committing;
this._committing = true;
fn(); // 修改状态的逻辑
this._committing = committing;
}
replaceState(newState) {
this._withCommiting(() => {
this._vm._data.$$state = newState; // 替换掉最新的状态
});
}
2
3
4
5
6
7
8
9
10
11
12
13
# vuex与redux的区别
vuex的流向:
- view——>commit——>mutations——>state变化——>view变化(同步操作)
- view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)
redux的流向: view——>dispatch——>actions——>reducer——>state变化——>view变化(同步异步一样)
不同点:
- 1.vuex以mutations函数取代redux中的reducer,只需在对应的mutation函数里改变state即可。
- 2.vuex支中的state直接关联到组件实例上,当state变化时自动重新渲染,无需订阅重新渲染函数。redux使用store对象存储整个应用的状态,状态变化时,从最顶层向下传递,每一级都会进行状态比较,从而达到更新。
- 3.vuex支持action异步处理,redux中只支持同步处理,对于异步处理需要借助于redux-thunk和redux-saga实现。
# Vue-router
# 懒加载如何实现
vue 路由懒加载有以下三种方式:
- vue 异步组件
- ES6 的 import()
- webpack 的 require.ensure()
非懒加载:
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
2
3
4
5
6
(1)方案一:使用箭头函数+require动态加载;
vue 异步组件 这种方法主要是使用了 resolve 的异步机制,用 require 代替了 import 实现按需加载
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
export default new Router({
routes: [
{
path: '/home',',
component: (resolve) => require(['@/components/home'], resolve),
},
{
path: '/about',',
component: (resolve) => require(['@/components/about'], resolve),
},
],
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(2)方案二(常用):使用箭头函数+import动态加载;
ES6 的 import() vue-router 在官网提供了一种方法,可以理解也是为通过 Promise 的 resolve 机制。因为 Promise 函数返回的 Promise 为 resolve 组件本身,而我们又可以使用 import 来导入组件。
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
export default new Router({
routes: [
{
path: '/home',
component: () => import('@/components/home'),
},
{
path: '/about',
component: () => import('@/components/home'),
},
],
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
webpack 的 require.ensure() 这种模式可以通过参数中的 webpackChunkName 将 js 分开打包。
// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
routes: [
{
path: '/list',
name: 'list',
component: List,
}
]
}))
export default new Router({
routes: [
{
path: '/home',
component: (resolve) => require.ensure([], () => resolve(require('@/components/home')), 'home'),
},
{
path: '/about',
component: (resolve) => require.ensure([], () => resolve(require('@/components/about')), 'about'),
},
],
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# params和query的区别
用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.name
和 this.$route.params.name
。
url地址显示:query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示;
注意:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。
# $route和 $router 的区别
- $route是
路由信息对象
,包括path,params,hash,query,fullPath,matched,name等路由信息参数。 - 而$router是
路由实例
对象包括了路由的跳转方法,钩子函数等。
vue-router 在注册时,为每个 vue 实例注入了router、route 对象。router为router实例信息,利用push和replace方法实现路由跳转,route 提供当前激活的路由信息。
// 方法1:
<router-link :to="{ name: 'users', params: { uname: samy }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:samy}})
// 方法3:
this.$router.push('/user/' + samy)
2
3
4
5
6
# 两种模式对比[要点]
对比 | Hash | History |
---|---|---|
观赏性 | 丑 | 美 |
兼容性 | >ie8 | >ie10 |
实用性 | 直接使用 | 需后端配合 |
命名空间 | 同一document | 同源 |
区别 \ mode | hash | history |
---|---|---|
监听事件 | hashChange | popstate |
缺点 | # 号不好看 | 子路由刷新404、ie9及以下不兼容 |
push操作 | window.location.assign | window.history.pushState |
replace操作 | window.location.replace | window.history.replaceState |
访问操作 | window.history.go | window.history.go |
后退操作 | window.history.go(-1) | window.history.go(-1) |
向前操作 | window.history.go(1) | window.history.go(1) |
# 注意事项
关于 popstate 事件监听路由的局限
history对象的 back(), forward() 和 go() 三个等操作会主动触发 popstate 事件,但是 pushState 和 replaceState 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。
关于子路由刷新的解决方式
history
模式子路由刷新会404,因此需要后端配合,将未匹配到的路由默认指向html
文件;
location / {
try_files $uri $uri/ /index.html;
}
2
3
浏览器(环境)兼容处理
history 模式中pushState
、replaceState
是HTML5
的新特性,在 IE9
下会强行降级使用 hash
模式,非浏览器环境转换成abstract
模式。
router-link
router-link
点击相当于调用$router.push
方法去修改url
;<router-link>
比起写死的 <a href="...">
会好一些,理由如下:
- 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
- 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面。
- 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了。
# Hash
- 监听
hashchange
window.location.hash
,window.location.replace
# History
- 监听
popstate
window.location.pathname
,history.pushState({},null,'/x')
,history.replaceState
- pushState不会触发popstate事件,所以需要手动调用渲染函数;
# hash 模式和 history 模式的区别【要点】
- url 展示上,hash 模式有“#”,history 模式没有
- 刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由。
try_files $uri $uri/ /index.html;
- 兼容性。hash 可以支持低版本浏览器和 IE。
# HTML5 History 模式
vue-router
默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面
。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
2
3
4
当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id
,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id
就会返回 404,这就不好看了。
# 后端配置
nginx 常用; try_files $uri $uri/ /index.html;
你也许注意到 router.push
、 router.replace
和 router.go
跟 window.history.pushState
、 window.history.replaceState
和 window.history.go
类似;
# 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave