vue

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来更改上下文环境

hello {{name}}
hello==> 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,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化
vue
文章图片
image.png 数据劫持 利用observe方法递归的去劫持,对外也可以使用这个api。使用defineReactive来劫持数据
class Observe{ constructor (value: any) { this.value = https://www.it610.com/article/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) { 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 = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/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、首先比对标签 ... -->
    在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、对比属性... --> ... 当标签相同时,我们可以复用老的标签元素,并且进行属性的比对。只需要把新的属性赋值到老的标签上即可
    3、对比子元素a
    -> b
    [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]; }

    vue
    文章图片
    image.png <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]; }

    vue
    文章图片
    image.png <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] }

    vue
    文章图片
    image.png <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] }

    vue
    文章图片
    image.png <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++;

    vue
    文章图片
    image.png <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:为了高效复用
    vue
    文章图片
    image.png
    A B C D E
    A B F C D E
    如果没有key:首先比对头和头,A、B都复用, 比到C和F时,tag一样key相同(都为undefined)则会复用,会成下图情况
    vue
    文章图片
    image.png
    如果有key:比对到C和F时,C和F的key不相同所以跳过,此时就该比oldE和newE,EDC都相同,多下来的F直接插入

    vue
    文章图片
    image.png
    如果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 }); }

    【vue】内部正常的操作(不是通过state直接修改的)都需要包装一层_withCommitting
    replaceState(newState) { // 用最新的状态替换掉 this._withCommitting(() => { this._vm._data.$$state = newState; }) } store._withCommitting(() => { Vue.set(parent, path[path.length - 1], module.state); })

      推荐阅读