乐趣区

关于vue.js:社招前端经典vue面试题附答案

Vuex 页面刷新数据失落怎么解决

体验

能够从 localStorage 中获取作为状态初始值:

const store = createStore({state () {
    return {count: localStorage.getItem('count')
    }
  }
})

业务代码中,提交批改状态同时保留最新值:虽说实现了,然而每次还要手动刷新 localStorage 不太优雅

store.commit('increment')
localStorage.setItem('count', store.state.count)

答复范例

  1. vuex只是在内存保留状态,刷新之后就会失落,如果要长久化就要存起来
  2. localStorage就很适合,提交 mutation 的时候同时存入 localStoragestore 中把值取出作为 state 的初始值即可。
  3. 这里有两个问题,不是所有状态都须要长久化;如果须要保留的状态很多,编写的代码就不够优雅,每个提交的中央都要独自做保留解决。这里就能够利用 vuex 提供的 subscribe 办法做一个对立的解决。甚至能够封装一个 vuex 插件以便复用。
  4. 相似的插件有 vuex-persistvuex-persistedstate,外部的实现就是通过订阅mutation 变动做对立解决,通过插件的选项管制哪些须要长久化

原理

能够看一下 vuex-persist (opens new window)外部的确是利用 subscribe 实现的

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题?你能说说如下代码的实现原理么?

1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题

  1. Vue 应用了 Object.defineProperty 实现双向数据绑定
  2. 在初始化实例时对属性执行 getter/setter 转化
  3. 属性必须在 data 对象上存在能力让 Vue 将它转换为响应式的(这也就造成了 Vue 无奈检测到对象属性的增加或删除)

所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)

2)接下来咱们看看框架自身是如何实现的呢?

Vue 源码地位:vue/src/core/instance/index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  // target 为数组  
  if (Array.isArray(target) && isValidArrayIndex(key)) {// 批改数组的长度, 防止索引 > 数组长度导致 splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的 splice 变异办法触发响应式  
    target.splice(key, 1, val)
    return val
  }
  // key 曾经存在,间接批改属性值  
  if (key in target && !(key in Object.prototype)) {target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // target 自身就不是响应式数据, 间接赋值
  if (!ob) {target[key] = val
    return val
  }
  // 对属性进行响应式解决
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

咱们浏览以上源码可知,vm.$set 的实现原理是:

  1. 如果指标是数组,间接应用数组的 splice 办法触发相应式;
  2. 如果指标是对象,会先判读属性是否存在、对象是否是响应式,
  3. 最终如果要对属性进行响应式解决,则是通过调用 defineReactive 办法进行响应式解决

defineReactive 办法就是 Vue 在初始化对象时,给对象属性采纳 Object.defineProperty 动静增加 getter 和 setter 的性能所调用的办法

Vue 为什么没有相似于 React 中 shouldComponentUpdate 的生命周期?

考点: Vue 的变动侦测原理

前置常识: 依赖收集、虚构 DOM、响应式零碎

根本原因是 Vue 与 React 的变动侦测形式有所不同

React 是 pull 的形式侦测变动, 当 React 晓得发生变化后, 会应用 Virtual Dom Diff 进行差别检测, 然而很多组件实际上是必定不会发生变化的, 这个时候须要用 shouldComponentUpdate 进行手动操作来缩小 diff, 从而进步程序整体的性能.

Vue 是 pull+push 的形式侦测变动的, 在一开始就晓得那个组件产生了变动, 因而在 push 的阶段并不需要手动管制 diff, 而组件外部采纳的 diff 形式实际上是能够引入相似于 shouldComponentUpdate 相干生命周期的, 然而通常正当大小的组件不会有适量的 diff, 手动优化的价值无限, 因而目前 Vue 并没有思考引入 shouldComponentUpdate 这种手动优化的生命周期.

vue 中应用了哪些设计模式

1. 工厂模式 – 传入参数即可创立实例

虚构 DOM 依据参数的不同返回根底标签的 Vnode 和组件 Vnode

2. 单例模式 – 整个程序有且仅有一个实例

vuex 和 vue-router 的插件注册办法 install 判断如果零碎存在实例就间接返回掉

3. 公布 - 订阅模式 (vue 事件机制)

4. 观察者模式 (响应式数据原理)

5. 装璜模式: (@装璜器的用法)

6. 策略模式 策略模式指对象有某个行为, 然而在不同的场景中, 该行为有不同的实现计划 - 比方选项的合并策略

组件通信

组件通信的形式如下:

(1)props / $emit

父组件通过 props 向子组件传递数据,子组件通过 $emit 和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间造成了一个单向上行绑定。子组件的数据会随着父组件不断更新。
  • props 能够显示定义一个或一个以上的数据,对于接管的数据,能够是各种数据类型,同样也能够传递一个函数。
  • props属性名规定:若在 props 中应用驼峰模式,模板中须要应用短横线的模式
// 父组件
<template>
  <div id="father">
    <son :msg="msgData" :fn="myFunction"></son>
  </div>
</template>

<script>
import son from "./son.vue";
export default {
  name: father,
  data() {msgData: "父组件数据";},
  methods: {myFunction() {console.log("vue");
    },
  },
  components: {son},
};
</script>
// 子组件
<template>
  <div id="son">
    <p>{{msg}}</p>
    <button @click="fn"> 按钮 </button>
  </div>
</template>
<script>
export default {name: "son", props: ["msg", "fn"] };
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过 v-on 监听并接管参数。
// 父组件
<template>
  <div class="section">
    <com-article
      :articles="articleList"
      @onEmitIndex="onEmitIndex"
    ></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from "./test/article.vue";
export default {
  name: "comArticle",
  components: {comArticle},
  data() {return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
  },
  methods: {onEmitIndex(idx) {this.currentIndex = idx;},
  },
};
</script>
// 子组件
<template>
  <div>
    <div
      v-for="(item, index) in articles"
      :key="index"
      @click="emitIndex(index)"
    >
      {{item}}
    </div>
  </div>
</template>

<script>
export default {props: ["articles"],
  methods: {emitIndex(index) {this.$emit("onEmitIndex", index); // 触发父组件的办法,并传递参数 index
    },
  },
};
</script>

(2)eventBus 事件总线($emit / $on

eventBus事件总线实用于 父子组件 非父子组件 等之间的通信,应用步骤如下:(1)创立事件核心治理组件之间的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 假如有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default {components: { firstCom, secondCom} };
</script>

firstCom 组件中发送事件:

<template>
  <div>
    <button @click="add"> 加法 </button>
  </div>
</template>

<script>
import {EventBus} from "./event-bus.js"; // 引入事件核心

export default {data() {return { num: 0};
  },
  methods: {add() {EventBus.$emit("addition", { num: this.num++});
    },
  },
};
</script>

(3)接管事件 secondCom 组件中发送事件:

<template>
  <div> 求和: {{count}}</div>
</template>

<script>
import {EventBus} from "./event-bus.js";
export default {data() {return { count: 0};
  },
  mounted() {EventBus.$on("addition", (param) => {this.count = this.count + param.num;});
  },
};
</script>

在上述代码中,这就相当于将 num 值存贮在了事件总线中,在其余组件中能够间接拜访。事件总线就相当于一个桥梁,不必组件通过它来通信。

尽管看起来比较简单,然而这种办法也有不变之处,如果我的项目过大,应用这种形式进行通信,前期保护起来会很艰难。

(3)依赖注入(provide / inject)

这种形式就是 Vue 中的 依赖注入 ,该办法用于 父子组件之间的通信。当然这里所说的父子不肯定是真正的父子,也能够是祖孙组件,在层数很深的状况下,能够应用这种办法来进行传值。就不必一层一层的传递了。

provide / inject是 Vue 提供的两个钩子,和 datamethods 是同级的。并且 provide 的书写模式和 data 一样。

  • provide 钩子用来发送数据或办法
  • inject钩子用来接收数据或办法

在父组件中:

provide() { 
    return {num: this.num};
}

在子组件中:

inject: ['num']

还能够这样写,这样写就能够拜访父组件中的所有属性:

provide() {
 return {app: this};
}
data() {
 return {num: 1};
}

inject: ['app']
console.log(this.app.num)

留神: 依赖注入所提供的属性是 非响应式 的。

(3)ref / $refs

这种形式也是实现 父子组件 之间的通信。

ref:这个属性用在子组件上,它的援用就指向了子组件的实例。能够通过实例来拜访组件的数据和办法。

在子组件中:

export default {data () {
    return {name: 'JavaScript'}
  },
  methods: {sayHello () {console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
  mounted() {console.log(this.$refs.child.name); // JavaScript
    this.$refs.child.sayHello(); // hello},
};
</script>

(4)$parent / $children

  • 应用 $parent 能够让组件拜访父组件的实例(拜访的是上一级父组件的属性和办法)
  • 应用 $children 能够让组件拜访子组件的实例,然而,$children并不能保障程序,并且拜访的数据也不是响应式的。

在子组件中:

<template>
  <div>
    <span>{{message}}</span>
    <p> 获取父组件的值为: {{parentVal}}</p>
  </div>
</template>

<script>
export default {data() {return { message: "Vue"};
  },
  computed: {parentVal() {return this.$parent.msg;},
  },
};
</script>

在父组件中:

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change"> 点击扭转子组件值 </button>
  </div>
</template>

<script>
import child from "./child.vue";
export default {components: { child},
  data() {return { msg: "Welcome"};
  },
  methods: {change() {
      // 获取到子组件
      this.$children[0].message = "JavaScript";
    },
  },
};
</script>

在下面的代码中,子组件获取到了父组件的 parentVal 值,父组件扭转了子组件中 message 的值。须要留神:

  • 通过 $parent 拜访到的是上一级父组件的实例,能够应用 $root 来拜访根组件的实例
  • 在组件中应用 $children 拿到的是所有的子组件的实例,它是一个数组,并且是无序的
  • 在根组件 #app 上拿 $parent 失去的是 new Vue() 的实例,在这实例上再拿 $parent 失去的是 undefined,而在最底层的子组件拿$children 是个空数组
  • $children 的值是 数组 ,而$parent 是个 对象

(5)$attrs / $listeners

思考一种场景,如果 A 是 B 组件的父组件,B 是 C 组件的父组件。如果想要组件 A 给组件 C 传递数据,这种隔代的数据,该应用哪种形式呢?

如果是用 props/$emit 来一级一级的传递,的确能够实现,然而比较复杂;如果应用事件总线,在多人开发或者我的项目较大的时候,保护起来很麻烦;如果应用 Vuex,确实也能够,然而如果仅仅是传递数据,那可能就有点节约了。

针对上述情况,Vue 引入了$attrs / $listeners,实现组件之间的跨代通信。

先来看一下 inheritAttrs,它的默认值 true,继承所有的父组件属性除props 之外的所有属性;inheritAttrs:false 只继承 class 属性。

  • $attrs:继承所有的父组件属性(除了 prop 传递的属性、class 和 style),个别用在子组件的子元素上
  • $listeners:该属性是一个对象,外面蕴含了作用在这个组件上的所有监听器,能够配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A 组件(APP.vue):

<template>
  <div id="app">
    // 此处监听了两个事件,能够在 B 组件或者 C 组件中间接触发
    <child1
      :p-child1="child1"
      :p-child2="child2"
      @test1="onTest1"
      @test2="onTest2"
    ></child1>
  </div>
</template>
<script>
import Child1 from "./Child1.vue";
export default {components: { Child1},
  methods: {onTest1() {console.log("test1 running");
    },
    onTest2() {console.log("test2 running");
    },
  },
};
</script>

B 组件(Child1.vue):

<template>
  <div class="child-1">
    <p>props: {{pChild1}}</p>
    <p>$attrs: {{$attrs}}</p>
    <child2 v-bind="$attrs" v-on="$listeners"></child2>
  </div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {props: ["pChild1"],
  components: {Child2},
  inheritAttrs: false,
  mounted() {this.$emit("test1"); // 触发 APP.vue 中的 test1 办法
  },
};
</script>

C 组件 (Child2.vue):

<template>
  <div class="child-2">
    <p>props: {{pChild2}}</p>
    <p>$attrs: {{$attrs}}</p>
  </div>
</template>
<script>
export default {props: ["pChild2"],
  inheritAttrs: false,
  mounted() {this.$emit("test2"); // 触发 APP.vue 中的 test2 办法
  },
};
</script>

在上述代码中:

  • C 组件中能间接触发 test 的起因在于 B 组件调用 C 组件时 应用 v-on 绑定了$listeners 属性
  • 在 B 组件中通过 v -bind 绑定 $attrs 属性,C 组件能够间接获取到 A 组件中传递下来的 props(除了 B 组件中 props 申明的)

(6)总结

(1)父子组件间通信

  • 子组件通过 props 属性来承受父组件的数据,而后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来取得子组件,子组件通过 $parent 取得父组件,这样也能够实现通信。
  • 应用 provide/inject,在父组件中通过 provide 提供变量,在子组件中通过 inject 来将变量注入到组件中。不管子组件有多深,只有调用了 inject 那么就能够注入 provide 中的数据。

(2)兄弟组件间通信

  • 应用 eventBus 的办法,它的实质是通过创立一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现音讯的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也能够进行通信。

(3)任意组件之间

  • 应用 eventBus,其实就是创立一个事件核心,相当于中转站,能够用它来传递事件和接管事件。

如果业务逻辑简单,很多组件之间须要同时解决一些公共的数据,这个时候采纳下面这一些办法可能不利于我的项目的保护。这个时候能够应用 vuex,vuex 的思维就是将这一些公共的数据抽离进去,将它作为一个全局的变量来治理,而后其余组件就能够对这个公共数据进行读写操作,这样达到理解耦的目标。

理解 nextTick 吗?

异步办法,异步渲染最初一步,与 JS 事件循环分割严密。次要应用了宏工作微工作(setTimeoutpromise那些),定义了一个异步办法,屡次调用 nextTick 会将办法存入队列,通过异步办法清空以后队列。

参考 前端进阶面试题具体解答

Vue 的性能优化有哪些

(1)编码阶段

  • 尽量减少 data 中的数据,data 中的数据都会减少 getter 和 setter,会收集对应的 watcher
  • v-if 和 v -for 不能连用
  • 如果须要应用 v -for 给每项元素绑定事件时应用事件代理
  • SPA 页面采纳 keep-alive 缓存组件
  • 在更多的状况下,应用 v -if 代替 v -show
  • key 保障惟一
  • 应用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动静加载
  • 图片懒加载

(2)SEO 优化

  • 预渲染
  • 服务端渲染 SSR

(3)打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 应用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化

(4)用户体验

  • 骨架屏
  • PWA
  • 还能够应用缓存 (客户端缓存、服务端缓存) 优化、服务端开启 gzip 压缩等。

什么是 mixin?

  • Mixin 使咱们可能为 Vue 组件编写可插拔和可重用的性能。
  • 如果心愿在多个组件之间重用一组组件选项,例如生命周期 hook、办法等,则能够将其编写为 mixin,并在组件中简略的援用它。
  • 而后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

Vue.mixin 的应用场景和原理

  • 在日常的开发中,咱们常常会遇到在不同的组件中常常会须要用到一些雷同或者类似的代码,这些代码的性能绝对独立,能够通过 Vuemixin 性能抽离公共的业务逻辑,原理相似“对象的继承”,当组件初始化时会调用 mergeOptions 办法进行合并,采纳策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以失当的形式进行“合并”;如果混入的数据和自身组件的数据抵触,会以组件的数据为准
  • mixin有很多缺点如:命名抵触、依赖问题、数据起源问题

根本应用

<script>
    // Vue.options
    Vue.mixin({ // 如果他是对象 每个组件都用 mixin 里的对象进行合并
        data(){return {a: 1,b: 2}
        }
    });
    // Vue.extend
    Vue.component('my',{ // 组件必须是函数 Vue.extend  => render(xxx)
        data(){return {x:1}
        }
    }) 
    // 没有 new 没有实例  _init()
    // const vm = this
    new Vue({
        el:'#app',
        data(){ // 根能够不是函数 
            return {c:3}
        }
    })
</script>

相干源码

export default function initMixin(Vue){Vue.mixin = function (mixin) {
    //   合并对象
      this.options=mergeOptions(this.options,mixin)
  };
}
};

// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
];

// 合并策略
const strats = {};
// mixin 外围办法
export function mergeOptions(parent, child) {const options = {};
  // 遍历父亲
  for (let k in parent) {mergeFiled(k);
  }
  // 父亲没有 儿子有
  for (let k in child) {if (!parent.hasOwnProperty(k)) {mergeFiled(k);
    }
  }

  // 真正合并字段办法
  function mergeFiled(k) {
    // strats 合并策略
    if (strats[k]) {options[k] = strats[k](parent[k], child[k]);
    } else {
      // 默认策略
      options[k] = child[k] ? child[k] : parent[k];
    }
  }
  return options;
}

你有对 Vue 我的项目进行哪些优化?

(1)代码层面的优化

  • v-if 和 v-show 辨别应用场景
  • computed 和 watch 辨别应用场景
  • v-for 遍历必须为 item 增加 key,且防止同时应用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化有限列表性能
  • 服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 缩小 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建后果输入剖析
  • Vue 我的项目的编译优化

(3)根底的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的应用
  • 应用 Chrome Performance 查找性能瓶颈

nextTick 在哪里应用?原理是?

  • nextTick 中的回调是在下次 DOM 更新循环完结之后执行提早回调,用于取得更新后的 DOM
  • 在批改数据之后立刻应用这个办法,获取更新后的 DOM
  • 次要思路就是采纳 微工作优先 的形式调用异步办法去执行 nextTick 包装的办法

nextTick 办法次要是应用了宏工作和微工作, 定义了一个异步办法. 屡次调用 nextTick 会将办法存入队列中,通过这个异步办法清空以后队列。所以这个 nextTick 办法就是异步办法

依据执行环境别离尝试采纳

  • 先采纳Promise
  • Promise不反对,再采纳MutationObserver
  • MutationObserver不反对,再采纳setImmediate
  • 如果以上都不行则采纳setTimeout
  • 最初执行 flushCallbacks,把callbacks 外面的数据顺次执行

答复范例

  1. nextTick 中的回调是在下次 DOM 更新循环完结之后执行提早回调,用于取得更新后的 DOM
  2. Vue有个异步更新策略,意思是如果数据变动,Vue不会立即更新 DOM,而是开启一个队列,把组件更新函数保留在队列中,在同一事件循环中产生的所有数据变更会异步的批量更新。这一策略导致咱们对数据的批改不会立即体现在 DOM 上,此时如果想要获取更新后的 DOM 状态,就须要应用nextTick
  3. 开发时,有两个场景咱们会用到nextTick
  4. created中想要获取 DOM
  • 响应式数据变动后获取 DOM 更新后的状态,比方心愿获取列表更新后的高度
  • nextTick签名如下:function nextTick(callback?: () => void): Promise<void>

所以咱们只须要在传入的回调函数中拜访最新 DOM 状态即可,或者咱们能够 await nextTick() 办法返回的 Promise 之后做这件事

  1. Vue 外部,nextTick之所以可能让咱们看到 DOM 更新后的后果,是因为咱们传入的 callback 会被增加到队列刷新函数 (flushSchedulerQueue) 的前面,这样等队列外部的更新函数都执行结束,所有 DOM 操作也就完结了,callback天然可能获取到最新的 DOM 值

根本应用

const vm = new Vue({
    el: '#app',
    data() {return { a: 1}
    }
}); 

// vm.$nextTick(() => {// [nextTick 回调函数 fn, 外部更新 flushSchedulerQueue]
//     console.log(vm.$el.innerHTML)
// })

// 是将内容保护到一个数组里,最终依照程序程序。第一次会开启一个异步工作

vm.a = 'test'; // 批改了数据后并不会马上更新视图
vm.$nextTick(() => {// [nextTick 回调函数 fn, 外部更新 flushSchedulerQueue]
    console.log(vm.$el.innerHTML)
})

// nextTick 中的办法会被放到 更新页面 watcher 的前面去

相干代码如下

// src/core/utils/nextTick
let callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false; // 把标记还原为 false
  // 顺次执行回调
  for (let i = 0; i < callbacks.length; i++) {callbacks[i]();}
}
let timerFunc; // 定义异步办法  采纳优雅降级
if (typeof Promise !== "undefined") {
  // 如果反对 promise
  const p = Promise.resolve();
  timerFunc = () => {p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 次要是监听 dom 变动 也是一个异步办法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {characterData: true,});
  timerFunc = () => {counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果后面都不反对 判断 setImmediate
  timerFunc = () => {setImmediate(flushCallbacks);
  };
} else {
  // 最初降级采纳 setTimeout
  timerFunc = () => {setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  // 除了渲染 watcher  还有用户本人手动调用的 nextTick 一起被收集到数组
  callbacks.push(cb);
  if (!pending) {
    // 如果屡次调用 nextTick  只会执行一次异步 等异步队列清空之后再把标记变为 false
    pending = true;
    timerFunc();}
}

数据更新的时候外部会调用nextTick

// src/core/observer/scheduler.js

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {has[id] = true
    if (!flushing) {queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {i--}
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()
        return
      }
      // 把更新办法放到数组中保护[nextTick 回调函数, 更新函数 flushSchedulerQueue]
      /**
       * vm.a = 'test'; // 批改了数据后并不会马上更新视图
        vm.$nextTick(() => {// [fn, 更新]
            console.log(vm.$el.innerHTML)
        })
       */
      nextTick(flushSchedulerQueue)
    }
  }
}

Vue 路由 hash 模式和 history 模式

1. hash模式

晚期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简略,location.hash 的值就是 URL# 前面的内容。比方上面这个网站,它的 location.hash 的值为 '#search'

https://interview2.poetries.top#search

hash 路由模式的实现次要是基于上面几个个性

  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 局部不会被发送;
  • hash 值的扭转,都会在浏览器的拜访历史中减少一个记录。因而咱们能通过浏览器的回退、后退按钮管制 hash 的切换;
  • 能够通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会产生扭转;或者应用 JavaScript 来对 loaction.hash 进行赋值,扭转 URLhash 值;
  • 咱们能够应用 hashchange 事件来监听 hash 值的变动,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);

每一次扭转 hashwindow.location.hash),都会在浏览器的拜访历史中减少一个记录利用 hash 的以上特点,就能够来实现前端路由“更新视图但不从新申请页面”的性能了

特点:兼容性好然而不美观

2. history模式

history采纳 HTML5 的新个性;且提供了两个新办法:pushState()replaceState()能够对浏览器历史记录栈进行批改,以及 popState 事件的监听到状态变更

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

这两个办法有个独特的特点:当调用他们批改浏览器历史记录栈后,尽管以后 URL 扭转了,但浏览器不会刷新页面,这就为单页利用前端路由“更新视图但不从新申请页面”提供了根底。

history 路由模式的实现次要基于存在上面几个个性:

  • pushStaterepalceState 两个 API 来操作实现 URL 的变动;
  • 咱们能够应用 popstate 事件来监听 url 的变动,从而对页面进行跳转(渲染);
  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时咱们须要手动触发页面跳转(渲染)。

特点:尽管好看,然而刷新会呈现 404 须要后端进行配置

vue 中应用了哪些设计模式

1. 工厂模式 – 传入参数即可创立实例

虚构 DOM 依据参数的不同返回根底标签的 Vnode 和组件 Vnode

2. 单例模式 – 整个程序有且仅有一个实例

vuex 和 vue-router 的插件注册办法 install 判断如果零碎存在实例就间接返回掉

3. 公布 - 订阅模式 (vue 事件机制)

4. 观察者模式 (响应式数据原理)

5. 装璜模式: (@装璜器的用法)

6. 策略模式 策略模式指对象有某个行为, 然而在不同的场景中, 该行为有不同的实现计划 - 比方选项的合并策略

… 其余模式欢送补充

Proxy 与 Object.defineProperty 优劣比照

Proxy 的劣势如下:

  • Proxy 能够间接监听对象而非属性;
  • Proxy 能够间接监听数组的变动;
  • Proxy 有多达 13 种拦挡办法, 不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象, 咱们能够只操作新的对象达到目标, 而 Object.defineProperty 只能遍历对象属性间接批改;

Proxy 作为新规范将受到浏览器厂商重点继续的性能优化,也就是传说中的新规范的性能红利;

Object.defineProperty 的劣势如下:

  • 兼容性好,反对 IE9,而 Proxy 的存在浏览器兼容性问题, 而且无奈用 polyfill 磨平,因而 Vue 的作者才申明须要等到下个大版本 (3.0) 能力用 Proxy 重写。

Vue 为什么须要虚构 DOM?优缺点有哪些

因为在浏览器中操作 DOM是很低廉的。频繁的操作 DOM,会产生肯定的性能问题。这就是虚构 Dom 的产生起因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 实质就是用一个原生的 JS 对象去形容一个 DOM 节点,是对实在 DOM 的一层形象

长处:

  • 保障性能上限:框架的虚构 DOM 须要适配任何下层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;然而比起粗犷的 DOM 操作性能要好很多,因而框架的虚构 DOM 至多能够保障在你不须要手动优化的状况下,仍然能够提供还不错的性能,即保障性能的上限;
  • 无需手动操作 DOM:咱们不再须要手动去操作 DOM,只须要写好 View-Model 的代码逻辑,框架会依据虚构 DOM 和 数据双向绑定,帮咱们以可预期的形式更新视图,极大进步咱们的开发效率;
  • 跨平台:虚构 DOM 实质上是 JavaScript 对象, 而 DOM 与平台强相干,相比之下虚构 DOM 能够进行更不便地跨平台操作,例如服务器渲染、weex 开发等等。

毛病:

  • 无奈进行极致优化:尽管虚构 DOM + 正当的优化,足以应答绝大部分利用的性能需求,但在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化。
  • 首次渲染大量 DOM 时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。

虚构 DOM 实现原理?

虚构 DOM 的实现原理次要包含以下 3 局部:

  • JavaScript 对象模仿实在 DOM 树,对实在 DOM 进行形象;
  • diff 算法 — 比拟两棵虚构 DOM 树的差别;
  • pach 算法 — 将两个虚构 DOM 对象的差别利用到真正的 DOM 树。

说说你对虚构 DOM 的了解?答复范例

思路

  • vdom是什么
  • 引入 vdom 的益处
  • vdom如何生成,又如何成为dom
  • 在后续的 diff 中的作用

答复范例

  1. 虚构 dom 顾名思义就是虚构的 dom 对象,它自身就是一个 JavaScript 对象,只不过它是通过不同的属性去形容一个视图构造
  2. 通过引入 vdom 咱们能够取得如下益处:
  3. 将实在元素节点形象成 VNode,无效缩小间接操作 dom 次数,从而进步程序性能

    • 间接操作 dom 是有限度的,比方:diffclone 等操作,一个实在元素上有许多的内容,如果间接对其进行 diff 操作,会去额定 diff 一些没有必要的内容;同样的,如果须要进行 clone 那么须要将其全部内容进行复制,这也是没必要的。然而,如果将这些操作转移到 JavaScript 对象上,那么就会变得简略了
    • 操作 dom 是比拟低廉的操作,频繁的 dom 操作容易引起页面的重绘和回流,然而通过形象 VNode 进行两头解决,能够无效缩小间接操作 dom 的次数,从而缩小页面重绘和回流
  • 不便实现跨平台

    • 同一 VNode 节点能够渲染成不同平台上的对应的内容,比方:渲染在浏览器是 dom 元素节点,渲染在 Native(iOS、Android)变为对应的控件、能够实现 SSR、渲染到 WebGL 中等等
    • Vue3 中容许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染
  • vdom如何生成?在 vue 中咱们经常会为组件编写模板 – template,这个模板会被编译器 – compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用 render 函数,返回的对象就是虚构 dom。但它们还不是真正的dom,所以会在后续的patch 过程中进一步转化为dom
  1. 挂载过程完结后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件从新 render,此时就会生成新的vdom,和上一次的渲染后果diff 就能失去变动的中央,从而转换为最小量的 dom 操作,高效更新视图

为什么要用 vdom?案例解析

当初有一个场景,实现以下需要:

[{ name: "张三", age: "20", address: "北京"},    
  {name: "李四", age: "21", address: "武汉"},    
  {name: "王五", age: "22", address: "杭州"},
]

将该数据展现成一个表格,并且轻易批改一个信息,表格也跟着批改。用 jQuery 实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="container"></div>
  <button id="btn-change"> 扭转 </button>

  <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
  <script>
    const data = [{
        name: "张三",
        age: "20",
        address: "北京"
      },
      {
        name: "李四",
        age: "21",
        address: "武汉"
      },
      {
        name: "王五",
        age: "22",
        address: "杭州"
      },
    ];
    // 渲染函数
    function render(data) {const $container = $('#container');
      $container.html('');
      const $table = $('<table>');
      // 重绘一次
      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));
      data.forEach(item => {
        // 每次进入都重绘
        $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))
      })
      $container.append($table);
    }

    $('#btn-change').click(function () {data[1].age = 30;
      data[2].address = '深圳';
      render(data);
    });
  </script>
</body>
</html>
  • 这样点击按钮,会有相应的视图变动,然而你审查以下元素,每次改变之后,table标签都得从新创立,也就是说 table 上面的每一个栏目,不论是数据是否和原来一样,都得从新渲染,这并不是现实中的状况,当其中的一栏数据和原来一样,咱们心愿这一栏不要从新渲染,因为 DOM 重绘相当耗费浏览器性能。
  • 因而咱们采纳 JS 对象模仿的办法,将 DOM 的比对操作放在 JS 层,缩小浏览器不必要的重绘,提高效率。
  • 当然有人说虚构 DOM 并不比实在的 DOM 快,其实也是有情理的。当上述 table 中的每一条数据都扭转时,显然实在的 DOM 操作更快,因为虚构 DOM 还存在 jsdiff算法的比对过程。所以,上述性能劣势仅仅实用于大量数据的渲染并且扭转的数据只是一小部分的状况。

如下 DOM 构造:

<ul id="list">
    <li class="item">Item1</li>
    <li class="item">Item2</li>
</ul>

映射成虚构 DOM 就是这样:

{
  tag: "ul",
  attrs: {id: "list"},
  children: [
    {
      tag: "li",
      attrs: {className: "item"},
      children: ["Item1"]
    }, {
      tag: "li",
      attrs: {className: "item"},
      children: ["Item2"]
    }
  ]
} 

应用 snabbdom 实现 vdom

这是一个繁难的实现 vdom 性能的库,相比 vuereact,对于vdom 这块更加繁难,适宜咱们学习 vdomvdom 外面有两个外围的 api,一个是h 函数,一个是 patch 函数,前者用来生成 vdom 对象,后者的性能在于做虚构 dom 的比对和将 vdom 挂载到实在 DOM

简略介绍一下这两个函数的用法:

h('标签名', {属性}, [子元素])
h('标签名', {属性}, [文本])
patch(container, vnode) // container 为容器 DOM 元素
patch(vnode, newVnode)

当初咱们就来用 snabbdom 重写一下方才的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="container"></div>
  <button id="btn-change"> 扭转 </button>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
  <script>
    let snabbdom = window.snabbdom;

    // 定义 patch
    let patch = snabbdom.init([
      snabbdom_class,
      snabbdom_props,
      snabbdom_style,
      snabbdom_eventlisteners
    ]);

    // 定义 h
    let h = snabbdom.h;

    const data = [{
        name: "张三",
        age: "20",
        address: "北京"
      },
      {
        name: "李四",
        age: "21",
        address: "武汉"
      },
      {
        name: "王五",
        age: "22",
        address: "杭州"
      },
    ];
    data.unshift({name: "姓名", age: "年龄", address: "地址"});

    let container = document.getElementById('container');
    let vnode;
    const render = (data) => {let newVnode = h('table', {}, data.map(item => {let tds = [];
        for(let i in item) {if(item.hasOwnProperty(i)) {tds.push(h('td', {}, item[i] + ''));
          }
        }
        return h('tr', {}, tds);
      }));

      if(vnode) {patch(vnode, newVnode);
      } else {patch(container, newVnode);
      }
      vnode = newVnode;
    }

    render(data);

    let btnChnage = document.getElementById('btn-change');
    btnChnage.addEventListener('click', function() {data[1].age = 30;
      data[2].address = "深圳";
      //re-render
      render(data);
    })
  </script>
</body>
</html>

你会发现,只有扭转的栏目才闪动,也就是进行重绘,数据没有扭转的栏目还是放弃原样,这样就大大节俭了浏览器从新渲染的开销

vue 中应用 h 函数 生成虚构 DOM 返回

const vm = new Vue({
  el: '#app',
  data: {user: {name:'poetry'}
  },
  render(h){// h()
    // h(App)
    // h('div',[])
    let vnode = h('div',{},'hello world');
    return vnode
  }
});

</details>

双向绑定的原理是什么

咱们都晓得 Vue 是数据双向绑定的框架,双向绑定由三个重要局部形成

  • 数据层(Model):利用的数据及业务逻辑
  • 视图层(View):利用的展现成果,各类 UI 组件
  • 业务逻辑层(ViewModel):框架封装的外围,它负责将数据与视图关联起来

而下面的这个分层的架构计划,能够用一个专业术语进行称说:MVVM这里的管制层的外围性能便是“数据双向绑定”。天然,咱们只需弄懂它是什么,便能够进一步理解数据绑定的原理

了解 ViewModel

它的主要职责就是:

  • 数据变动后更新视图
  • 视图变动后更新数据

当然,它还有两个次要局部组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数

v-show 与 v-if 有什么区别?

v-if 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是 惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的“display”属性进行切换。

所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。

实现双向绑定

咱们还是以 Vue 为例,先来看看 Vue 中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对 data 执行响应化解决,这个过程产生 Observe
  2. 同时对模板执行编译,找到其中动静绑定的数据,从 data 中获取并初始化视图,这个过程产生在 Compile
  3. 同时定义⼀个更新函数和 Watcher,未来对应数据变动时Watcher 会调用更新函数
  4. 因为 data 的某个 key 在⼀个视图中可能呈现屡次,所以每个 key 都须要⼀个管家 Dep 来治理多个Watcher
  5. 未来 data 中数据⼀旦发生变化,会首先找到对应的 Dep,告诉所有Watcher 执行更新函数

流程图如下:

先来一个构造函数:执行初始化,对 data 执行响应化解决

class Vue {constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  

    // 对 data 选项做响应式解决  
    observe(this.$data);  

    // 代理 data 到 vm 上  
    proxy(this);  

    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

data 选项执行响应化具体操作

function observe(obj) {if (typeof obj !== "object" || obj == null) {return;}  
  new Observer(obj);  
}  

class Observer {constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {Object.keys(obj).forEach((key) => {defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

编译Compile

对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数

class Compile {constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取 dom  
    if (this.$el) {this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) {console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {return node.nodeType == 1;}  
  isInterpolation(node) {return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  

依赖收集

视图中会用到 data 中某 key,这称为依赖。同⼀个key 可能呈现屡次,每次都须要收集进去用⼀个 Watcher 来保护它们,此过程称为依赖收集多个 Watcher 须要⼀个 Dep 来治理,须要更新时由 Dep 统⼀告诉

实现思路

  1. defineReactive时为每⼀个 key 创立⼀个 Dep 实例
  2. 初始化视图时读取某个key,例如name1,创立⼀个watcher1
  3. 因为触发 name1getter办法,便将 watcher1 增加到 name1 对应的 Dep
  4. name1 更新,setter触发时,便可通过对应 Dep 告诉其治理所有 Watcher 更新
// 负责更新视图  
class Watcher {constructor(vm, key, updater) {  
    this.vm = vm  
    this.key = key  
    this.updaterFn = updater  

    // 创立实例时,把以后实例指定到 Dep.target 动态属性上  
    Dep.target = this  
    // 读一下 key,触发 get  
    vm[key]  
    // 置空  
    Dep.target = null  
  }  

  // 将来执行 dom 更新函数,由 dep 调用的  
  update() {this.updaterFn.call(this.vm, this.vm[this.key])  
  }  
}  

申明Dep

class Dep {constructor() {this.deps = [];  // 依赖治理  
  }  
  addDep(dep) {this.deps.push(dep);  
  }  
  notify() {this.deps.forEach((dep) => dep.update());  
  }  
} 

创立 watcher 时触发getter

class Watcher {constructor(vm, key, updateFn) {  
    Dep.target = this;  
    this.vm[this.key];  
    Dep.target = null;  
  }  
}  

依赖收集,创立 Dep 实例

function defineReactive(obj, key, val) {this.observe(val);  
  const dep = new Dep();  
  Object.defineProperty(obj, key, {get() {Dep.target && dep.addDep(Dep.target);// Dep.target 也就是 Watcher 实例  
      return val;  
    },  
    set(newVal) {if (newVal === val) return;  
      dep.notify(); // 告诉 dep 执行更新办法},  
  });  
}  

keep-alive 应用场景和原理

  • keep-aliveVue 内置的一个组件,能够实现组件缓存 ,当组件切换时不会对以后组件进行卸载。 个别联合路由和动静组件一起应用,用于缓存组件
  • 提供 includeexclude 属性,容许组件有条件的进行缓存。两者都反对字符串或正则表达式,include 示意只有名称匹配的组件会被缓存,exclude 示意任何名称匹配的组件都不会被缓存,其中 exclude 的优先级比 include
  • 对应两个钩子函数 activateddeactivated,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
  • keep-alive 的中还使用了 LRU(最近起码应用) 算法,抉择最近最久未应用的组件予以淘汰
  • <keep-alive></keep-alive> 包裹动静组件时,会缓存不流动的组件实例, 次要用于保留组件状态或防止从新渲染
  • 比方有一个列表和一个详情,那么用户就会常常执行关上详情 => 返回列表 => 关上详情…这样的话列表和详情都是一个频率很高的页面,那么就能够对列表组件应用 <keep-alive></keep-alive> 进行缓存,这样用户每次返回列表的时候,都能从缓存中疾速渲染,而不是从新渲染

对于 keep-alive 的根本用法

<keep-alive>
  <component :is="view"></component>
</keep-alive>

应用 includesexclude

<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (应用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (应用 `v-bind`) -->
<keep-alive :include="['a','b']">
  <component :is="view"></component>
</keep-alive>

匹配首先查看组件本身的 name 选项,如果 name 选项不可用,则匹配它的部分注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated

应用场景

应用准则:当咱们在某些场景下不须要让页面从新加载时咱们能够应用keepalive

举个栗子:

当咱们从 首页 –> 列表页 –> 商详页 –> 再返回,这时候列表页应该是须要keep-alive

首页 –> 列表页 –> 商详页 –> 返回到列表页 (须要缓存)–> 返回到首页 (须要缓存)–> 再次进入列表页(不须要缓存),这时候能够按需来管制页面的keep-alive

在路由中设置 keepAlive 属性判断是否须要缓存

{
  path: 'list',
  name: 'itemList', // 列表页
  component (resolve) {require(['@/pages/item/list'], resolve)
 },
 meta: {
  keepAlive: true,
  title: '列表页'
 }
}

应用<keep-alive>

<div id="app" class='wrapper'>
    <keep-alive>
        <!-- 须要缓存的视图组件 --> 
        <router-view v-if="$route.meta.keepAlive"></router-view>
     </keep-alive>
      <!-- 不须要缓存的视图组件 -->
     <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

思考题:缓存后如何获取数据

解决方案能够有以下两种:

  • beforeRouteEnter:每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
    next(vm=>{console.log(vm)
        // 每次进入路由执行
        vm.getData()  // 获取数据})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
// 留神:服务器端渲染期间 avtived 不被调用
activated(){this.getData() // 获取数据
},

扩大补充:LRU 算法是什么?

LRU 的核心思想是如果数据最近被拜访过,那么未来被拜访的几率也更高,所以咱们将命中缓存的组件 key 从新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据行将来被拜访几率越低,所以当缓存数量达到最大值时,咱们就删除未来被拜访几率最低的数据,即 this.keys 中第一个缓存的组件

相干代码

keep-alivevue 中内置的一个组件

源码地位:src/core/components/keep-alive.js

export default {
  name: "keep-alive",
  abstract: true, // 形象组件

  props: {
    include: patternTypes, // 要缓存的组件
    exclude: patternTypes, // 要排除的组件
    max: [String, Number], // 最大缓存数
  },

  created() {this.cache = Object.create(null); // 缓存对象  {a:vNode,b:vNode}
    this.keys = []; // 缓存组件的 key 汇合 [a,b]
  },

  destroyed() {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 动静监听 include  exclude
    this.$watch("include", (val) => {pruneCache(this, (name) => matches(val, name));
    });
    this.$watch("exclude", (val) => {pruneCache(this, (name) => !matches(val, name));
    });
  },

  render() {
    const slot = this.$slots.default; // 获取包裹的插槽默认值 获取默认插槽中的第一个组件节点
    const vnode: VNode = getFirstComponentChild(slot); // 获取第一个子组件
    // 获取该组件节点的 componentOptions
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // 获取该组件节点的名称,优先获取组件的 name 字段,如果 name 不存在则获取组件的 tag
      const name: ?string = getComponentName(componentOptions);
      const {include, exclude} = this;
      // 不走缓存 如果 name 不在 inlcude 中或者存在于 exlude 中则示意不缓存,间接返回 vnode
      if (
        // not included  不蕴含
        (include && (!name || !matches(include, name))) ||
        // excluded  排除外面
        (exclude && name && matches(exclude, name))
      ) {
        // 返回虚构节点
        return vnode;
      }

      const {cache, keys} = this;
      // 获取组件的 key 值
      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;
      // 拿到 key 值后去 this.cache 对象中去寻找是否有该值,如果有则示意该组件有缓存,即命中缓存
      if (cache[key]) {
        // 通过 key 找到缓存 获取实例
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key); // 通过 LRU 算法把数组外面的 key 删掉
        keys.push(key); // 把它放在数组开端
      } else {cache[key] = vnode; // 没找到就换存下来
        keys.push(key); // 把它放在数组开端
        // prune oldest entry  // 如果超过最大值就把数组第 0 项删掉
        if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }

      vnode.data.keepAlive = true; // 标记虚构节点曾经被缓存
    }
    // 返回虚构节点
    return vnode || (slot && slot[0]);
  },
};

能够看到该组件没有 template,而是用了render,在组件渲染的时候会主动执行render 函数

this.cache是一个对象,用来存储须要缓存的组件,它将以如下模式存储:

this.cache = {
  'key1':'组件 1',
  'key2':'组件 2',
  // ...
}

在组件销毁的时候执行 pruneCacheEntry 函数

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {const cached = cache[key]
  /* 判断以后没有处于被渲染状态的组件,将其销毁 */
  if (cached && (!current || cached.tag !== current.tag)) {cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

mounted 钩子函数中观测 includeexclude 的变动,如下:

mounted () {
  this.$watch('include', val => {pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))
  })
}

如果 includeexclude 产生了变动,即示意定义须要缓存的组件的规定或者不须要缓存的组件的规定产生了变动,那么就执行pruneCache 函数,函数如下

function pruneCache (keepAliveInstance, filter) {const { cache, keys, _vnode} = keepAliveInstance
  for (const key in cache) {const cachedNode = cache[key]
    if (cachedNode) {const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

在该函数内对 this.cache 对象进行遍历,取出每一项的 name 值,用其与新的缓存规定进行匹配,如果匹配不上,则示意在新的缓存规定下该组件曾经不须要被缓存,则调用 pruneCacheEntry 函数将其从 this.cache 对象剔除即可

对于 keep-alive 的最弱小缓存性能是在 render 函数中实现

首先获取组件的 key 值:

const key = vnode.key == null? 
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

拿到 key 值后去 this.cache 对象中去寻找是否有该值,如果有则示意该组件有缓存,即命中缓存,如下:

/* 如果命中缓存,则间接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {vnode.componentInstance = cache[key].componentInstance
    /* 调整该组件 key 的程序,将其从原来的中央删掉并从新放在最初一个 */
    remove(keys, key)
    keys.push(key)
} 

间接从缓存中拿 vnode 的组件实例,此时从新调整该组件 key 的程序,将其从原来的中央删掉并从新放在 this.keys 中最初一个

this.cache对象中没有该 key 值的状况,如下:

/* 如果没有命中缓存,则将其设置进缓存 */
else {cache[key] = vnode
    keys.push(key)
    /* 如果配置了 max 并且缓存的长度超过了 this.max,则从缓存中删除第一个 */
    if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}

表明该组件还没有被缓存过,则以该组件的 key 为键,组件 vnode 为值,将其存入 this.cache 中,并且把 key 存入 this.keys

此时再判断 this.keys 中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉

computed 和 watch 的区别和使用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值;

watch: 更多的是「察看」的作用,相似于某些数据的监听回调,每当监听的数据变动时都会执行回调进行后续操作;

使用场景:

  • 当咱们须要进行数值计算,并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时,都要从新计算;
  • 当咱们须要在数据变动时执行异步或开销较大的操作时,应该应用 watch,应用 watch 选项容许咱们执行异步操作 (拜访一个 API),限度咱们执行该操作的频率,并在咱们失去最终后果前,设置中间状态。这些都是计算属性无奈做到的。
退出移动版