MVVM
model和view层通过中间的vm连接和驱动。model层数据变化会改变视图,view改变通过事件来修改数据。vue参考了MVVM实现了双向绑定,react是MVC,但是vue仍然可以通过parent等操作dom所以不全是mvvm
vue模板解析
1、先将代码转换为AST树
根据正则匹配,从第一个字符开始,筛选过的就删掉继续index++向后匹配。
如果匹配开始标签就放入一个stack中,此时如果匹配到结束标签则出栈对比是否一致,不一致报错
2、优化AST树
找出静态节点并标记,之后就不需要diff了
递归遍历ast树中的节点,如果没有表达式、v-if、v-for等,就标记static为true
3、生成render函数、在使用new Function(with() {})包裹
转换成render函数。编译结束。一定要包裹new Function和with来更改上下文环境
<div id="app"><p>hello {{name}}</p> hello</div> ==>
new Function(with(this) {_c("div",{id:app},_c("p",undefined,_v('hello' + _s(name) )),_v('hello'))})
v-if解析出来就是三元表达式,v-for解析出来_l((3),..)
4、render函数执行后得到的是虚拟dom
ast是需要吧代码使用正则匹配生成的,然后转换成render,而虚拟dom则是通过render函数直接生成一个对象
初始化data中的proxy
将所有的数据全部代理到this上
for (let key in data) {
proxy(vm, '_data', key);
}
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key]
},
set(newValue){
vm[source][key] = newValue;
}
})
}
vue的双向数据绑定、响应式原理
监听器 Observer ,用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者
订阅器 Dep,用来收集订阅者,对监听器 Observer和订阅者 Watcher进行统一管理,每一个属性数据都有一个dep记录保存订阅他的watcher。
订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图,每个watcher上都会保存对应的dep
解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化
数据劫持
利用observe方法递归的去劫持,对外也可以使用这个api。使用defineReactive来劫持数据
class Observe{
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 判断是否为数组,如果是数组则修改__proto__原型。会再原来原型和实例中间增加一层。
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
//遍历数组,继续调用observe方法。因为数组中有可能有二维数组或者对象
this.observeArray(value)
} else {
// 如果是对象则直接绑定响应式
this.walk(value)
}
}
}
对象的劫持:不断的递归,劫持到每一个属性。在defineReactive中会继续递归执行let childOb = !shallow && observe(val)方法递归绑定,因为对象中有可能还有对象
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//直接遍历对象去递归拦截get、set
defineReactive(obj, keys[i])
}
}
数组的劫持:不劫持下标,value.proto = arrayMethods,增加一层原型链重写数组的push、splice等方法来劫持新增的数据。在数组方法中进行派发更新ob.dep.notify()
// 继续遍历数组,再次执行observe来递归绑定值。
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
响应式原理数据劫持,首先执行observe方法new 一个Observe类,其中会判断是数组还是对象。
1、如果数据是[1,[2,3],{a:1}],不会去劫持下标。会修改数组的proto修改原型的方法。但是其中的[2,3],{a:1}并没有被监控,所以继续调用observeArray递归调用,其中又递归调用了let childOb = !shallow && observe(val)继续监控
2、如果数据是{a:{b:2},c:3}, 会执行walk去遍历对象执行defineReactive拦截key的get、set。其中会去递归调用observe方法继续递归劫持
依赖收集
渲染watcher的收集:
首次渲染:执行完劫持之后,会走挂载流程会new一个渲染watcher,watcher中会立即执行回调render方法,方法中会去创建Vnode需要去数据中取值,就会进入到属性的get方法。会去收集依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 如果有current watcher,会去收集依赖。Dep.target全局只有一个。一个时刻只能更新一个watcher。每次在执行watcher时会先pushStack,等执行完后会去popstack
if (Dep.target) {
// 收集属性的依赖,每个属性获取后都会有个dep。这个dep挂在每个属性上。
// 例如直接this.a = xxx修改属性就可以找到这个属性上的dep更新watcher
dep.depend()
// 如果儿子也是对象或者数组会去递归让儿子也收集
if (childOb) {
// 这个dep挂在对象或者数组上。为了给$set或者数组派发更新使用。在 {b:1} 、[1,2,3]上挂dep
// 例如新增属性,this.$set(obj, b, xxx)或this.arr.push(2)。
// a:{b:[1,2,3]}、a:{b:{c:1}},先收集a的依赖挂在属性dep上,因为childOb又为object,需要继续收集依赖挂在该对象上
// 此时如果更新a,则直接找到属性上的dep更新。但是a上如果想新增一个c属性,则需要使用$set。或者数组上push一个。
// 此时是找不到属性上的dep的,因为该属性是新增的,数组增加一项需要更新watcher。所以需要在对象或者数组的Ob类上挂一个dep方便更新
childOb.dep.depend()
// 如果仍然是数组需要持续递归收集
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
dep中收集watcher
// dep.js
depend () {
// Dep.target 是此刻唯一准备被收集的watcher
if (Dep.target) {
Dep.target.addDep(this)
}
}
// watcher.js
addDep (dep: Dep) {
const id = dep.id
// 去重,如果添加过就不需要添加了
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// dep中保存此watcher
dep.addSub(this)
}
}
}
派发更新
在set中派发更新,数组是在劫持的方法中派发更新。会执行当前所有dep中的watcher的notify方法更新视图或者数据。
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// this.a如果直接改了引用,需要重新递归劫持属性,例如a:{b:1} this.a = {c:2}
childOb = !shallow && observe(newVal)
// 执行派发更新操作
dep.notify()
}
})
dep中派发更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// 遍历执行所有watcher的update方法
subs[i].update()
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
// 如果是computed, 会去执行重新取值操作
this.dirty = true
} else if (this.sync) {
// 如果是同步watcher直接run()会去执行watcher的get()
this.run()
} else {
// 默认watcher都是放入队列中异步执行的
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
// .......调用全局的nextTick方法来异步执行队列
nextTick(flushSchedulerQueue)
}
watcher都是会异步更新,调用nexttick去更新,为了整合多次操作为一次。提高效率
watch
watch内部会调用$watch创建一个user watcher(设置user为true),等依赖变化执行update方法
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// $watcher最终也会去new 一个watcher,传入vm实例和检测的key(expOrFn)和watcher的回调(cb)和watcher的设置(deep等设置)
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 如果设置是immediate,则需要立即执行一次cb
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
// watcher.js中会去判断expOrFn 如果是function说明是渲染watcher传入的回调,
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 如果传入的是字符串,则将取值函数赋给getter(调用一次就是取值一次),例如watcher中监控的是'a.b.c'则需要从this中一直取到c
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
依赖收集:会去执行watcher的getter方法,其实就是去取值,此时获取的值保存起来。执行取值函数会走属性的get进行依赖收集。
watch初始化时会去取值,为了保存下一次变化时的oldvalue
this.value = this.lazy
? undefined
: this.get()
get () {
pushTarget(this)
let value
const vm = this.vm
try {
//如果是user watcher的话,执行的就是取值函数其实就是依赖收集过程
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 如果设置了deep则需要遍历获取子属性进行全部的依赖收集(把子属性都取值一遍)
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
派发更新:更新时会执行watcher的update方法,其中如果设置同步则直接run,如果没有默认放入队列异步更新
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
// run就是重新调用get执行getter,去重新取值,取出来的就是新值
const value = this.get()
// 如果是新老值不相同才需要调用user watcher的回调
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 取老的值并设置新值
const oldValue = this.value
this.value = value
// 调用user watcher的回调
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
computed
computed会设置lazy为true。并且会执行脏检查,只有当这些依赖变化时才会去重新计算computed的值,获取完之后再设置dirty为false
// 设置lazy为true表示是computed
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// 如果用户传入对象表示自己定义了get函数则使用用户的,没有则直接设置getter
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// 创建一个computed watcher, 初始化时其中不会执行get函数获取值。
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 定义computed,需要去劫持计算属性的值进行依赖收集。
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
// 定义computed
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 其实就是重新劫持computed的值, sharedPropertyDefinition中有定义的get函数
Object.defineProperty(target, key, sharedPropertyDefinition)
}
收集依赖: 创建computed时,需要对computed的变量也进行劫持,如果页面中使用到了这个计算属性,则会走下面的createComputedGetter 创建的get方法。之后会去收集依赖。
// 创建劫持computed的get函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果dirty为true才取值,创建时默认第一次是true,会去执行get方法
if (watcher.dirty) {
watcher.evaluate()
}
// 如果有target则去收集依赖。firstName和lastName收集渲染依赖, 计算属性上不需要收集渲染watcher,因为如果页面中使用到了这个计算属性,计算属性是根据函数中依赖变化计算的,所以其中任何一个依赖都需要收集一下渲染watcher,因为任何一个变化都有可能导致重新渲染
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// 取值
evaluate () {
// 执行get方法会去取值,例如:return this.firstName + this.lastName,此时也是对依赖firstName和lastName的取值收集依赖的过程,那么他们也会将当前的computed watcher添加到dep的sub队列中。取值完置换成false
this.value = this.get()
this.dirty = false
}
所以如果计算属性中写了data中其他的值也会使他进行收集依赖,浪费性能
let vm = new Vue({
el:'#app',
data: {
firstName: 'super',
lastName: 'kimi',
kimi: 888
},
computed: {
fullName() {
// 最后返回没有kimi但是打印进行取值了,他就会收集computed和渲染watcher
console.log(this.kimi)
return `${this.firstName}-${this.lastName}`
}
}
})
// 如果更新了kimi也会让视图重新渲染
vm.kimi = 999
派发更新:如果此时改变了firstName的值,因为firstName之前收集依赖中有依赖他的computed watcher和渲染watcher,会去执行两个watcher上的update方法
update () {
// 如果是计算属性则设置dirty为true即可,之后再去执行渲染watcher的update会重新渲染,那就会重新取计算属性的值,到时候就可以取到最新的值了
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
provide、inject
provide是定义在当前实例上,inject会去遍历$parent找到谁定义了,然后再转成响应式挂在当前实例,只是单向
nextTick
优雅降级,先使用promise,如果不支持会使用MutationObserver,不兼容再使用setImmediate,最后降级成setTimeout
slot
普通插槽和作用域插槽的实现。它们有一个很大的差别是数据作用域,普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes。而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。
Vue.extend
传入一个vue组件配置,然后创建一个构造函数,然后进行合并配置,修改指针等操作。生成一个vue的构造函数,之后进行new操作就可以生成一个vue组件实例,然后进行vm.$mount可以动态挂载
Vue.$set
1、对象会重新递归添加响应式,数组则会调用splice方法,方法已经被劫持
2、执行ob.dep.notify(),让视图更新
Vue组件化
全局组件:Vue.component内部会调用Vue.extend方法,将定义挂载到Vue.options.components上。这也说明所有的全局组件最终都会挂载到这个变量上
局部组件:在调用render时,也会去调用Vue.extend方法,在真正patch时会去new
data.hook = {
init(vnode){
let child = vnode.componentInstance = new Ctor({});
child.$mount(); // 组件的挂载
}
}
虚拟DOM
用js对象来表示dom节点。配合diff算法可以提高渲染的效率。
和ast的区别:ast是转换语法(js、html语法转换为ast)两者很相像
生命周期
组件的渲染生命周期都是先子后父。beforeCreate中拿不到this。create中可以拿到data,但是没挂载拿不到$el.
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
diff算法
1、首先比对标签 <div>...</div> --> <ul></ul>
在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
// 如果标签不一致说明是两个不同元素
if(oldVnode.tag !== vnode.tag){
oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
}
如果标签一致,有可能都是文本节点,那就比较文本的内容即可
// 如果标签一致但是不存在则是文本节点
if(!oldVnode.tag){
if(oldVnode.text !== vnode.text){
oldVnode.el.textContent = vnode.text;
}
}
2、对比属性<div>...</div> --> <div className=‘aaa’>...</div>
当标签相同时,我们可以复用老的标签元素,并且进行属性的比对。只需要把新的属性赋值到老的标签上即可
3、对比子元素<div><p>a</p></div> -> <div><p>b</p></div>
[1]新老都有孩子需要updateChildren比对
[2]新有老没有则需要遍历插入
[3]新没有老有则需要删除即可
// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
updateChildren(el, oldChildren, newChildren)
// 老的有儿子新的没有清空即可
}else if(oldChildren.length > 0 ){
el.innerHTML = '';
// 新的有儿子
}else if(newChildren.length > 0){
for(let i = 0 ; i < newChildren.length ;i++){
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
4、updateChildren 核心
设置四个index:oldS、oldE、newS、newE
<1>先比对oldS和newS,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldS和newS都++,节点复用即可 例如:ABCD -> ABCE
// 优化向后追加逻辑
if(isSameVnode(oldStartVnode,newStartVnode)){
patch(oldStartVnode,newStartVnode); // 递归比较儿子
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
<2>oldS和newS如果不相等再比对oldE和newE,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldE和newE都--,节点复用即可 例如:ABCD -> EBCD
// 优化向前追加逻辑
else if(isSameVnode(oldEndVnode,newEndVnode)){
patch(oldEndVnode,newEndVnode); // 递归比较孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
<3>oldE和newE如果不相等再比对oldS和newE,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldS++和newE--,将old节点插入到最后 例如:ABCD -> BCDA
// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode,newEndVnode); // 递归处理儿子
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
}
<4>oldS和newE如果不相等再比对oldE和newS,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldE--和newS++,将old节点插入到最前 例如:ABCD -> DABC
// 尾部移动到头部
else if(isSameVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode,newStartVnode);
parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
}
<5>如果使用index都判断节点不相同,则需要建立vnode的key-index map表,然后匹配map表,如果能匹配上挪到当前oldS前面,如果匹配不上则创建新节点往当前oldS前面插入,newS++ 例如:ABCD -> CDME
// 建立key-index的map表
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
// 在map表中寻找有没有key匹配的vnode
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
图中第一步比对index都不同,则开始比对key发现有C相同则把C挪到最前面,newS++;下来发现D有相同的把D挪到oldS前面,newS++;接着M找不到则插入oldS前面,newS++;最后E找不到则插入前面,newS++;
<6>全部比对完后需要对当前index进行检查,因为有可能有多或者少节点的情况
if (oldStartIdx > oldEndIdx) {
// oldNode先扫完说明new有多余,需要添加进去
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// newNode先扫完说明old有多余,需要删除掉
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
diff对开头、结尾插入删除节点&头节点移到尾部&尾节点移到头部有很大的优化
key:为了高效复用
A B C D E
A B F C D E
如果没有key:首先比对头和头,A、B都复用,
比到C和F时
,tag一样key相同(都为undefined)则会复用,会成下图情况如果有key:比对到C和F时
,C和F的key不相同所以跳过,此时就该比oldE和newE,EDC都相同,多下来的F直接插入
如果key使用index,遇到表单元素比如带checkbox的列表,如果状态勾选后,会复用勾选状态产生bug
keep-alive组件
会将组件缓存到this.cache中,放入内存中缓存起来。
vue-router
1、install方法注册全局组件,挂载router
// 递归给每个子组件实例上都挂载一个_routerRoot 、_router属性,以便于每个组件实例上都可以取到路由实例
export default function install(Vue) {
_Vue = Vue;
Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法
beforeCreate() {
if (this.$options.router) { // 如果有router属性说明是根实例
this._routerRoot = this; // 将根实例挂载在_routerRoot属性上
this._router = this.$options.router; // 将当前router实例挂载在_router上
this._router.init(this); // 初始化路由,这里的this指向的是根实例
} else { // 父组件渲染后会渲染子组件
this._routerRoot = this.$parent && this.$parent._routerRoot;
// 保证所有子组件都拥有_routerRoot 属性,指向根实例
// 保证所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象
}
}
})
}
// 做一层代理,方便用户$route和$router取值
Object.defineProperty(Vue.prototype,'$route',{ // 每个实例都可以获取到$route属性
get(){
return this._routerRoot._route;
}
});
Object.defineProperty(Vue.prototype,'$router',{ // 每个实例都可以获取router实例
get(){
return this._routerRoot._router;
}
})
2、路由先生成map表
addRouter方法其实就是给路由表中插入对应的值即可。
export default function createMatcher(routes) {
// 收集所有的路由路径, 收集路径的对应渲染关系
// pathList = ['/','/about','/about/a','/about/b']
// pathMap = {'/':'/的记录','/about':'/about记录'...}
let {pathList,pathMap} = createRouteMap(routes);
// 这个方法就是动态加载路由的方法
function addRoutes(routes){
// 将新增的路由追加到pathList和pathMap中
createRouteMap(routes,pathList,pathMap);
}
function match(){} // 稍后根据路径找到对应的记录
return {
addRoutes,
match
}
}
3、三种模式,如果是hash监听onHashChange事件,hash变化会赋值给this.current,并且利用defineReactive方法定义响应式对象_route。
window.addEventListener('hashchange', ()=> {
// 根据当前hash值 过度到对应路径
this.transitionTo(getHash());
})
// 核心逻辑
transitionTo(location, onComplete) {
// 去匹配路径
let route = this.router.match(location);
// 相同路径不必过渡
if(
location === route.path &&
route.matched.length === this.current.matched.length){
return
}
this.updateRoute(route); // 更新路由即可
onComplete && onComplete();
}
updateRoute(route){ // 跟新current属性
this.current =route;
}
//使用vue的方法defineReactive将_route变为响应式并设置值为this.current
Vue.util.defineReactive(this,'_route',this._router.history.current);
4、router-view拿到$route去使用render函数渲染其中的组件。(如果/about/a会先渲染about再渲染a)
export default {
functional:true,
render(h,{parent,data}){
// 拿到$route其实就是拿到了_route,其实也是设置的this.current,此时取值也就相当于收集依赖。收集到渲染watcher
let route = parent.$route;
let depth = 0;
data.routerView = true;
while(parent){ // 根据matched 渲染对应的router-view
if (parent.$vnode && parent.$vnode.data.routerView){
depth++;
}
parent = parent.$parent;
}
let record = route.matched[depth];
if(!record){
return h();
}
// 读取路由表中配置的component(此时已经转换成render函数了),执行render
return h(record.component, data);
}
}
渲染过程:页面开始渲染后会去取
$route
,会去找内部_route,之前此属性已经变为响应式,所以会进行收集依赖操作,添加渲染watcher。
当hash改变时,会修改_route属性,此时进行派发更新,执行渲染watcher update重新渲染,router-view组件会去重新获取$route属性渲染。
路由钩子
①导航被触发。
②在失活的组件里调用 beforeRouteLeave 守卫。
③调用全局的 beforeEach 守卫。
④在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
⑤在路由配置里调用 beforeEnter。
⑥解析异步路由组件。
⑦在被激活的组件里调用 beforeRouteEnter。
⑧调用全局的 beforeResolve 守卫 (2.5+)。
⑨导航被确认。
⑩调用全局的 afterEach 钩子。
⑪触发 DOM 更新。
⑫调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
Vuex
1、创建一个Store类,再导出一个install方法,同样是利用mixin在beforeCreate钩子中递归注入$store对象
export const install = (_Vue) =>{
_Vue.mixin({
beforeCreate() {
const options = this.$options;
if (options.store) {
// 给根实例增加$store属性
this.$store = options.store;
} else if (options.parent && options.parent.$store) {
// 给组件增加$store属性
this.$store = options.parent.$store;
}
}
})
}
2、实现state和getter。都是利用vue中的data和computed来实现。这样可以为每一个store中的数据绑定响应式。并做一层代理,如果用户调用this.store.getter会去返回创建的vue实例上的属性
// state
export class Store {
constructor(options){
let state = options.state;
this._vm = new Vue({
data:{
$$state:state,
}
});
}
get state(){
return this._vm._data.$$state
}
}
// getter
this.getters = {};
const computed = {}
forEachValue(options.getters, (fn, key) => {
computed[key] = () => {
return fn(this.state);
}
Object.defineProperty(this.getters,key,{
get:()=> this._vm[key]
})
});
this._vm = new Vue({
data: {
$$state: state,
},
computed // 利用计算属性实现缓存
});
3、添加mutation和action。其实就是存储一个对象,利用发布订阅来保存回调函数数组。
export class Store {
constructor(options) {
this.mutations = {};
forEachValue(options.mutations, (fn, key) => {
this.mutations[key] = (payload) => fn.call(this, this.state, payload)
});
}
commit = (type, payload) => {
this.mutations[type](payload);
}
}
export class Store {
constructor(options) {
this.actions = {};
forEachValue(options.actions, (fn, key) => {
this.actions[key] = (payload) => fn.call(this, this,payload);
});
}
dispatch = (type, payload) => {
this.actions[type](payload);
}
}
整体流程:vuex ->install方法中会去遍历绑定$store。所以组件都可以取到-> 格式化用户配置成一个树形结构。->安装模块,递归把模块mutation、action、getter、state都挂在store上,mutation、action都是数组(子模块和父模块重名会push都执行),getter是对象(子模块和父模块重名会覆盖)。state也是对象->会new 一个vue将state放到data上、将getter放到computed上利用vue的原理来实现响应式和计算缓存。
4、namespace模块, 其实就是给安装的模块增加了path
1、如果不写namespace是没有作用域的,调用根、子模块的同名mutations都会执行修改。
2、状态不能和模块重名,默认会使用模块, a模块namespace:true state中也有a
3、默认会找当前模块的namespace,再向上找父亲的。比如父亲b有namespace儿子c没有,会给儿子也加 使用方式:b/c/,子c有父b没有。则调用时不需要加父亲b。调用:c/xxx
假设根模块下中有a模块并且都有命名空间
mutation、action如果在子模块和父模块中都有,会都挂到store中的——mutation、action对象中,其中增加命名空间。例如:store.action = {'setA':[()=>{}] ,'a/b/setA':[()=>{}]}
,如果namespace没写的话就都在一个数组中,不会覆盖
使用就$store.mutation('a/b/setA')
getter如果在子模块和父模块中都有的话,会都挂载到store的_getter对象中,增加命名空间,但是不是数组,重名会覆盖,例如:{'getA':()=>{},'a/b/getA':()=>{}}
,如果namespace没写的话就都在一个对象中会覆盖
使用就$store.getter('a/b/getA')
state会递归放入store中,变成一个对象,例如 {name:'kimi', a:{name:'bob'}}
代表根节点中的state name是kimi,模块a中是bob。所以使用的时候$store.state.a.name
vuex 插件
插件会使用发布订阅。在每次数据mutation更新的时候去发布。然后提供replaceState方法来替换state,可以写一个持久化插件,存到localStorge中,刷新后再从localStorge中取使用replaceState方法替换
function persists(store) { // 每次去服务器上拉去最新的 session、local
let local = localStorage.getItem('VUEX:state');
if (local) {
store.replaceState(JSON.parse(local)); // 会用local替换掉所有的状态
}
store.subscribe((mutation, state) => {
// 这里需要做一个节流 throttle lodash
localStorage.setItem('VUEX:state', JSON.stringify(state));
});
}
plugins: [
persists
]
内部原理实现:
// 执行插件
options.plugins.forEach(plugin => plugin(this));
subscribe(fn){
this._subscribers.push(fn);
}
replaceState(state){
this._vm._data.$$state = state;
}
registered
也提供动态注册模块功能,就是重新走 -> 格式化树形数据 -> 安装模块到store上 -> 重新new vue实例,此时会销毁之前的vue实例
strict模式
如果开启strict模式,mutation中只能放同步代码,不能放异步。并且不能直接修改state只能通过commit修改state。
更改属性时包裹一层切片,先置换状态_commite修改完再改回
也就是说只要是正常操作(不是通过state修改的)都会将_committing改为true
this._committing = false;
_withCommitting(fn) {
let committing = this._committing;
this._committing = true; // 在函数调用前 表示_committing为true
fn();
this._committing = committing;
}
此时修改mutation的值是需要包裹一层_withCommitting
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload); // 这里更改状态
})
严格模式会去利用vue的$watch方法去监控state,并且设置deep,sync为true,sync代表同步触发,如果data变了会立即执行回调不会放入queue中nextTick执行。这样就可以监控state变化,如果其中之前的_commite为false说明没有经过commit或者异步更新(fn是异步执行,则此时的_committing已经重置回false了)。就可以抛错
if (store.strict) {
// 只要状态一变化会立即执行,在状态变化后同步执行
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, '在mutation之外更改了状态')
}, { deep: true, sync: true });
}
内部正常的操作(不是通过state直接修改的)都需要包装一层_withCommitting
replaceState(newState) { // 用最新的状态替换掉
this._withCommitting(() => {
this._vm._data.$$state = newState;
})
}
store._withCommitting(() => {
Vue.set(parent, path[path.length - 1], module.state);
})