写点什么

最近面试被问到的 vue 题

作者:bb_xiaxia1998
  • 2022-11-16
    浙江
  • 本文字数:12368 字

    阅读完需:约 41 分钟

v-for 为什么要加 key

如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速


更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。


更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

如果让你从零开始写一个 vuex,说说你的思路

思路分析


这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态 API。


  • vuex需求分析

  • 如何实现这些需求


回答范例


  1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex


  • 要实现一个Store存储全局状态

  • 要提供修改状态所需 API:commit(type, payload), dispatch(type, payload)


  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commitdispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件

  2. 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.mutations this._actions = options.actions this.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 = _Vue
Vue.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.$storeVue.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: { }})
复制代码

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 压缩等。

谈谈对 keep-alive 的了解

keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated deactivated


参考:前端vue面试题详细解答

created 和 mounted 的区别

  • created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。

  • mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。

Vue3.x 响应式数据原理

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
复制代码

data 为什么是一个函数而不是对象

JavaScript 中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。


而在 Vue 中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。


所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

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


  • created中想要获取DOM

  • 响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度


  1. 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/nextTicklet 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 性能优化

编码优化


  • 事件代理

  • keep-alive

  • 拆分组件

  • key 保证唯一性

  • 路由懒加载、异步组件

  • 防抖节流


Vue 加载性能优化


  • 第三方模块按需导入( babel-plugin-component

  • 图片懒加载


用户体验


  • app-skeleton 骨架屏

  • shellap p 壳

  • pwa


SEO 优化


  • 预渲染

描述下 Vue 自定义指令

在 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 的区别

对于 Computed:


  • 它支持缓存,只有依赖的数据发生了变化,才会重新计算

  • 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化

  • computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。

  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed

  • 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。


对于 Watch:


  • 它不支持缓存,数据变化时,它就会触发相应的操作

  • 支持异步监听

  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值

  • 当一个属性发生变化时,就需要执行相应的操作

  • 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:

  • immediate:组件加载立即触发回调函数

  • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。


当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。


总结:


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

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


运用场景:


  • 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。

  • 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

slot 是什么?有什么作用?原理是什么?

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。


  • 默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。

  • 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。

  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。


实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

谈谈你对 SPA 单页面的理解

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTMLJavaScriptCSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载


优点:


  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

  • 基于上面一点,SPA 相对对服务器压力小;

  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理


缺点:


  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScriptCSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势


单页应用与多页应用的区别



实现一个 SPA


  • 监听地址栏中hash变化驱动界面变化

  • pushsate记录浏览器的历史,驱动界面发送变化



  1. hash 模式 :核心通过监听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
复制代码


  1. history 模式history 模式核心借用 HTML5 history apiapi 提供了丰富的 router 相关属性先了解一个几个相关的 api


  • history.pushState 浏览器历史纪录添加记录

  • history.replaceState修改浏览器历史纪录中当前纪录

  • history.popStatehistory 发生变化时触发


// 定义 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


  1. SSR 服务端渲染


将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js


  1. 静态化


目前主流的静态化主要有两种:


  • 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中

  • 另外一种是通过 WEB 服务器的 URL Rewrite的方式,它的原理是通过 web 服务器内部模块按一定规则将外部的 URL 请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现 URL 静态化的效果


  1. 使用Phantomjs针对爬虫处理


原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图


虚拟 DOM 的优劣如何?

优点:


  • 保证性能下限: 虚拟 DOM 可以经过 diff 找出最小差异,然后批量进行 patch,这种操作虽然比不上手动优化,但是比起粗暴的 DOM 操作性能要好很多,因此虚拟 DOM 可以保证性能下限

  • 无需手动操作 DOM: 虚拟 DOM 的 diff 和 patch 都是在一次更新中自动进行的,我们无需手动操作 DOM,极大提高开发效率

  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等


缺点:


  • 无法进行极致优化: 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,比如 VScode 采用直接手动操作 DOM 的方式进行极端的性能优化

说说 Vue 的生命周期吧

什么时候被调用?


  • beforeCreate :实例初始化之后,数据观测之前调用

  • created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、 watch/event 事件回调。无 $el .

  • beforeMount:在挂载之前调用,相关 render 函数首次被调用

  • mounted:了被新创建的vm.$el替换,并挂载到实例上去之后调用改钩子。

  • beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。

  • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。

  • beforeDestroy:实例销毁前调用,实例仍然可用。

  • destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除


每个生命周期内部可以做什么?


  • created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。

  • mounted:实例已经挂载完成,可以进行一些 DOM 操作。

  • beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。

  • updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。

  • destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。


ajax 放在哪个生命周期?:一般放在 mounted 中,保证逻辑统一性,因为生命周期是同步执行的, ajax 是异步执行的。单数服务端渲染 ssr 同一放在 created 中,因为服务端渲染不支持 mounted 方法。 什么时候使用 beforeDestroy?:当前页面使用 $on ,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove

vue 是如何实现响应式数据的呢?(响应式数据原理)

Vue2: Object.defineProperty 重新定义 data 中所有的属性, Object.defineProperty 可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。


具体的过程:首先 Vue 使用 initData 初始化用户传入的参数,然后使用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineeReactive 循环对象属性定义响应式变化,核心就是使用 Object.defineProperty 重新定义数据。

虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:


  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;

  • diff 算法 — 比较两棵虚拟 DOM 树的差异;

  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

Vue2.x 响应式数据原理

整体思路是数据劫持+观察者模式


对象内部通过 defineReactive 方法,使用 Object.defineProperty 来劫持各个属性的 settergetter(只会劫持已经存在的属性),数组则是通过重写数组7个方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)


Object.defineProperty 基本使用


function observer(value) { // proxy reflect    if (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为例说明,通过数据响应式加上虚拟DOMpatch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的 DOM 操作,从而大大提升开发效率,降低开发难度

  • vue2中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的 7 个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的MapSet这些数据结构不支持等问题

  • 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了

MVVM 的优缺点?

优点:


  • 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的"View"上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel⾥⾯,让很多 view 重⽤这段视图逻辑

  • 提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码

  • ⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放


缺点:


  • Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的

  • ⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存

  • 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼。


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
最近面试被问到的vue题_Vue_bb_xiaxia1998_InfoQ写作社区