一、常用API
1. event
经典的发布-订阅模式
1. vm.$on(event, fn)
订阅者订阅事件,按事件的名event把回调函数存储到事件中心vm._events;
2. vm.$emit(event)
发布者发布事件,根据事件名event找到所有的回调函数vm._events[event],然后遍历执行所有的回调函数;
3. vm.$off(event, fn)
移除指定事件名event和指定的fn;
3. vm.$once(event, fn)
内部就是执行vm.off移除事件的回调,这样就确保了回调函数只执行一次;
2. v-model
1. 表单元素
<input
v-bind:value="message"
v-on:input="message=$event.target.value">
动态绑定了input的value指向message变量,并且在触发input事件的时候去动态把message设置为目标值
2. 组件
let Child = {
template: '<div>'
+ '<input :value="msg" @input="updateValue" placeholder="edit me">' +
'</div>',
props: ['msg'],
model: {
prop: 'msg',
event: 'change'
},
methods: {
updateValue(e) {
this.$emit('change', 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
}
})
子组件传递的value绑定到父组件的message,同时监听自定义input事件,当子组件派发input事件的时候,父组件会在事件回调函数中修改message值,同时value也会发生变化,子组件的input值被更新。
3. slot
1. 普通插槽
普通插槽是父组件编译和渲染阶段生成vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的vnodes。
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
}
})
作用域插槽,父组件在编译和渲染阶段并不会直接生成vnodes,而是在父节点vnode的data中保留一scopedSlots对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段,才会指向这个渲染函数生成vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。
4. keep-alive
- 包裹的组件首次渲染,除了在keep-alive中建立缓存,和普通组件渲染没什么区别;
- 包裹的组件在有缓存的时候,就不会再执行组件的created、mounted等钩子函数,提供了activated、deacitvated钩子函数。
5. transition
vue的过渡实现分为以下几个步骤:
- 自动嗅探目标元素是否应用了css过渡动画,如果是,在恰当的时机添加或删除css类名
- 如果过渡组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用
- 如果没有找到JavaScript钩子函数并且没有检测到css过渡动画,DOM操作(插入/删除)在下一帧中立即执行
总结:
所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的 <transition> 只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。
二、数据驱动
new Vue --> init --> $mount --> compile --> render --> vnode --> patch --> DOM
1. new Vue发生了什么
Vue初始化:
合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化data props computed watcher。
2. Vue实例挂载实现
原型方法$mount调用mountComponent实现。mountComponent核心是先实例化一个渲染Watcher, 在它的回调函数中调用updateComponent方法,在此方法中调用vm._render方法先生成虚拟Node, 最终调用vm._update更新DOM。
vm._render最终是通过执行createElement方法并返回vnode。
vm._update核心就是调用vm.__path__方法。
三、组件化
1. 渲染组件createComponent
三步关键逻辑:构造子类构造函数,安装组件钩子函数和实例化vnode。
1. 构造子类构造函数
Vue.extend的作用就是构造一个Vue的子类,使用了一个经典的原型继承的方式把一个纯对象转换一个继承于Vue的构造器Sub并返回,然后对Sub这个对象本身扩展了一些属性,;并且对配置中的props和computed做了初始化工作;最后对于这个Sub构造函数做了缓存,避免多次执行Vue.extend的时候对同一个子组件重复构造。
2. 安装组件钩子函数
installComponentHooks整个过程就是把componentVNodeHooks的钩子函数合并到data.hook中
3. 实例化Vnode
通过new VNode实例化一个vnode并返回
三、生命周期
1. new Vue
合并配置,初始化事件中心,初始化生命周期。
2. beforeCreate & created
钩子函数的调用在initState的前后,initState的作用是初始化props data methods computed等属性。beforeCreate钩子函数初始化vue-router, vuex等插件的配置。
3. beforeMount & mounted
在执行vm._render()函数渲染VNode之前,执行了beforeMount钩子函数,在执行玩vm._update()把VNode patch到真实DOM后,执行mounted钩子函数。对于同步渲染的子组件,mounted钩子函数的执行顺序是先子后父。
4. beforeUpdate & updated
beforeUpdate的执行时机是在渲染Watcher的before函数中。updated的执行在Wathcer的update函数中。
5. beforeDestroy & destroyed
beforeDestroy钩子函数的执行时机是在children中删掉自身,删除watcher,当前渲染的VNode执行销毁钩子函数等,执行完毕后再调用destroy钩子函数。destroy钩子函数的执行顺序是先子后父。
6. activated & deactivated
actived钩子函数的执行时机是在activateChildComponent方法执行时;deactivated钩子函数的执行时机是在deactivateChildComponent方法执行时。
7. 父子组件生命周期执行顺序
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
->父beforeUpdate->子beforeUpdate->子updated->父updated
->父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
三、Vue-Router和Vuex
1. Vue-Router
执行install方法,安装插件
- 利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中
- 执行this._router.init()方法初始化router, 用defineReactive方法把this._router变成响应式对象
- Vue原型上定义route属性的get方法
- Vue.component定义全局组件和
- 定义路由中的钩子函数的合并策略
2. Vuex
安装
执行install方法,安装插件。利用Vue.mixin全局混入一个beforeCreate钩子函数,往所有组件实例上添加一个$store的实例。
store实例化
把store作为一个数据仓库,为了方便管理仓库,把一个大的store拆成一些modules, 整个modules是一个树型结构。每个module又分别定义了state getters mutations actions, 通过递归遍历模块的方式完成了它们的初始化。为了module具有更高的封装度和复用性,还定义了namespace。定义了一个内部的Vue实例,用来建立state到getters的联系,并且可以在严格模式下检测state的变化是不是来自外部,确保改变state的唯一途径是显示地提交mutation。
四、响应式原理
(1) 三个重要对象:Observer,Watcher,Dep:
- Observer对象:vue中的数据对象在初始化过程中转换为Observer对象。
- Watcher对象:将模板和Observer对象结合在一起生成Watcher实例,Watcher是订阅中的订阅者。
- Dep对象:Watcher对象和Observer对象之间纽带,每一个Observer都有一个Dep实例,用来存储订阅者Watcher。
(2) 响应式实现:
- 1、在initState方法中将data,prop,method,computed,watch中的数据劫持, 通过observe方法与Object.defineProperty方法将相关对象转为换Observer对象。
- 2、然后在initRender方法中解析模板,通过Watcher对象、Dep对象实现的观察者模式将模板中的 指令与数据对象建立依赖关系,使用全局对象Dep.depend实现依赖收集。
-
3、当数据变化时,setter被调用,触发dep.notify方法, 遍历该数据依赖列表subs,然后调用每一个
watcher的update方法进行视图更新。
五、模板编译
(1) 解析模板 parse
把
template模板字符串转换成 AST 树
-
从 options 中获取方法和配置
-
利用正则表达式顺序解析模板
(2) 优化AST树 optimize
- 深度遍历这个AST树,标记静态节点和标记静态根
- 优化模板的更新
(3) 生成代码 codegen
- 把优化后的 AST 树转换成可执行的代码
六、nextTick
(1) 事件循环
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。
-
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
-
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
-
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
-
主线程不断重复上面的第三步。
(2) Vue nextTick
- 对于 macro task 的实现,优先检测是否支持原生
setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的MessageChannel,如果也不支持的话就会降级为setTimeout 0; - 而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
- nextTick把传入的回调函数
cb压入callbacks数组,最后一次性地根据useMacroTask条件执行macroTimerFunc或者是microTimerFunc,而它们都会在下一个 tick 执行flushCallbacks,flushCallbacks的逻辑非常简单,对callbacks遍历,然后执行相应的回调函数。
七、keep-alive是啥?能干嘛?
基本用法
三个「管理规则」属性:
- include:只缓存名字匹配的组件(支持字符串/正则/数组)
- exclude:不缓存名字匹配的组件(规则同上)
- max:最多缓存多少个组件实例(超了就删最早的)
<!-- 只缓存a、b组件 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则匹配 -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
使用原则就一条:「需要「保留状态」或「避免重复加载」的页面」。
{
path:'/list',
name:'itemList',
component:() => import('@/pages/item/list'),
meta:{
keepAlive:true, // 需要缓存
title:'商品列表'
}
}
<div id="app"class="wrapper">
<!-- 需要缓存的组件 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
原理揭秘
keep-alive本质是个「无模板」的Vue内置组件,核心靠render函数和两个「缓存池」工 作:
- cache对象:存组件实例(键是组件唯一key,值是组件vnode)
- keys数组:记录缓存顺序(控制最大缓存数时用)
当组件被包裹时,它会:
- 1.检查组件名是否符合include/exclude规则,不符合就不缓存
- 2.生成组件唯一key(优先用vnode.key,否则用cid+tag)
- 3.查cache有没有这个key:
- 有→直接取缓存实例,把key移到keys末尾(表示「最近使用」)
- 没有→把组件存进cache,key推进keys
- 如果超了max→删掉keys里第一个(最久未使用的)
当include/exclude变化时,它还会「清理缓存」——遍历cache,把不符合新规则的组件销 毁。
缓存后数据怎么更新?
组件被缓存后,created、mounted这些钩子只会走一次,后续进入页面数据可能不会更新。咋办?
方案1:用beforeRouteEnter 每次路由进入时触发,适合需要「路由级更新」的场景:
beforeRouteEnter(to,from,next){
next(vm =>{
vm.getData()// 每次进页面都调接口
})
}
方案2:用activated钩子 缓存组件被激活时触发(比beforeRouteEnter更「组件级」):
activated(){
this.getData()// 激活时更新数据
}
注意:服务端渲染时activated不会被调用哦~