这些知识最好的文档就是官方文档
生命周期
所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法
例如 created: () => this.fetchTodos()。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。
-
beforeCreate 在数据观测和初始化事件(event/watcher)还未开始之前
-
created 完成数据观测,属性和方法的运算。但 $el 属性还不可用
-
beforeMount 在挂载前调用,相关的 render 函数第一次调用。实例已完成编译模板,把 data 的数据和模板生成 html。但还未挂载 html 到页面上。
该钩子在服务器端渲染期间不被调用。 -
mounted 实例被挂载后调用,这时 el 被新创建的 vm.nextTick。
该钩子在服务器端渲染期间不被调用。 -
beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。 -
updated 虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
该钩子在服务器端渲染期间不被调用。 -
activated 被 keep-alive 缓存的组件激活时调用。
该钩子在服务器端渲染期间不被调用。 -
deactivated 被 keep-alive 缓存的组件停用时调用。
该钩子在服务器端渲染期间不被调用。 -
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
该钩子在服务器端渲染期间不被调用。 -
destroyed 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
该钩子在服务器端渲染期间不被调用。
总计: data 在 created 可获得。 el 在 mounted 获得, $nextTick beforeCreated 和 created 可在服务端渲染使用,其余都不可以。 每个生命周期都不能用箭头函数
父子组件生命周期
加载渲染过程
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
更新过程
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
销毁过程
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
keep-alive 缓存组件
-
keep-alive 是 vue 内置的一个组件,可以是被包含的组件保留状态,避免重新渲染
-
一般结合路由和动态组件一起使用,用于缓存组件
-
有三个属性。
- include 只有匹配的组件会被缓存。
- exclude 任何名称匹配的都不会被缓存,ex 比 in 优先级高。
- max 缓存组件的最大个数
-
和路由配合使用时,可设置 router 的元信息 meta 来决定要不要缓存
-
对应两个生命周期 activated , deactivated。当组件激活时,触发钩子函数 activated,当组件被移除时,触发 deactivated
keep-alive 缓存的是 Vnode
v-for / v-if 唯一 key
key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。key 不同就会被重新渲染。
Vue 会最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素(即同层元素)必须有独特的 key。重复的 key 会造成渲染错误。(因为 diff算法 同层比较原则)
-
key 会用在虚拟DOM 算法(diff 算法)中,用来辨别新旧节点。
SameVnode 判断节点是否相同,第一步就是先判断 key -
不带 key 的时候会最大限度减少元素的变动,尽可能用相同元素。(就地复用)
-
带 key 的时候,会基于相同的key来进行排列。(相同的复用)
-
带 key 还能触发过渡效果,以及触发组件的生命周期
参考文章 vue中的key
不要用 index 做 key index 为什么不要做 key
在 v-for 里,如果是中间插入数据,会导致期之后的 index 全部变化,都要重新渲染,开销很大。而且可能会发生两个不一样的东西却被误以为是一样的了,因为用了一样的 index。推荐使用唯一 id 做 key
v-show v-if
v-show 的元素始终会被渲染并保留在 DOM 中,只是简单的基于CSS进行切换。而 v-if 是在销毁和重建中的。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
v-if 中也会遇到 key 的问题
cn.vuejs.org/v2/guide/co…
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
在项目遇到这样的情形,很多表单,用v-if v-else 区分不同状态下的显隐。因为不同状态用的模板一样,但状态改变时,输入框前的 label 改变了,但是输入框内的文字依然保留之前的文字。如果加了表达检验的话,这些校验都错乱了。
这就是因为这些表单并没有重新渲染,我们就要加上唯一key,让vue认为他们都是完全独立的。这又可以引出 diff算法了。
data 为什么是函数
一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。如果是对象的话,一个组件被多次引用,就会被多个地方更改数据,这是不合理的。
watch 和 computed 和 methods
重点, computed 当且仅当其依赖的数据改变时,会重新计算。
computed 和 methods 对比 同一函数也可定义为一个方法。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。而方法是每次都要重新计算。 计算属性是基于响应性依赖缓存的,方式没有。 调用时方式必须是函数,计算属性是属性,并可以定义成 get/set变成可读写属性
watch的使用场景是:当在data中的某个数据发生变化时, 我们需要做一些操作, 或者当需要在数据变化时执行异步或开销较大的操作时. 我们就可以使用watch来进行监听。
watch 和 computed的区别是:
相同点:他们两者都是观察页面数据变化的。
不同点:computed 只有当依赖的数据变化时才会计算, 当数据没有变化时, 它会读取缓存数据。 watch 没有缓存,每次都需要执行函数。当需要在数据变化时执行异步或开销较大的操作时,watch 最有用的。
vue 响应式原理,依赖收集
Dep 对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅
Vue 采用数据劫持和发布订阅模式
-
Vue2 在初始化时,首先通过 Object.defineProperty 对 Data 每个属性绑定 get 和 set ,监听所有的 Data 中的数据变化。同时 Observer模块 会创建 Dep 用来搜集使用该 Data 的 Watcher。Dep 对象基于发布订阅模式实现的,用于依赖收集。Watcher 是在 beforeMount生命周期 创建 (并将 Dep.target 标识为当前 Watcher。) Watcher 创建时会执行 render 方法,最终将 Vue 代码渲染成真实的 DOM。
-
编译模板时,如果使用到了 Data 中的数据,就会触发 Data 的 get 方法,然后触发 Dep 的 depend 方法,最终触发 Dep 的 addSub 将当前的 Watcher 对象加入到依赖收集池 Dep 中。
-
数据更新时,会触发 Data 的 set 方法,继而触发 Dep 的 notify方法,notify方法会通知所有使用到该 Data 的 Watcher对象 调用其 update 方法去更新试图,所有使用到这个 Data 的 Watcher 会加入一个队列,并开启一个异步队列进行更新,最终执行 _render 方法完成页面更新。
响应式原理 juejin.cn/post/685766…
3.0 之前是使用 Object,defineProperty 劫持数据,3.0 使用 Proxy
object.defineProperty 缺点
-
不能监听数组;因为数组没有 getter 和 setter,因为数组的长度不确定,太长性能负担很大
-
只能监听属性,而不是整个对象,需要遍历对象。
-
只能监听属性的变化,不能监听属性的删减。也正因如此,vue文档要求,给数组或对象新增属性时,需要用 vm.$set 才能保证新增的属性也是响应式的。
proxy
优点
-
可以监听数组
-
可以监听整个对象,而不是属性
-
13种拦截方法,强大很多
-
返回新对象,而不是直接修改原对象。
Reflect 是内置对象,可以简单化内部操作 一些语言内置的方法 [[get]] [[set]] [[delete]] 不方便调用,用 Reflect 就可以,还是函数返回
缺点
不兼容 IE,且目前无法用 polyfill 磨平
发布订阅模式
class Observer {
constructor() {
this.caches = {}
}
on(event, fn) {
this.caches[event] = this.caches[event] || [];
this.caches[event].push(fn);
}
emit(event, data) {
if (this.caches[event]) {
this.caches[event].forEach(fn => fn(data))
}
}
off(event, fn) {
if (this.caches[event]) {
const newCaches = fn ? this.caches[event].filter(e => e !== fn) : [];
this.caches[event] = newCaches;
}
}
}
组件传值问题
1. 父子组件传值
1 最常用 props 2 子组件用 $parent 获取父组件 3 v-bind 配合 .sync
2. 子父组件传值
1 子组件 children 获取子组件 3 父组件 $refs 获取子组件
3. 祖先组件
1 listeners 情形:A 是 B 父组件,B 是 C 父组件。A 给 C 传值
<C v-bind="$attrs" v-on="$listeners"></C>
利用B组件为中介,B 中调用 C 组件时,使用 v-on 绑定 attrs;
这样 C 组件就可以获取 A组件调用B组件写的属性和事件了
attrs 是属性 C 组件使用: emit('funA')
举例 segmentfault.com/a/119000002…
2 provide 和 inject 父组件通过 provide 提供属性,不论子组件多深,都可调用 inject 获取数据
// 父组件
provide: {
for: 'test';
},
data() {
return ...
}
// 子组件
inject: ['for'],
data() {
return {
msg: this.for
}
}
4. event bus 非父子组件,其实所有组件都可以
使用一个空的 Vue 实例作为中央事件总线,结合 on 使用
Bus 定义方式
1 抽离为独立模块,组件按需引入 2 将Bus挂载到Vue根实例的原型上 3 将Bus注入到Vue根对象上
// bus.js
import Vue from 'vue';
const Bus = new Vue();
export default Bus;
// 2
import Vue from 'vue';
Vue.prototype.$bus = new Vue();
// 3
import Vue from 'vue';
const Bus = new Vue();
new Vue({
el: '#app',
data: {
Bus
}
})
// 组件使用时
this.$Bus.$emit() // 触发事件
this.$Bus.$on() // 监听事件
// 注册的Bus要在组件销毁时卸载,否则会多次挂载,造成一次触发多次响应的问题。
beforeDestory() {
this.$Bus.$off('方法名')
}
5. Vuex 状态管理
Vuex 是 Vue 的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。简单来说就是:应用遇到多个组件共享状态时,使用 vuex。
属性:
- state:vuex的基本数据,用来存储变量
- getter:state 的读取方法,相当于state的计算属性
- mutation:提交更新数据的方法,必须是同步的。它会接受 state 作为第一个参数,提交载荷作为第二个参数。
- action:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
- modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。
vuex 的使用流程:
页面通过 dispatch 派发异步事件到 action。action 通过 commit 把对应参数同步提交到 mutation,mutation 会修改 state 中对应的值。最后通过 getter 把对应值跑出去。
异步代码只有 actions 来处理,不能使用 mutation。换句话说,mutation 必须是同步的,而action 里想干嘛就干嘛。 vuex 用 devtools 追踪状态变化,就是追踪 Mutation,如果 mutation 有异步操作就不能正常追踪了。(异步状态未知)
同步的意义在于每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。
尤雨溪自己说的 www.zhihu.com/question/48…
之前公司实际项目中。一般都是将 接口函数 统一封装起来在一个 api 文件夹下,通过 api字段 暴露出来。然后将会被多个组件公用的请求,写在 action 中。再通过改变 mutation 来改变 state 。组件 dispatch 派发即可。
简单实现:
原文:https://zhuanlan.zhihu.com/p/166087818
class Store {
constructor(options) {
this.vm = new Vue({
data:{
state: options.state
}
})
let getters = options.getter || {}
this.getters = {}
Object.keys(getters).forEach(getterName => {
Object.defineProperty(this.getters,getterName,{
get:()=> {
return getters[getterName](this.state)
}
})
})
let mutations = options.mutations || {}
this.mutations = {}
Object.keys(mutations).forEach(mutationName => {
this.mutations[mutationName] = (arg) => {
mutations[mutationName](this.state,arg)
}
})
let actions = options.actions
this.actions = {}
Object.keys(actions).forEach(actionName => {
this.actions[actionName] = (arg) => {
actions[actionName](this,arg)
}
})
}
dispatch(method,arg){
this.actions[method](arg)
}
// 修改代码
commit = (method,arg) => {
this.mutations[method](arg)
}
get state(){
return this.vm.state
}
}
vuex 与 redux 对比
虚拟 DOM 与 diff 算法
虚拟 DOM
Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点。 就是一个普通的 JavaScript 对象,包含了 tag、props、children 三个属性。
<div id="app">
<p class="text">hello world!!!</p>
</div>
// 虚拟 dom
{ tag: 'div', props: { id: 'app' },
chidren: [{ tag: 'p', props: { className: 'text' },
chidren: ['hello world!!!']}]}
如何生成 虚拟 DOM
如果用纯手写的方式把整个页面的 虚拟 DOM 打出来,那是反人类的设计。
主流的虚拟 DOM 库(snabbdom、virtual-dom),通常都有一个 h 函数,也就是 React 中的 React.createElement,以及 Vue 中的 render 方法中的 createElement。(另外 React 是通过 babel 将 jsx 转换为 h 函数渲染的形式,而 Vue 是使用 vue-loader 将模版转为 h 函数渲染的形式。)
通过这些渲染函数可以把页面模板编译成 虚拟 DOM。
Vue template
vue 的模板语法,是一种形象描述视图的标记语法。被 Vue-template-compiler 解析成 reder 函数,通过 VNode 和 diff 算法统一替换为 DOM 生成页面。
.vue 文件常用到的 template 标签是声明 虚拟DOM 的标签模板,是模板占位符,包裹元素。但是不会被渲染到页面上。
Vue template 到 render 的过程
简单以 vue 为例,介绍下这个过程
过程主要如下 template -> ast -> render 函数
-
调用 parse 方法通过正则表达式将 template 转化为 ast抽象语法树
ast树节点有三种类型,type 1 普通元素;type 2 表达式;type 3 纯文本 -
对静态节点进行优化
分析那些是静态节点,做标记,静态节点的 DOM 永远不会改变,后续更新渲染可直接跳过。 这对更新模板有极大的优化作用 -
生成 render 函数
将 ast 抽象语法树 编译成 render 字符串,最后通过 new Function(redner) 生成 render 函数
有了 render 渲染函数,就可以生成 虚拟DOM。就可以被各平台渲染出页面。
图片出处: 详解Vue中的虚拟DOM
虚拟DOM 用处
-
相当于在 js 和真实 dom 中间加了一层缓存,利用 dom diff 算法避免没有必要的 dom 操作,从而提高性能(JS 线程和 操作DOM 是两个线程,频繁的交互影响性能)。当然算法有时并不是最优解,因为它需要兼容很多实际中可能发生的情况,比如后续会讲到两个节点的 dom 树移动。
-
虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM
参考大佬回答:Vue采用虚拟DOM的目的是什么?
diff 算法
目的是什么?
diff 算法是为了减少 DOM 操作的性能开销,因为浏览器生成新 DOM 会占用很多资源。所以我们要尽可能的复用 DOM 元素。diff 算法就是帮助我们判断出是否有节点需要移动,应该如何移动,找出需要添加或删除的节点
diff 算法是一种通过同层的树节点进行比较,而非对树进行逐层搜索遍历的高效算法,降低时间复杂度为O(n)。
diff 在很多场景下都有用,比如 vue 虚拟DOM 渲染成真实DOM 的新旧 VNode 比较更新。
diff算法的特点
-
只同级比较,不会跨层级比较
-
diff 比较循环两边,并往中间收拢。
因为对节点的操作一般都发生在头和尾。
我们先看下 Vue 中 diff算法的使用吧。
Vue 中的 diff算法
从源码分析
-
必要性,lifecycle.js - mountComponent()
Vue 中每生成一个组件都有一个对应的 Watcher,两者为一一对应的,为了降低 Watcher 的粒度,每个组件只有一个 Watcher。但是问题来了,组件可能存在多个 data 变化,有多个 key 变化。这就需要 diff算法 精确的找到发生改变的节点。
-
执行方式,patch.js - patchVode()
diff算法 执行的地方。执行策略:深度优先,同层比较
在比较同层节点时,有几种情况:
1. 旧节点有孩子节点,新节点无孩子节点。则直接删除旧节点的孩子节点
2. 旧节点无孩子节点,新节点有孩子节点。则直接添加新节点的孩子节点
3. 都有文本节点,则选择新文本节点
4. 双方都有孩子节点,则执行 updateChildren 函数比较孩子节点。这是最重要的,算法核心优化在此
3. 高效性,patch.js - updateChildren() 这是 diff算法 优化的核心。
1. 创建四个指针,分别指向新旧VNode 的头和尾。依次比较。
2. 以上四步都不是相同节点,则在旧VNode 中遍历查找是否有 新VNode 的头指针指向的节点。有的话则把节点移动至旧VNode 头指针前。如果没有则在旧VNode 头指针前插入该节点。
3. 第一个节点操作完成后,指针后移,重复操作。**结果为:新增,删除,移动**
总结:
-
diff算法 是虚拟DOM 技术的必然产物,通过新旧虚拟DOM 的对比,将变化的地方更新在真实DOM 上。也需要 diff算法高效的对比过程,降低时间复杂度为O(n)
-
vue2 中为了降低 Watcher 的粒度,每个组件只对应一个 Watcher,只有引入 diff算法 才能精确的找到发生变化的地方。
-
vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会对比上一次渲染结果 oldVnode 和新的渲染结果 newVnode ,此过程称为 patch。
-
diff 策略是:深度优先,同层比较。两个节点之间会根据是否拥有子节点或者文本节点做不同操作(上面有介绍)。比较两组子节点是重点(上面有介绍)。patch 过程更高效了。