写点什么

校招前端一面必会 vue 面试题指南

作者:bb_xiaxia1998
  • 2023-01-05
    浙江
  • 本文字数:12913 字

    阅读完需:约 42 分钟

写过自定义指令吗?原理是什么

回答范例


  1. Vue有一组默认指令,比如v-modelv-for,同时Vue也允许用户注册自定义指令来扩展 Vue 能力

  2. 自定义指令主要完成一些可复用低层级DOM操作

  3. 使用自定义指令分为定义、注册和使用三步:


  • 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted 和updated时执行

  • 注册自定义指令类似组件,可以使用app.directive()全局注册,使用{directives:{xxx}}局部注册

  • 使用时在注册名称前加上v-即可,比如v-focus


  1. 我在项目中常用到一些自定义指令,例如:


  • 复制粘贴 v-copy

  • 长按 v-longpress

  • 防抖 v-debounce

  • 图片懒加载 v-lazy

  • 按钮权限 v-premission

  • 页面水印 v-waterMarker

  • 拖拽指令 v-draggable


  1. vue3中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2之后,可以在setup中以一个小写v开头方便的定义自定义指令,更简单了

基本使用

当 Vue 中的核心内置指令不能够满足我们的需求时,我们可以定制自定义的指令用来满足开发的需求


我们看到的v-开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令


// 指令使用的几种方式://会实例化一个指令,但这个指令没有参数 `v-xxx`
// -- 将值传到指令中`v-xxx="value"`
// -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"``v-xxx="'string'"`
// -- 传参数(`arg`),如`v-bind:class="className"``v-xxx:arg="value"`
// -- 使用修饰符(`modifier`)`v-xxx:arg.modifier="value"`
复制代码


注册一个自定义指令有全局注册与局部注册


// 全局注册注册主要是用过Vue.directive方法进行注册// Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数// 注册一个全局自定义指令 `v-focus`Vue.directive('focus', {  // 当被绑定的元素插入到 DOM 中时……  inserted: function (el) {    // 聚焦元素    el.focus()  // 页面加载完成之后自动让输入框获取到焦点的小功能  }})
// 局部注册通过在组件options选项中设置directive属性directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能 } }}
// 然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<input v-focus />
复制代码


钩子函数


  1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。

  4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。

  5. unbind:只调用一次,指令与元素解绑时调用。


所有的钩子函数的参数都有以下:


  • el:指令所绑定的元素,可以用来直接操作 DOM

  • binding:一个对象,包含以下 property

  • name:指令名,不包括 v- 前缀。

  • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2

  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。

  • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"

  • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"

  • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

  • vnodeVue 编译生成的虚拟节点

  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用


除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行


<div v-demo="{ color: 'white', text: 'hello!' }"></div><script>    Vue.directive('demo', function (el, binding) {    console.log(binding.value.color) // "white"    console.log(binding.value.text)  // "hello!"    })</script>
复制代码


应用场景


使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:


  1. 防抖


// 1.设置v-throttle自定义指令Vue.directive('throttle', {  bind: (el, binding) => {    let throttleTime = binding.value; // 防抖时间    if (!throttleTime) { // 用户若不设置防抖时间,则默认2s      throttleTime = 2000;    }    let cbFun;    el.addEventListener('click', event => {      if (!cbFun) { // 第一次执行        cbFun = setTimeout(() => {          cbFun = null;        }, throttleTime);      } else {        event && event.stopImmediatePropagation();      }    }, true);  },});// 2.为button标签设置v-throttle自定义指令<button @click="sayHello" v-throttle>提交</button>
复制代码


  1. 图片懒加载


设置一个v-lazy自定义组件完成图片懒加载


const LazyLoad = {    // install方法    install(Vue,options){       // 代替图片的loading图        let defaultSrc = options.default;        Vue.directive('lazy',{            bind(el,binding){                LazyLoad.init(el,binding.value,defaultSrc);            },            inserted(el){                // 兼容处理                if('InterpObserver' in window){                    LazyLoad.observe(el);                }else{                    LazyLoad.listenerScroll(el);                }
}, }) }, // 初始化 init(el,val,def){ // src 储存真实src el.setAttribute('src',val); // 设置src为loading图 el.setAttribute('src',def); }, // 利用InterpObserver监听el observe(el){ let io = new InterpObserver(entries => { let realSrc = el.dataset.src; if(entries[0].isIntersecting){ if(realSrc){ el.src = realSrc; el.removeAttribute('src'); } } }); io.observe(el); }, // 监听scroll事件 listenerScroll(el){ let handler = LazyLoad.throttle(LazyLoad.load,300); LazyLoad.load(el); window.addEventListener('scroll',() => { handler(el); }); }, // 加载真实图片 load(el){ let windowHeight = document.documentElement.clientHeight let elTop = el.getBoundingClientRect().top; let elBtm = el.getBoundingClientRect().bottom; let realSrc = el.dataset.src; if(elTop - windowHeight<0&&elBtm > 0){ if(realSrc){ el.src = realSrc; el.removeAttribute('src'); } } }, // 节流 throttle(fn,delay){ let timer; let prevTime; return function(...args){ let currTime = Date.now(); let context = this; if(!prevTime) prevTime = currTime; clearTimeout(timer);
if(currTime - prevTime > delay){ prevTime = currTime; fn.apply(context,args); clearTimeout(timer); return; }
timer = setTimeout(function(){ prevTime = Date.now(); timer = null; fn.apply(context,args); },delay); } }
}export default LazyLoad;
复制代码


  1. 一键 Copy 的功能


import { Message } from 'ant-design-vue';
const vCopy = { // /* bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置 el: 作用的 dom 对象 value: 传给指令的值,也就是我们要 copy 的值 */ bind(el, { value }) { el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 el.handler = () => { if (!el.$value) { // 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意 Message.warning('无复制内容'); return; } // 动态创建 textarea 标签 const textarea = document.createElement('textarea'); // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 textarea.readOnly = 'readonly'; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; // 将要 copy 的值赋给 textarea 标签的 value 属性 textarea.value = el.$value; // 将 textarea 插入到 body 中 document.body.appendChild(textarea); // 选中值并复制 textarea.select(); // textarea.setSelectionRange(0, textarea.value.length); const result = document.execCommand('Copy'); if (result) { Message.success('复制成功'); } document.body.removeChild(textarea); }; // 绑定点击事件,就是所谓的一键 copy 啦 el.addEventListener('click', el.handler); }, // 当传进来的值更新的时候触发 componentUpdated(el, { value }) { el.$value = value; }, // 指令与元素解绑的时候,移除事件绑定 unbind(el) { el.removeEventListener('click', el.handler); },};
export default vCopy;
复制代码


  1. 拖拽


<div ref="a" id="bg" v-drag></div>
directives: { drag: { bind() {}, inserted(el) { el.onmousedown = (e) => { let x = e.clientX - el.offsetLeft; let y = e.clientY - el.offsetTop; document.onmousemove = (e) => { let xx = e.clientX - x + "px"; let yy = e.clientY - y + "px"; el.style.left = xx; el.style.top = yy; }; el.onmouseup = (e) => { document.onmousemove = null; }; }; }, }, }
复制代码

原理

  • 指令本质上是装饰器,是 vueHTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

  • 自定义指令有五个生命周期(也叫钩子函数),分别是 bindinsertedupdatecomponentUpdatedunbind


原理


  1. 在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性

  2. 通过 genDirectives 生成指令代码

  3. patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子

  4. 当执行指令对应钩子函数时,调用对应指令定义的方法

说一下 Vue 的生命周期

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是 Vue 的⽣命周期。


  1. beforeCreate(创建前):数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到 data、computed、watch、methods 上的方法和数据。

  2. created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。

  3. beforeMount(挂载前):在挂载开始之前被调用,相关的 render 函数首次被调用。实例已完成以下的配置:编译模板,把 data 里面的数据和模板生成 html。此时还没有挂载 html 到页面上。

  4. mounted(挂载后):在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的 html 内容替换 el 属性指向的 DOM 对象。完成模板中的 html 渲染到 html 页面中。此过程中进行 ajax 交互。

  5. beforeUpdate(更新前):响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。

  6. updated(更新后) :在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

  7. beforeDestroy(销毁前):实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。

  8. destroyed(销毁后):实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。


另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。

Vue 中 computed 和 watch 有什么区别?

计算属性 computed: (1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数; (2)计算属性内不支持异步操作; (3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法; (4)计算属性是自动监听依赖值的变化,从而动态返回内容。


侦听属性 watch: (1)不支持缓存,只要数据发生变化,就会执行侦听函数; (2)侦听属性内支持异步操作; (3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性; (3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情

v-if 和 v-show 的区别

  • 手段:v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐;

  • 编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换;

  • 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留;

  • 性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;

  • 使用场景:v-if 适合运营条件不大可能改变;v-show 适合频繁切换。

对 React 和 Vue 的理解,它们的异同

相似之处:


  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;

  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;

  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能;

  • 都有 props 的概念,允许组件间的数据传递;

  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。


不同之处 :


1)数据流


Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流


2)虚拟 DOM


Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。


  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。

  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。


3)组件化


React 与 Vue 最大的不同是模板的编写。


  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。

  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。


具体来讲:React 中 render 函数是支持闭包特性的,所以 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 4)监听数据变化的实现原理不同


  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能

  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。


5)高阶组件


react 可以通过高阶组件(HOC)来扩展,而 Vue 需要通过 mixins 来扩展。


高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不能采用 HOC 来实现。


6)构建工具


两者都有自己的构建工具:


  • React ==> Create React APP

  • Vue ==> vue-cli


7)跨平台


  • React ==> React Native

  • Vue ==> Weex

如何从真实 DOM 到虚拟 DOM

涉及到 Vue 中的模板编译原理,主要过程:


  1. 将模板转换成 ast 树, ast 用对象来描述真实的 JS 语法(将真实 DOM 转换成虚拟 DOM)

  2. 优化树

  3. ast 树生成代码


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

过滤器的作用,如何实现一个过滤器

根据过滤器的名称,过滤器是用来过滤数据的,在 Vue 中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。


使用场景:


  • 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。

  • 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用fliters过滤器来处理数据。


过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }}v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。


例如,在显示金额,给商品价格添加单位:


<li>商品价格:{{item.price | filterPrice}}</li>
filters: { filterPrice (price) { return price ? ('¥' + price) : '--' } }
复制代码

Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:


// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 需要进行功能拓展的方法const methodsToPatch = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse"];
/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组方法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组功能 const result = original.apply(this, args); // 响应式处理 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; });});
复制代码


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。

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

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


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

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

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


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

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

概念:


  • SPA 单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次 js、css 等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。

  • MPA 多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载 js、css 等相关资源。多页应用跳转,需要整页资源刷新。

说说 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

MVC 和 MVVM 区别

MVC


MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范


  • Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据

  • View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的

  • Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据


MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。


MVVM


MVVM 新增了 VM 类


  • ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。


MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应 Vue 数据驱动的思想)


整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性


注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明


那么问题来了 为什么官方要说 Vue 没有完全遵循 MVVM 思想呢?


  • 严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了 $refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

$nextTick 是什么?

Vue 实现响应式并不是在数据发生后立即更新 DOM,使用 vm.$nextTick 是在下次 DOM 更新循环结束之后立即执行延迟回调。在修改数据之后使用,则可以在回调中获取更新后的 DOM

那 vue 中是如何检测数组变化的呢?

数组就是使用 object.defineProperty 重新定义数组的每一项,那能引起数组变化的方法我们都是知道的, pop push shift unshift splice sort reverse 这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。


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

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


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

vue 如何监听对象或者数组某个属性的变化

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为 Object.defineProperty()限制,监听不到变化。


解决方式:


  • this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么 value)


this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
复制代码


  • 调用以下几个数组的方法


splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
复制代码


vue 源码里缓存了 array 的原型链,然后重写了这几个方法,触发这几个方法的时候会 observer 数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用 splice 方法会比较好自定义,因为 splice 可以在数组的任何位置进行删除/添加操作


vm.$set 的实现原理是:


  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

v-model 可以被用在自定义组件上吗?如果可以,如何使用?

可以。v-model 实际上是一个语法糖,如:


<input v-model="searchText">
复制代码


实际上相当于:


<input  v-bind:value="searchText"  v-on:input="searchText = $event.target.value">
复制代码


用在自定义组件上也是同理:


<custom-input v-model="searchText">
复制代码


相当于:


<custom-input  v-bind:value="searchText"  v-on:input="searchText = $event"></custom-input>
复制代码


显然,custom-input 与父组件的交互如下:


  1. 父组件将searchText变量传入 custom-input 组件,使用的 prop 名为value

  2. custom-input 组件向父组件传出名为input的事件,父组件将接收到的值赋值给searchText


所以,custom-input 组件的实现应该类似于这样:


Vue.component('custom-input', {  props: ['value'],  template: `    <input      v-bind:value="value"      v-on:input="$emit('input', $event.target.value)"    >  `})
复制代码

Vue 的基本原理

当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 使用 proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

描述下 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)使用案例


初级应用:


  • 鼠标聚焦

  • 下拉菜单

  • 相对时间转换

  • 滚动动画


高级应用:


  • 自定义指令实现图片懒加载

  • 自定义指令集成第三方插件

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 这种手动优化的生命周期.

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

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


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


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


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
校招前端一面必会vue面试题指南_Vue_bb_xiaxia1998_InfoQ写作社区