vue

MVVM

model和view层通过中间的vm连接和驱动。model层数据变化会改变视图,view改变通过事件来修改数据。vue参考了MVVM实现了双向绑定,react是MVC,但是vue仍然可以通过ref、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,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化

image.png

数据劫持

利用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];    
}
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];
}
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]
}
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]
}
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++;


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:为了高效复用

image.png

A B C D E
A B F C D E
如果没有key:首先比对头和头,A、B都复用,比到C和F时,tag一样key相同(都为undefined)则会复用,会成下图情况
image.png

如果有key:比对到C和F时,C和F的key不相同所以跳过,此时就该比oldE和newE,EDC都相同,多下来的F直接插入

image.png

如果key使用index,遇到表单元素比如带checkbox的列表,如果状态勾选后,会复用勾选状态产生bug

keep-alive组件

会将组件缓存到this.cache中,放入内存中缓存起来。

vue-router

1、install方法注册全局组件,挂载route\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.state 或者 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);
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351