如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
思路分析
这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态API。
vuex需求分析回答范例
vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuexStore存储全局状态commit(type, payload), dispatch(type, payload)Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果实践
Store的实现:
class Store {constructor(options) {this.state = reactive(options.state)this.options = options}commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)}
}
vuex简易版
/*** 1 实现插件,挂载$store* 2 实现store*/let Vue;class Store {constructor(options) {// state响应式处理// 外部访问: this.$store.state.***// 第一种写法// this.state = new Vue({// data: options.state// })// 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更this._vm = new Vue({data: {$$state: options.state}})this._mutations = options.mutationsthis._actions = options.actionsthis.getters = {}options.getters && this.handleGetters(options.getters)this.commit = this.commit.bind(this)this.dispatch = this.dispatch.bind(this)}get state () {return this._vm._data.$$state}set state (val) {return new Error('Please use replaceState to reset state')}handleGetters (getters) {Object.keys(getters).map(key => {Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)})})}commit (type, payload) {let entry = this._mutations[type]if (!entry) {return new Error(`${type} is not defined`)}entry(this.state, payload)}dispatch (type, payload) {let entry = this._actions[type]if (!entry) {return new Error(`${type} is not defined`)}entry(this, payload)}
}const install = (_Vue) => {Vue = _VueVue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}},})
}export default { Store, install }
验证方式
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)export default new Vuex.Store({state: {counter: 0},mutations: {// state从哪里来的add (state) {state.counter++}},getters: {doubleCounter (state) {return state.counter * 2}},actions: {add ({ commit }) {setTimeout(() => {commit('add')}, 1000)}},modules: {}
})
(1)编码阶段
(2)SEO优化
(3)打包优化
(4)用户体验
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated , deactivated
参考:前端vue面试题详细解答
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
proxy基本用法
// proxy默认只会代理第一层对象,只有取值再次是对象的时候再次代理,不是一上来就代理,提高性能。不像vue2.x递归遍历每个对象属性
let handler = {set(target, key, value) {return Reflect.set(target, key, value);},get(target, key) {if (typeof target[key] == 'object' && target[key] !== null) {return new Proxy(target[key], handler); // 懒代理,只有取值再次是对象的时候再次代理,提高性能}return Reflect.get(target, key);}
}
let obj = { school: { name: 'poetry', age: 20 } };
let proxy = new Proxy(obj, handler);// 返回对象的代理
proxy.school
JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
nextTick 中的回调是在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOMDOM微任务优先的方式调用异步方法去执行 nextTick 包装的方法
nextTick方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。所以这个nextTick方法就是异步方法
根据执行环境分别尝试采用
PromisePromise不支持,再采用MutationObserverMutationObserver不支持,再采用setImmediatesetTimeoutflushCallbacks,把callbacks里面的数据依次执行
回答范例
nextTick 中的回调是在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick
开发时,有两个场景我们会用到nextTick
created中想要获取DOM时
响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度
nextTick签名如下:function nextTick(callback?: () => void): Promise所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事
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") {// 如果支持promiseconst 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") {// 如果前面都不支持 判断setImmediatetimerFunc = () => {setImmediate(flushCallbacks);};
} else {// 最后降级采用setTimeouttimerFunc = () => {setTimeout(flushCallbacks, 0);};
}export function nextTick(cb) {// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组callbacks.push(cb);if (!pending) {// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为falsepending = true;timerFunc();}
}
数据更新的时候内部会调用nextTick
// src/core/observer/scheduler.jsexport function queueWatcher (watcher: Watcher) {const id = watcher.idif (has[id] == null) {has[id] = trueif (!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 - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// queue the flushif (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()return}// 把更新方法放到数组中维护[nextTick回调函数,更新函数flushSchedulerQueue]/*** vm.a = 'test'; // 修改了数据后并不会马上更新视图vm.$nextTick(() => {// [fn,更新]console.log(vm.$el.innerHTML)})*/nextTick(flushSchedulerQueue)}}
}
编码优化:
keep-alive key 保证唯一性Vue加载性能优化
babel-plugin-component )用户体验
app-skeleton 骨架屏 shellap p壳 pwaSEO优化
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数据;
(1)自定义指令基本内容
全局定义:Vue.directive("focus",{})
局部定义:directives:{focus:{}}
钩子函数:指令定义对象提供钩子函数
o bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
o inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
o update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
o ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用。
o unbind:只调用一次,指令与元素解绑时调用。
钩子函数参数
o el:绑定元素
o bing: 指令核心对象,描述指令全部信息属性
o name
o value
o oldValue
o expression
o arg
o modifers
o vnode 虚拟节点
o oldVnode:上一个虚拟节点(更新钩子函数中才有用)
(2)使用场景
普通DOM元素进行底层操作的时候,可以使用自定义指令
自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。
(3)使用案例
初级应用:
高级应用:
.stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;.prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture :与事件冒泡的方向相反,事件捕获由外到内;.self :只会触发自己范围内的事件,不包含子元素;.once :只会触发一次。对于Computed:
对于Watch:
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
总结:
运用场景:
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
SPA( single-page application )仅在Web页面初始化时加载相应的HTML、JavaScript和CSS。一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变换,UI与用户的交互,避免页面的重新加载
优点:
SPA 相对对服务器压力小;缺点:
Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势单页应用与多页应用的区别
| 单页面应用(SPA) | 多页面应用(MPA) | |
|---|---|---|
| 组成 | 一个主页面和多个页面片段 | 多个主页面 |
| 刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |
| 数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
| 页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
| 维护成本 | 相对容易 | 相对复杂 |
实现一个SPA
hash变化驱动界面变化pushsate记录浏览器的历史,驱动界面发送变化
url中的hash来进行路由跳转// 定义 Router
class Router { constructor () { this.routes = {}; // 存放路由path及callback this.currentUrl = ''; // 监听路由change调用相对应的路由回调 window.addEventListener('load', this.refresh, false); window.addEventListener('hashchange', this.refresh, false); } route(path, callback){ this.routes[path] = callback; } push(path) { this.routes[path] && this.routes[path]() }
} // 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2')) miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
history 模式核心借用 HTML5 history api,api 提供了丰富的 router 相关属性先了解一个几个相关的apihistory.pushState 浏览器历史纪录添加记录history.replaceState修改浏览器历史纪录中当前纪录history.popState 当 history 发生变化时触发// 定义 Router
class Router { constructor () { this.routes = {}; this.listerPopState() } init(path) { history.replaceState({path: path}, null, path); this.routes[path] && this.routes[path](); } route(path, callback){ this.routes[path] = callback; } push(path) { history.pushState({path: path}, null, path); this.routes[path] && this.routes[path](); } listerPopState () { window.addEventListener('popstate' , e => { const path = e.state && e.state.path; this.routers[path] && this.routers[path]() }) }
} // 使用 Router window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2')) // 跳转
miniRouter.push('/page2') // page2
题外话:如何给SPA做SEO
将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js
目前主流的静态化主要有两种:
一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
Phantomjs针对爬虫处理原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图

优点:
缺点:
什么时候被调用?
watch/event 事件回调。无 $el . render 函数首次被调用vm.$el替换,并挂载到实例上去之后调用改钩子。每个生命周期内部可以做什么?
ajax放在哪个生命周期?:一般放在 mounted 中,保证逻辑统一性,因为生命周期是同步执行的, ajax 是异步执行的。单数服务端渲染 ssr 同一放在 created 中,因为服务端渲染不支持 mounted 方法。 什么时候使用beforeDestroy?:当前页面使用 $on ,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove 。
Vue2: Object.defineProperty 重新定义 data 中所有的属性, Object.defineProperty 可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。
具体的过程:首先Vue使用 initData 初始化用户传入的参数,然后使用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineeReactive 循环对象属性定义响应式变化,核心就是使用 Object.defineProperty 重新定义数据。
虚拟 DOM 的实现原理主要包括以下 3 部分:
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive 方法,使用 Object.defineProperty 来劫持各个属性的 setter、getter(只会劫持已经存在的属性),数组则是通过重写数组7个方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)
Object.defineProperty基本使用
function observer(value) { // proxy reflectif (typeof value === 'object' && typeof value !== null)for (let key in value) {defineReactive(value, key, value[key]);}
}function defineReactive(obj, key, value) {observer(value);Object.defineProperty(obj, key, {get() { // 收集对应的key 在哪个方法(组件)中被使用return value;},set(newValue) {if (newValue !== value) {observer(newValue);value = newValue; // 让key对应的方法(组件重新渲染)重新执行}}})
}
let obj1 = { school: { name: 'poetry', age: 20 } };
observer(obj1);
console.log(obj1)
源码分析

class Observer {// 观测值constructor(value) {this.walk(value);}walk(data) {// 对象上的所有属性依次进行观测let keys = Object.keys(data);for (let i = 0; i < keys.length; i++) {let key = keys[i];let value = data[key];defineReactive(data, key, value);}}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {observe(value); // 递归关键// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止// 思考?如果Vue数据嵌套层级过深 >>性能会受影响Object.defineProperty(data, key, {get() {console.log("获取值");//需要做依赖收集过程 这里代码没写出来return value;},set(newValue) {if (newValue === value) return;console.log("设置值");//需要做派发更新过程 这里代码没写出来value = newValue;},});
}
export function observe(value) {// 如果传过来的是对象或者数组 进行属性劫持if (Object.prototype.toString.call(value) === "[object Object]" ||Array.isArray(value)) {return new Observer(value);}
}
说一说你对vue响应式理解回答范例
MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度vue2中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题vue3重新编写了这一部分的实现:利用ES6的Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了优点:
缺点: