关于vue的问题「干货」

关于vue的阅读与总结,这是一份深刻思考后的关于vue的理解。举一反三,多多学习。javascript

背景(为何要学习开源项目的源码)

举一个最近本身看到的例子: vue-router插件中,借用vue.min能够混入生命周期,在这里混入的生命周期在每一个组件的这个生命周期的这个阶段都会调用:css

Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
复制代码

看到这个实现,对于之后想要实现vue插件而且绑定生命周期,提供了一种很好的思路和方法,每每能够举一反三,有意想不到的收获。html

说说你对MVVM的理解

MVVM 由如下三个内容组成前端

  • View:界面
  • Model:数据模型
  • ViewModel:做为桥梁负责沟通 View 和 Model

在 JQuery 时期,若是须要刷新 UI 时,须要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。vue

在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。java

MVVM 究竟是什么?与其专一于说明 MVVM 的来历,不如让咱们看一个典型的应用是如何构建的,并从那里了解 MVVM:node

这是一个典型的 MVC 设置。Model 呈现数据,View 呈现用户界面,而 View Controller 调节它二者之间的交互。react

虽然 View 和 View Controller 是技术上不一样的组件,但它们几乎老是手牵手在一块儿,成对的。算法

能够尝试将它们联系: vue-router

在典型的 MVC 应用里,许多逻辑被放在 View Controller 里。它们中的一些确实属于 View Controller,但更多的是所谓的“表示逻辑(presentation logic)”

以 MVVM 属术语来讲,就是那些将 Model 数据转换为 View 能够呈现的东西的事情,例如将一个 NSDate 转换为一个格式化过的 NSString

咱们的图解里缺乏某些东西,那些使咱们能够把全部表示逻辑放进去的东西。咱们打算将其称为 “View Model” —— 它位于 View/Controller 与 Model 之间:

这个图解准确地描述了什么是 MVVM:一个 MVC 的加强版,咱们正式链接了视图和控制器,并将表示逻辑从 Controller 移出放到一个新的对象里,即 View Model。

说说v-if和v-show

v-show和v-if

  1. v-if: 真正的条件渲染。false,不在dom中。
  2. v-show: 一直在dom中,只是用css的display属性进行切换(存在于html结构中,可是未用css进行渲染)。存在dom结构中
  3. display:none时,不在render(渲染树)树中。

visibility:hidden和display:none

display: none: 标签不会出如今页面上(尽管你仍然能够经过dom与它进行交互)。其它标签不会为它分配空间。 visibility:hidden: 标签会出如今页面上,只是看不见而已。其它标签会为它分配空间。

组件里的 data 必须是一个函数返回的对象,而不能就只是一个对象

若是须要,能够经过将 vm.$data 传入 JSON.parse(JSON.stringify(...)) 获得深拷贝的原始数据对象。

说说组件通讯经常使用的几种

props emit

父传子:props 子传父:emit

问题:多级嵌套组件

provide / inject

这对选项须要一块儿使用,以容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

element-ui的button组件, 部分源码

// Button 组件核心源码
export default {
    name: 'ElButton',
    // 经过 inject 获取 elForm 以及 elFormItem 这两个组件
    inject: {
        elForm: {
            default: ''
        },
        elFormItem: {
            default: ''
        }
    },
    // ...
    computed: {
        _elFormItemSize() {
            return (this.elFormItem || {}).elFormItemSize;
        },
        buttonSize() {
            return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        },
        //...
    },
    // ...
};
复制代码

问题:不可以实现子组件向祖先组件传递数据

$attrs $listeners

上述的provideinject实现了多层级组件数据的传输,可是不可以实现子组件向祖先组件传递数据,若是要实现子传祖,可使用$ attrs和$ listeners

EventBus

对于一些没有必要引进vuex的项目,可考虑

事件总线:EventBus能够用来很方便的实现兄弟组件和跨级组件的通讯,可是使用不当时也会带来不少问题(vue是单页应用,若是你在某一个页面刷新了以后,与之相关的EventBus会被移除,这样就致使业务走不下去);因此适合逻辑并不复杂的小页面,逻辑复杂时仍是建议使用vuex

class EventBus{
    constructor(){
        // 一个map,用于存储事件与回调之间的对应关系
        this.event=Object.create(null);
    };
    //注册事件
    on(name,fn){
        if(!this.event[name]){
            //一个事件可能有多个监听者
            this.event[name]=[];
        };
        this.event[name].push(fn);
    };
    //触发事件
    emit(name,...args){
        //给回调函数传参
        this.event[name]&&this.event[name].forEach(fn => {
            fn(...args)
        });
    };
    //只被触发一次的事件
    once(name,fn){
        //在这里同时完成了对该事件的注册、对该事件的触发,并在最后取消该事件。
        const cb=(...args)=>{
            //触发
            fn(...args);
            //取消
            this.off(name,fn);
        };
        //监听
        this.on(name,cb);
    };
    //取消事件
    off(name,offcb){
        if(this.event[name]){
            let index=this.event[name].findIndex((fn)=>{
                return offcb===fn;
            })
            this.event[name].splice(index,1);
            if(!this.event[name].length){
                delete this.event[name];
            }
        }
    }
}
复制代码

Vuex

状态管理,逻辑复杂时仍是建议使用vuex

Vue生命周期,各阶段都作了什么

beforeCreatecreated

beforeCreatecreated生命周期是在初始化的时候,在_init中执行

具体代码在vue/src/core/instance/init.js

Vue.prototype._init = function() {
      // expose real self
    //...
    vm._self = vm
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化props,methods,data,computed等
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // ...
}
复制代码
  1. beforeCreate. 不能用props,methods,data,computed等。
  2. initState. 初始化props,methods,data,computed等。
  3. created. 此时已经有,props,methods,data,computed等,要用data属性则能够在这里调用。

beforeCreatecreated这俩个钩子函数执行的时候,并无渲染 DOM,因此咱们也不可以访问 DOM,通常来讲,若是组件在加载的时候须要和后端有交互,放在这俩个钩子函数执行均可以,若是是须要访问 props、data 等数据的话,就须要使用 created 钩子函数。

beforeMountmounted

挂载是指将编译完成的HTML模板挂载到对应虚拟dom

在挂载开始以前被调用:相关的 render 函数首次被调用。

该钩子在服务器端渲染期间不被调用。

export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      // ...
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // ...
      vm._update(vnode, hydrating)
      // ...
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

在执行 vm._render() 函数渲染 VNode 以前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mounted 钩子。

beforeUpdateupdated

beforeUpdateupdated 的钩子函数执行时机都应该是在数据更新的时候

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // ...
  }
复制代码

这里有个细节是_isMounted, 表示要在mounted以后才执行beforeUpdate

至于updated则表示,当这个钩子被调用时 组件 DOM 已经更新,因此你如今能够执行依赖于 DOM 的操做

beforeDestroydestroyed

beforeDestroydestroyed 钩子函数的执行时机在组件销毁的阶段

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
复制代码

beforeDestroy 钩子函数的执行时机是在 $destroy 函数执行最开始的地方,接着执行了一系列的销毁动做,包括从 parent$children 中删掉自身,删除 watcher,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy 钩子函数。

$destroy 的执行过程当中,它又会执行 vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用,因此 destroy 钩子函数执行顺序是先子后父,和 mounted 过程同样。

activeddeactivated

activateddeactivated 钩子函数是专门为 keep-alive 组件定制的钩子

  1. activatedkeep-alive 组件激活时调用。
  2. deactivatedkeep-alive 组件销毁时调用。

errorCaptured

当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子能够返回 false 以阻止该错误继续向上传播。

new Vue时发生了什么

  1. 调用_init合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等
  2. 经过 Object.defineProperty 设置 settergetter 函数,用来实现响应式以及依赖收集

说说响应式原理的过程

当建立 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),而且在内部追踪依赖,在属性被访问和修改时通知变化。

每一个组件实例会有相应的 watcher 实例,会在组件渲染的过程当中记录依赖的全部数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),以后依赖项被改动时,setter 逻辑会通知依赖与此 data 的 watcher 实例从新计算(派发更新),从而使它关联的组件从新渲染。

总结就是: vue.js 采用数据劫持结合发布-订阅模式,经过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变更时发布消息给订阅者,触发响应的监听回调

核心角色

  • Observer(监听器):给对象添加getter和setter,用于依赖搜集和派发更新。不只是一个数据监听器,也是发布者;
  • Watcher(订阅者):observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。watcher实例分为渲染watcher(render watcher), computed watcher, 侦听器user watcher。维护了一个deps(用于收集依赖)的实例数组。二次依赖收集时,cleanupDeps 在每次添加完新的订阅,会移除掉旧的订阅的deps;
  • compile(编译器):MVVM 框架特有的角色,负责对每一个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的建立这些“杂活”也归它管;
  • Dep:用于收集当前响应式对象的依赖关系,每一个响应式对象都有一个Dep实例(里边subs是Watcher实例数组),数据变动触发setter逻辑时,经过dep.notify()(遍历subs,调用每一个Watcher的update()方法)通知各个Watcher

核心角色的关系以下:

核心代码

实现observer

// 遍历对象
function observer(target) {
  // target是对象,则遍历
  if (target && typeof target === 'object') {
    Object.keys(target).forEach(key => {
      defineReactive(target, key, target[key])
    })
  }
}

// 用defineProperty监听当前属性
function defineReactive(target, key, val) {
  const dep = new Dep()
  // 递归
  observer(val)
  Object.defineProperty(target, key, {
    // 可枚举
    enumerable: true,
    // 不可配置
    configurable: false,
    get: function() {
      return val
    },
    set: function(value) {
      console.log(val, value)
      val = value
    }
  })
}
复制代码

实现dep订阅者

class Dep {
  constructor() {
    // 初始化订阅队列
    this.subs = []
  }
  // 增长订阅
  addSub(sub) {
    this.subs.push(sub)
  }
  // 通知订阅者
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
复制代码

订阅者Dep里的subs数组是Watcher实例

实现Watcher类

class Watcher {
  constructor() {}
  update() {
    // 更新视图
  }
}
复制代码

改写 defineReactive 中的 setter 方法,在监听器里去通知订阅者了:

// 用defineProperty监听当前属性
function defineReactive(target, key, val) {
  const dep = new Dep()
  // 递归
  observer(val)
  Object.defineProperty(target, key, {
    // 可枚举
    enumerable: true,
    // 不可配置
    configurable: false,
    get: function() {
      return val
    },
    set: function(value) {
      console.log(val, value)
      dep.notify()
    }
  })
}
复制代码

2. Watcher和Dep的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者, dep 经过 notify 遍历了 dep.subs 通知每一个 watcher 更新。

3. computed 和 watch

computed 本质是一个惰性求值的观察者。

computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会马上求值,同时持有一个 dep 实例。 其内部经过 this.dirty 属性标记计算属性是否须要从新求值

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher, computed watcher 经过 this.dep.subs.length 判断有没有订阅者, 有的话,会从新计算,而后对比新旧值,若是变化了,会从新渲染。 (Vue 想确保不只仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 从新渲染,本质上是一种优化。)

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其余数据时,属性并不会当即从新计算,只有以后其余地方须要读取属性的时候,它才会真正计算,即具有 lazy(懒计算)特性。)

区别

computed 计算属性 : 依赖其它属性值,而且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会从新计算 computed 的值。

watch 侦听器 : 更多的是「观察」的做用,无缓存性,相似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操做。

4. 依赖收集

  1. initState时,对computed属性初始化时,触发computed Watcher依赖收集
  2. initState时,对watch属性初始化时,触发user Watcher依赖收集
  3. render()的过程,触发render watcher依赖收集
  4. re-render时,vm.render()再次执行,会移除subs的订阅,从新赋值

5. 派发更新

  1. 组件中,对响应式的数据进行了修改,触发setter的逻辑
  2. 调用dep.notity()
  3. 遍历dep.subs(Watcher 实例),调用每一个Watcehr 的 update()
  4. update()过程,又利用了队列作了进一步优化,在 nextTick 后执行全部 watcher 的 run,最后执行它们的回调函数。

在一个for循环中改变当前组件依赖的数据,改变一万次,会有什么效果( nextTick 的原理)

假如我在一个for循环中改变当前组件依赖的数据,改变一万次,会有什么效果?(涉及批量更新和 nextTick 原理) 总体过程: 在这里插入图片描述

JS运行机制

JS 执行是单线程的,它是基于事件循环的。事件循环大体分为如下几个步骤:

  1. 全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。
  2. 主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而全部的异步结果都是经过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,而且每一个 macro task 结束后,都要清空全部的 micro task。

for (macroTask of macroTaskQueue) {  
  // 1. Handle current MACRO-TASK  
  handleMacroTask();  
  // 2. Handle all MICRO-TASK  
  for (microTask of microTaskQueue) {    
    handleMicroTask(microTask);  
}}
复制代码

在浏览器环境中 :

常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

常见的 micro task 有 MutationObsever 和 Promise.then

异步更新队列

例题解答:number会被不停地进行++操做,不断地触发它对应的Dep中的Watcher对象的update方法。而后最终queue中由于对相同idWatcher对象进行了筛选(过滤),从而queue中实际上只会存在一个number对应的Watcher对象。在下一个 tick 的时候(此时number已经变成了 1000),触发Watcher对象的run方法来更新视图,将视图上的number` 从 0 直接变成 1000。

若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做是很是重要的。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,若是执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的实现原理:

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调前后执行
  2. microtask 由于其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 考虑兼容问题,vue 作了 microtask 向 macrotask 的降级方案

Vue 的响应式对数组是如何处理的

  1. 对数组中全部能改变数组自身的方法,如 push、pop 等这些方法进行重写。
  2. 而后手动调用 notify,通知 render watcher,执行 update

computed 属性为何可以在依赖改变的时候,本身发生变化

computed 和 watch 公用一个 Watcher 类,在 computed 的状况下有一个 deps 。 Vue 在二次收集依赖时用 cleanupDeps 在每次添加完新的订阅,会移除掉旧的订阅

为何在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty

  1. Object.defineProperty 自己有必定的监控到数组下标变化的能力, 可是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为何不能检测数组变更 )。

为了解决这个问题, 对数组中全部能改变数组自身的方法,如 push、pop 等这些方法进行重写。 而后手动调用 notify,通知 render watcher,执行 update

push();
pop();
shift();
unshift();
splice();
sort();
reverse();
复制代码
  1. Object.defineProperty 只能劫持对象的属性,所以咱们须要对每一个对象的每一个属性进行遍历。若是属性值也是对象那么须要深度遍历, 显然若是能劫持一个完整的对象是才是更好的选择。

Proxy 能够劫持整个对象,并返回一个新的对象。Proxy 不只能够代理对象,还能够代理数组。还能够代理动态增长的属性。

Vue 中的 key 到底有什么用

key 是给每个 vnode 的惟一 id,依靠 key,咱们的 diff 操做能够更准确、更快速 (对于简单列表页渲染来讲 diff 节点也更快,但会产生一些隐藏的反作用,好比可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。

diff 算法的过程当中,先会进行新旧节点的首尾交叉对比,当没法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点

更准确 : 由于带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中能够避免就地复用的状况。因此会更加准确,若是不加 key,会致使以前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的惟一性能够被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)

说说Vue的渲染过程

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程以下:

    • parse 函数解析 template,生成 ast(抽象语法树)
    • optimize 函数优化静态节点 (标记不须要每次都更新的内容,diff 算法会直接跳过静态节点,从而减小比较的过程,优化了 patch 的性能)
    • generate 函数生成 render 函数字符串
  2. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象

  3. 调用 patch 方法,对比新旧 vnode 对象,经过 DOM diff 算法,添加、修改、删除真正的 DOM 元素。

说说keep-alive 的实现原理和缓存策略

export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例创建父子关系的时候会被忽略,发生在 initLifecycle 的过程当中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },

  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },

  destroyed() {
    for (const key in this.cache) {
      // 删除全部缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },

  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }

      const { cache, keys } = this;
      // 获取键,优先获取组件的name字段,不然是组件的tag
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 命中缓存,直接从缓存拿vnode 的组件实例,而且从新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 不命中缓存,把 vnode 设置进缓存
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 若是配置了 max 而且缓存的长度超过了 this.max,还要从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};
复制代码

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

  2. 根据设定的 include/exclude(若是有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。若是存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

  4. 在 this.cache 对象中存储该组件实例并保存 key 值,以后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(便是下标为 0 的那个 key)

  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

缓存策略

LRU (Least Recently Used)缓存策略:从内存中找出最久未使用的数据置换新的数据。

核心思想是“若是数据最近被访问过,那么未来被访问的概率也更高。

最多见的实现是使用一个链表保存缓存数据,详细算法实现以下:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 链表满的时候,将链表尾部的数据丢弃;

Vue2.0引入虚拟 DOM 的目的是什么

Vue 为何要用虚拟 DOM (Virtual DOM)

  1. 能够接受 Parser 解析转化,抽象了本来的渲染过程。
  2. 跨平台的能力。渲染到 DOM 之外的平台, 实现 SSR、同构渲染这些高级特性。
  3. 关于虚拟dom比真实dom更快。虚拟DOM的优点体如今大量、频繁更改dom的状况。可是这样的状况并很少。

能谈谈vue实现时,有哪些性能优化方面的考虑吗

翻开vue源代码的过程,发现里边有写多值得学习的优化过程,这里记录下来:

  1. cache函数,利用闭包实现缓存

  2. 二次依赖收集时,cleanupDeps, 剔除上一次存在但本次渲染不存在的依赖

  3. traverse,处理深度监听数据,解除循环引用

  4. 编译优化阶段,optimize的主要做用是标记 static 静态节点

  5. keep-alive组件利用lRU缓存淘汰算法

  6. 异步组件,分两次渲染

Diff 本质的过程,简单说说

过程

  1. 先同级比较再比较子节点

  2. 先判断一方有子节点和一方没有子节点的状况。若是新的一方有子节点,旧的一方没有,至关于新的子节点替代了原来没有的节点;同理,若是新的一方没有子节点,旧的一方有,至关于要把老的节点删除。

  3. 再来比较都有子节点的状况,这里是diff的核心。首先会经过判断两个节点的key、tag、isComment、data同时定义或不定义以及当标签类型为input的时候type相不相同来肯定两个节点是否是相同的节点,若是不是的话就将新节点替换旧节点。

  4. 若是是相同节点的话才会进入到patchVNode阶段。在这个阶段核心是采用双指针的算法,同时重新旧节点的两端进行比较,在这个过程当中,会用到模版编译时的静态标记配合key来跳过对比静态节点,若是不是的话再进行其它的比较。

举例说明:

// old arr
["a", "b", "c", "d", "e", "f", "g", "h"]
// new arr
["a", "b", "d", "f", "c", "e", "x", "y", "g", "h"]
复制代码
  1. 从头至尾开始比较,[a,b]是sameVnode,进入patch,到 [c] 中止;

  2. 从尾到头开始比较,[h,g]是sameVnode,进入patch,到 [f] 中止;

  3. 判断旧数据是否已经比较完毕,多余的说明是新增的,须要mount(本例中没有)

  4. 判断新数据是否已经比较完毕,多余的说明是删除的,须要unmount(本例中没有)

  5. patchVNode阶段。在这个阶段核心是采用双指针的算法,同时重新旧节点的两端进行比较,在这个过程当中,会用到模版编译时的静态标记配合key来跳过对比静态节点,若是不是的话再进行其它的比较。

缺点:由于采用的是同级比较,因此若是发现本级的节点不一样的话就会将新节点之间替换旧节点,不会再去比较其下的子节点是否有相同

vue二、vue3和react比较

Vue二、Vue3

Vue3.x借鉴了ivi算法和inferno算法。

它在建立VNode的时候就肯定了其类型,以及在mount/patch的过程当中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提高

vue 和 react

相同是都是用同层比较,不一样是 vue使用双指针比较,react 是用 key 集合级比较

谈谈vue-router

VueRouter对不一样模式的实现大体是这样的:

  1. 首先根据mode来肯定所选的模式,若是当前环境不支持history模式,会强制切换到hash模式;

  2. 若是当前环境不是浏览器环境,会切换到abstract模式下。而后再根据不一样模式来生成不一样的history操做对象。

new Router过程

  1. init 方法内的 app变量即是存储的当前的vue实例的this。
  2. 将 app 存入数组apps中。经过this.app判断是实例否已经被初始化。
  3. 经过history来肯定不一样路由的切换动做动做 history.transitionTo。
  4. 经过 history.listen来注册路由变化的响应回调。

hash和history的区别

  1. 最明显的是在显示上,hash模式的URL中会夹杂着#号,而history没有。
  2. Vue底层对它们的实现方式不一样。hash模式是依靠onhashchange事件(监听location.hash的改变),而history模式(popstate)是主要是依靠的HTML5 history中新增的两个方法,pushState()能够改变url地址且不会发送请求,replaceState()能够读取历史记录栈,还能够对浏览器记录进行修改。
  3. 当真正须要经过URL向后端发送HTTP请求的时候,好比常见的用户手动输入URL后回车,或者是刷新(重启)浏览器,这时候history模式须要后端的支持。由于history模式下,前端的URL必须和实际向后端发送请求的URL一致,例若有一个URL是带有路径path的(例如www.lindaidai.wang/blogs/id),若是后端没有对这个路径作处理的话,就会返回404错误。因此须要后端增长一个覆盖全部状况的候选资源,通常会配合前端给出的一个404页面。
做者:大俊_  
相关文章
相关标签/搜索