写点什么

常见经典 vue 面试题(面试必问)

作者:bb_xiaxia1998
  • 2022-12-14
    浙江
  • 本文字数:14509 字

    阅读完需:约 48 分钟

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 的构建和维护的成本都会⽐较⾼。

Vue-router 除了 router-link 怎么实现跳转

声明式导航


<router-link to="/about">Go to About</router-link>
复制代码


编程式导航


// literal string pathrouter.push('/users/1')// object with pathrouter.push({ path: '/users/1' })// named route with params to let the router build the urlrouter.push({ name: 'user', params: { username: 'test' } })
复制代码


回答范例


  • vue-router导航有两种方式:声明式导航和编程方式导航

  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息

  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个 a 标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航

  • 实际上内部两者调用的导航函数是一样的

v-for 为什么要加 key

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


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


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

为什么 Vue 采用异步渲染呢?

Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick


dep.notify() 通知 watcher 进行更新, subs[i].update 依次调用 watcher 的 update queueWatcher 将 watcher 去重放入队列, nextTick( flushSchedulerQueue )在下一 tick 中刷新 watcher 队列(异步)。

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。


其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

vue 初始化页面闪动问题

使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。


首先:在 css 里加上以下代码:


[v-cloak] {    display: none;}
复制代码


如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"


参考 前端进阶面试题详细解答

computed 和 watch 区别

  1. 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed


Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理



<template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',        }    },    computed:{        fullName: function(){            return this.firstName + ' ' + this.lastName        }    }}
复制代码


  1. watch用于观察和监听页面上的 vue 实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择


Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销



<template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',            fullName: 'zhang san'        }    },    watch:{        firstName(val) {            this.fullName = val + ' ' + this.lastName        },        lastName(val) {            this.fullName = this.firstName + ' ' + val        }    }}
复制代码


computed:


  • computed是计算属性,也就是计算值,它更多用于计算值的场景

  • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算

  • computed适用于计算比较消耗性能的计算场景


watch:


  • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作

  • 无缓存性,页面重新渲染时值不变化也会执行


小结:


  • computedwatch都是基于watcher来实现的

  • computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行

  • watch是监控值的变化,当值发生变化时调用其对应的回调函数

  • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed

  • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化


回答范例


思路分析


  • 先看computed, watch两者定义,列举使用上的差异

  • 列举使用场景上的差异,如何选择

  • 使用细节、注意事项

  • vue3变化


computed特点:具有响应式的返回值


const count = ref(1)const plusOne = computed(() => count.value + 1)
复制代码


watch特点:侦测变化,执行回调


const state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })
复制代码


回答范例


  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computedmethods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch 没有返回值,但可以执行异步操作等复杂逻辑

  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的 DOM 操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.

  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deepimmediate等选项

  4. vue3watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watchwatchEffect可以完全替代目前的watch选项,且功能更加强大


基本使用


// src/core/observer:45;
// 渲染watcher / computed watcher / watchconst vm = new Vue({ el: '#app', data: { firstname:'张', lastname:'三' }, computed:{ // watcher => firstname lastname // computed 只有取值时才执行
// Object.defineProperty .get fullName(){ // firstName lastName 会收集fullName计算属性 return this.firstname + this.lastname } }, watch:{ firstname(newVal,oldVal){ console.log(newVal) } }});
setTimeout(() => { debugger; vm.firstname = '赵'}, 1000);
复制代码


相关源码


// 初始化statefunction initState (vm: Component) {  vm._watchers = []  const opts = vm.$options  if (opts.props) initProps(vm, opts.props)  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    initData(vm)  } else {    observe(vm._data = {}, true /* asRootData */)  }
// 初始化计算属性 if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }}
// 计算属性取值函数function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值 watcher.evaluate(); // this.firstname lastname } if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher watcher.depend() } return watcher.value } }}
// watch的实现Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this debugger; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() }}
复制代码


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 重写。

vue3 中 watch、watchEffect 区别

  • watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect不同,每次代码加载watchEffect都会执行(忽略watch第三个参数的配置,如果修改配置项也可以实现立即执行)

  • watch需要传递监听的对象,watchEffect不需要

  • watch只能监听响应式数据:ref定义的属性和reactive定义的对象,如果直接监听reactive定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getter

  • watchEffect如果监听reactive定义的对象是不起作用的,只能监听对象中的属性


看一下watchEffect的代码


<template><div>  请输入firstName:  <input type="text" v-model="firstName"></div><div>  请输入lastName:  <input type="text" v-model="lastName"></div><div>  请输入obj.text:  <input type="text" v-model="obj.text"></div> <div> 【obj.text】 {{obj.text}} </div></template>
<script>import {ref, reactive, watch, watchEffect} from 'vue'export default { name: "HelloWorld", props: { msg: String, }, setup(props,content){ let firstName = ref('') let lastName = ref('') let obj= reactive({ text:'hello' }) watchEffect(()=>{ console.log('触发了watchEffect'); console.log(`组合后的名称为:${firstName.value}${lastName.value}`) }) return{ obj, firstName, lastName } }};</script>
复制代码




改造一下代码


watchEffect(()=>{  console.log('触发了watchEffect');  // 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论  console.log(`组合后的名称为:${firstName}${lastName}`)})
复制代码



watchEffect(()=>{  console.log('触发了watchEffect');  console.log(obj);})
复制代码



稍微改造一下


let obj = reactive({  text:'hello'})watchEffect(()=>{  console.log('触发了watchEffect');  console.log(obj.text);})
复制代码



再看一下 watch 的代码,验证一下


let obj= reactive({  text:'hello'})// watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行watch(obj, (newValue, oldValue) => {  // 回调函数  console.log('触发监控更新了new',  newValue);  console.log('触发监控更新了old',  oldValue);},{  // 配置immediate参数,立即执行,以及深层次监听  immediate: true,  deep: true})
复制代码



  • 监控整个reactive对象,从上面的图可以看到 deep 实际默认是开启的,就算我们设置为false也还是无效。而且旧值获取不到。

  • 要获取旧值则需要监控对象的属性,也就是监听一个getter,看下图




总结


  • 如果定义了reactive的数据,想去使用watch监听数据改变,则无法正确获取旧值,并且deep属性配置无效,自动强制开启了深层次监听。

  • 如果使用 ref 初始化一个对象或者数组类型的数据,会被自动转成reactive的实现方式,生成proxy代理对象。也会变得无法正确取旧值。

  • 用任何方式生成的数据,如果接收的变量是一个proxy代理对象,就都会导致watch这个对象时,watch回调里无法正确获取旧值。

  • 所以当大家使用watch监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行


watch 和 watchEffect 异同总结


体验


watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数


const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0count.value++// -> logs 1
复制代码


watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数


const state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })
复制代码


回答范例


  1. watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数

  2. watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch

  3. watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项

  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})


watchEffect定义如下


export function watchEffect(  effect: WatchEffect,  options?: WatchOptionsBase): WatchStopHandle {  return doWatch(effect, null, options)}
复制代码


watch定义如下


export function watch<T = any, Immediate extends Readonly<boolean> = false>(  source: T | WatchSource<T>,  cb: any,  options?: WatchOptions<Immediate>): WatchStopHandle {  return doWatch(source as any, cb, options)}
复制代码


很明显watchEffect就是一种特殊的watch实现。

computed 的实现原理

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


computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。


其内部通过 this.dirty 属性标记计算属性是否需要重新求值。


当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,


computed watcher 通过 this.dep.subs.length 判断有没有订阅者,


有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)


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

父子组件生命周期调用顺序(简单)

渲染顺序:先父后子,完成顺序:先子后父


更新顺序:父更新导致子更新,子更新完成后父


销毁顺序:先父后子,完成顺序:先子后父

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),源码如下:


function createKeyToOldIdx(children, beginIdx, endIdx) {  let i, key;  const map = {};  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key;    if (isDef(key)) map[key] = i;  }  return map;}
复制代码

谈谈你对 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,返回给爬虫。下面是大致流程图


vue 中使用了哪些设计模式

1.工厂模式 - 传入参数即可创建实例


虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode


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


vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉


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


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


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


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


...其他模式欢迎补充

Vue 中如何检测数组变化

前言


Vue 不能检测到以下数组的变动:


  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

  • 当你修改数组的长度时,例如:vm.items.length = newLength


Vue 提供了以下操作方法


// Vue.setVue.set(vm.items, indexOfItem, newValue)// vm.$set,Vue.set的一个别名vm.$set(vm.items, indexOfItem, newValue)// Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue)
复制代码


分析


数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)


所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新


  • 用函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新

  • 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)


原理


Vuedata 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api 时,可以通知依赖更新,如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。



手写简版分析


let oldArray = Object.create(Array.prototype);['shift', 'unshift', 'push', 'pop', 'reverse','sort'].forEach(method => {    oldArray[method] = function() { // 这里可以触发页面更新逻辑        console.log('method', method)        Array.prototype[method].call(this,...arguments);    }});let arr = [1,2,3];arr.__proto__ = oldArray;arr.unshift(4);
复制代码


源码分析


// 拿到数组原型拷贝一份const arrayProto = Array.prototype // 然后将arrayMethods继承自数组原型// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
methodsToPatch.forEach(function (method) { // 重写原型方法 const original = arrayProto[method] // 调用原数组的方法
def(arrayMethods, method, function mutator (...args) { // 这里保留原型方法的执行结果 const result = original.apply(this, args) // 这句话是关键 // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例 const ob = this.__ob__
// 这里的标志就是代表数组有新增操作 let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测 if (inserted) ob.observeArray(inserted)
ob.dep.notify() // 当调用数组方法后,手动通知视图更新
return result }) })
this.observeArray(value) // 进行深度监控
复制代码


vue3:改用 proxy ,可直接监听对象数组的变化

说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势

Object.defineProperty() 的问题主要有三个:


  • 不能监听数组的变化 :无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应

  • 必须遍历对象的每个属性 :只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象

  • 必须深层遍历嵌套的对象


Proxy 的优势如下:


  • 针对对象: 针对整个对象,而不是对象的某个属性 ,所以也就不需要对 keys 进行遍历

  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的

  • Proxy的第二个参数可以有 13 种拦截方:不限于applyownKeysdeletePropertyhas等等是Object.defineProperty不具备的

  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改

  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利


proxy详细使用点击查看(opens new window)


Object.defineProperty 的优势如下:


兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平


defineProperty 的属性值有哪些


Object.defineProperty(obj, prop, descriptor)
// obj 要定义属性的对象// prop 要定义或修改的属性的名称// descriptor 要定义或修改的属性描述符
Object.defineProperty(obj,"name",{ value:"poetry", // 初始值 writable:true, // 该属性是否可写入 enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等) configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改 get: function() {}, set: function(newVal) {}})
复制代码


相关代码如下


import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑import { isObject } from "./util"; // 工具方法
export function reactive(target) { // 根据不同参数创建不同响应式对象 return createReactiveObject(target, mutableHandlers);}function createReactiveObject(target, baseHandler) { if (!isObject(target)) { return target; } const observed = new Proxy(target, baseHandler); return observed;}
const get = createGetter();const set = createSetter();
function createGetter() { return function get(target, key, receiver) { // 对获取的值进行放射 const res = Reflect.get(target, key, receiver); console.log("属性获取", key); if (isObject(res)) { // 如果获取的值是对象类型,则返回当前对象的代理对象 return reactive(res); } return res; };}function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); if (!hadKey) { console.log("属性新增", key, value); } else if (hasChanged(value, oldValue)) { console.log("属性值被修改", key, value); } return result; };}export const mutableHandlers = { get, // 当获取属性时调用此方法 set, // 当修改属性时调用此方法};
复制代码


Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?


判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。


监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢?


我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

为什么 vue 组件中 data 必须是一个函数?

对象为引用类型,当复用组件时,由于数据对象都指向同一个 data 对象,当在一个组件中修改 data 时,其他重用的组件中的 data 会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object 的实例),引用地址不同,则不会出现这个问题。

Vue.js 的 template 编译

简而言之,就是先转化成 AST 树,再得到的 render 函数返回 VNode(Vue 的虚拟 DOM 节点),详细步骤如下:


首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。


然后,AST 会经过 generate(将 AST 语法树转化成 render funtion 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有(标签名、子节点、文本等等)

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 ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。


相关代码如下


import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑import { isObject } from "./util"; // 工具方法
export function reactive(target) { // 根据不同参数创建不同响应式对象 return createReactiveObject(target, mutableHandlers);}function createReactiveObject(target, baseHandler) { if (!isObject(target)) { return target; } const observed = new Proxy(target, baseHandler); return observed;}
const get = createGetter();const set = createSetter();
function createGetter() { return function get(target, key, receiver) { // 对获取的值进行放射 const res = Reflect.get(target, key, receiver); console.log("属性获取", key); if (isObject(res)) { // 如果获取的值是对象类型,则返回当前对象的代理对象 return reactive(res); } return res; };}function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); if (!hadKey) { console.log("属性新增", key, value); } else if (hasChanged(value, oldValue)) { console.log("属性值被修改", key, value); } return result; };}export const mutableHandlers = { get, // 当获取属性时调用此方法 set, // 当修改属性时调用此方法};
复制代码


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
常见经典vue面试题(面试必问)_Vue_bb_xiaxia1998_InfoQ写作社区