vue面试和核心原理

160 阅读7分钟

谈谈你对MVVM的理解?

  • MVVM 分别是model viewModel view 目的是为了实现分层的。 借助了后台的分层思想来实现代码的划分。 前端借助了后端mvc但是发现所有的逻辑都放在controller这一层,逻辑非常臃肿。难以维护。 隐藏controller这一层,MVVM模式就是可以直接将数据映射到视图上,同样可以自动监控视图的变化,视图变化后可以更新数据 。vue 里面提供了一个指令可以实现双向绑定 v-model

请说一下Vue2Vue3响应式数据的理解

  • 什么叫响应式数据,可以拦截用户的获取数据的操作和设置数据的操作
  • vue2中响应式原理 Object.defineProperty来实现的 (缺点就是需要将整个对象递归的增加get和set针对的属性)。 vue中对象采用了defineProperty,数组并没有采用(重写数组的7个方法),因为可能会有性能问题.
  • Vue3采用了proxy proxy针对的是对象,而且不用重写某个属性 性能高 (缺点兼容性不好) (在vue3中如果你采用老的data的写法,那么无论是对象还是数组都会采用defineProperty,为了兼容vue2的写法,但是没有全部拿过来)

vue2中核心定义响应式的api Vue.util.defineReactive(内部采用了递归,如果属性不可被配置那么不能增加get和set)

Vue中如何检测数组变化?

  • Vue2中采用的是重写数组的方法 通过创建一个对象,实现原型链继承。在对象身上重写了7个方法,如果在vue中定义了数组对象,我会让这个数组通过链找到我们饿创建的这个对象。 用户在数组上调用方法会触发我们重写的方法。就可以监控到数组的变化 缺陷就是没有监控数组的索引,也没有监控length属性 在vue中改变索引和长度是无法实现响应式更新页面的
  • Vue3中的proxy天生就支持数组的拦截,所以不会出现这个问题

Vue中如何进行依赖收集?

我们希望数据变化后就可以更新视图,我们视图在渲染的时候会创建所谓的渲染watcher。 每个组件都有一个渲染watcher, 可以用来渲染页面(渲染页面就会去取数据,调用get方法)除了渲染watcher之外还有计算属性watcher和用户watcher (无论是哪种watcher在创建watcher后都会发生取值操作)。 取值的时候就可以收集依赖(每个属性和对象都会收集当前的watcher), 我们会给对象和属性都增加一个dep属性来收集watcher,watcher和dep的关系是多对多的 一个页面中有多个属性 一个watcher对应多个dep , 一个属性可以对应多个视图 所以一个dep 可以存多个watcher。 这种依赖收集方式主要借助js单线程的特性

稍后用户更改对应的属性或者调用VUe.$set 都会通过当前的dep找到记录的watcher依次执行

如何理解Vue中模板编译原理

  • 1.将模板编译成ast语法树 parser 栈的使用确定父子关系
  • 2.静态优化 优化不会变的内容,在做diff算法的时候可以跳过被标记成静态节点的元素 optimize 树的遍历
  • 3.将ast对应的内容重新拼接成对应的js代码 codeGen new Function + with

因为不能直接做字符串的diff算法,我们需要根据模板产生前后的虚拟节点来实现diff算法

Vue生命周期钩子是如何实现的

  • Vue在内部会把用户的选项进行合并处理 mergeOptions 针对不同的字段来进行合并, 其中就包含生命周期hook, 我们会把合并的同名钩子维护成一个数组(一个钩子也是数组) 最后当初始化话的时候会将数组中的每一个钩子依次执行.

Vue的生命周期方法有哪些?一般在哪一步发送请求及原因

  • 在哪里发请求都可以但是一般我们发请求主要在两个方法 created, mounted? 请求操作都是异步的,可能在created会提前被发送,但是最后回来都是异步的。 如果在做vue服务端渲染的时候会有不同的选择,服务端渲染指的是在服务器中把我们的vue应用渲染成一个字符串,服务端没有浏览器,所以不能触发mounted方法,此时如果想统一我们可以把请求放在created上(在哪里发请求取决于干什么事)

Vue.mixin的使用场景和原理

  • 核心在于就是抽离公共逻辑, vuex和vue-router 给每个组件都增添一个storestore router 这时候就可以使用mixin , 还有一些公共方法 都可以放在mixin里面 , 开可以通过mixin属性来注入。 缺陷数据来源不明确,而且会产生命名冲突。
  • 在React中最早都采用的是高阶组件,hook
  • Vue3 来说 就是compositionApi 组合式api (组合由于继承) Vue3中依旧可以使用mixin

Vue组件data为什么必须是个函数?

因为我们的组件的数据必须保持独立,我们在使用组件的时候 都是采用new 同一个构造函数,如果构造函数中存的是一个对象数据源,那么所有的组件都会使用这个对象,为了解决这个问题我们需要把data定义成一个函数,这样每个组件在初始化的时候都可以生成一个全新的data

nextTick在哪里使用?原理是?

每次数据变化后会内部调用nextTick将更新操作延迟,将用户的操作延迟到页面更新之后。 内部使用了批处理的方案将用户的回调和内部更新维护到了同一个队列中 [内部更新的回调,用户自己的写的回调], 稍后会开启一个异步任务 (微任务 宏任务),之后批量执行代码. 内部采用的是promise.then, mutationObserver,setImmediate,setTimeout (在vue3中不考虑兼容性 所以内部实现就是promise.then)

computed和watch区别

  • computed 我们叫他计算属性, 他要根据其他数据计算出来。 计算属性就是一个defineProperty,但是为了实现依赖的值不发生变化就不更新计算属性的值,内部将计算属性也包装成了一个watcher,lazy:true. 计算属性不会立即执行,当我们取值的时候才会执行. 并且有一个标识,标识叫做dirty:true,如果dirty为true的时候,会重新计算,如果没有变化dirty:false,否则会采用上次的计算结果。 如果依赖的值变化了会重新更新dirty值 (防抖操作)
  • watch方法 就是针对一个属性创建一个watcher, 属性的值发生变化了 就调用用户对应的回调方法

Vue.set方法是如何实现的

默认给响应式对象新增属性不会触发页面更新,为了实现新增的属性可以触发页面更新我们就可以采用Vue.set方法来实现 , 默认我们针对对象采用的是defineProperty(属性),所以后加的属性不具备getter和setter, 针对数组数组只是重写7个方法,并没有 针对索引添加进行更新操作.

  • 针对对象采用的是defineReactive 将对象新增的属性变成响应式的 (对象本身有dep属性),所以需要触发对象被身存储的watcher来实现重新渲染逻辑
  • 针对数组 直接调用splice方法即可实现 ,数据变化和更新视图
  • Vue.util.defineReactive() 不管这个对象是不是响应式都会帮你用defineProperty, 如果是set方法必须要求增添属性的对象是响应式

Vue为什么需要虚拟DOM

vue会将模板编译成render函数,render函数返回的就是虚拟节点 (数据变化后渲染模板,字符串的比较)。我们要实现只更新差异化的内容,需要只对动态节点来进行比较 diff算法 (好处可以类似于缓存,将差异记录到虚拟节点上,之后更新dom) 同时防止了用户恶劣修改dom元素 操作dom元素是非常浪费性能, 虚拟dom可以实现跨平台。 如果没有虚拟dom,那么无法实现跨平台。runtime -》 针对不同平台的代码 虚拟就是一个对象来描述dom元素的

Vuediff算法原理

  • 比较两个节点的差异,是采用平级比对,主要就是比对标签名和key属性
  • patchVnode 会先比较两个节点,如果两个节点不是同一个节点就创建一个新的节点替换掉老的
    • 如果两个节点是同一个节点则比对两个节点属性 复用老节点
    • 比对完自己比较儿子: 一方有儿子一方没儿子 , 两方都有儿子的情况
    • 采用头头比较 尾尾比较 交叉比对 (做优化的)
    • 乱序比对。 内部会采用双指针的方式进行比对操作 (Vue3 采用的是最长递增子序列优化了diff算法) 在比对的过程中vue2 会跳过静态节点

既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异

  • 如果不用diff算法相当于给每个属性都增添了watcher, 属性变化后通知watcher更新, 如果页面属性过多,那么watcher就多, 粒度太细 不好维护了. vue2 采用了 watcher 配合 组件级更新+ diff算法的方式将粒度放大,从而减少了watcher的个数

请说明Vue中key的作用和原理,谈谈你对它的理解

vue中更新靠的是key来实现的,如果相同的key会认为是同一个元素,vue会尽可能的复用。 在渲染列表时需要使用key保证key是唯一的,这样在更改列表时不会导致意想不到的问题 A 0 E 0 B 1 A 1
C 2 B 2 D 3 c 3

谈一谈对Vue组件化的理解

vue组件化的目的 , 为了能实现组件化更新,每个组件都有一个watcher. 可以实现组件的复用 维护起来比较方便

组件不是静态节点

Vue的组件渲染流程

组件的初始化会先创建组件的虚拟节点 Vue.extend 、 hook钩子 -》 创造组件的真实节点 init 方法 -》 创造组件的实例 -》 拿到实例对应$el 将内如插入到父dom中

Vue组件更新流程

父组件更新子组件一定要更新吗? 取决于子组件中的数据是否有变化,如果有变化才更新 ,属性是否有变化 主要更新有三个地方 , 插槽变化会更新, 属性变化更新, 事件变化更新,因为属性是响应式的 属性在使用的时候会做依赖收集 收集子组件的渲染watcher,属性变化会通知子组件重新渲染

如果遇到组件 会对组件进行diff操作 调用组件的prepatch方法来进行diff

属性是响应式的 属性在子组件中使用了,那么属性变了 那么页面不更新吗?

Vue中异步组件原理

  • 默认渲染组件如果发现组件的定义是一个函数,那么会先渲染一个 异步组件的占位符 <!---->
  • 在第一次渲染的时候 会把定义的函数进行解析。 获取到结果Vue.extend方法将结果放到Ctor上后再重新渲染
  • 核心就是靠的 $forceRender() 强制更新组件 + 组件的重新渲染

原理就是图片懒加载 先渲染空,组件加载完成了 在强制重新渲染

函数组件的优势及原理

函数组件的特点是没有this render(h,{parent,data})。 函数式组件是一个函数不会产生watcher。 性能高,组件初始化 需要走一遍new的流程 (不包含生命周期一系列的内容), 因为没有生成一个组件实例他的更新是跟随父组件的

如果只是用于纯渲染的话可以考虑使用函数式组件,因为不用new, 不用创建watcher

Vue组件间传值的方式及之间区别

(attrs 和 props有什么区别 如果用户在props中声明过的就不会在attrs里面出现了)

  • 最基本的传值方式 我们可以在父组件中给我们子组件传递一个函数,子组件调用函数把数据回传

  • 属性的原理就是 在模板编译的时候 会解析出带:号的属性 把属性放到propsData, 在组件初始化的时候会将propsData定义在我们的实例组件上 (父给子传递的属性会在解析的时候 放到,propsData,internalComponent的时候会把propsData放到 vm.$options.propsData, 在初始化属性的时候会进行校验,并且把属性定在组件的实例上)

  • 父组件中给子组件绑定的时间 _parentListeners 我会将这个属性全部定在组件的实例vm.$on(‘绑定事件’)

  • attrsattrs listeners 这两个表示父组件给我传递的所有属性和事件 他们两个是响应式的

  • parentparent child 在组件中获取父亲和孩子 用的比较少 jquery 或者一般使用的时候 我们通过$parent + name的方式来使用

  • provide + inject 在开发的时候也不建议使用 数据来源不明确 (实现原理就是在父组件中 填入数据,后代组件 都可以获取到) 父组件将数据挂载到_provide , 后代通过属性查找_provide属性找到后定义在自己的身上,一直向上找

  • ref 可以获取组件实例 vnode.componentInstance || vnode.elm ref就是获取组件的实例或元素

看下v-modal
with (this) {
  return _c("input", { // updateDirectives
    directives: [ // v-model 和 v-show 才能称之为指令 ,其他v-if v-for都不是正规的指令
      { name: "model", rawName: "v-model", value: title, expression: "title" },
    ],
    attrs: { type: "text" },  input.type = 'text' :value="title" @input="xxx"
    domProps: { value: title }, // dom属性的值是title的结果
    on: {
      input: function ($event) { // 输入的时候会修改title的内容
        if ($event.target.composing) return; // 如果 event.target.composing 就不更新数据了
        title = $event.target.value;
      },
    },
  });
}

 <div id="app">
        <!-- 语法糖 组件的双向绑定的语法糖 -->
        <my v-model="title" ></my>
    </div>
    <script>
        const vm = new Vue({ // 父组件 
            el: '#app',
            components:{
                my:{
                    model:{
                        prop:'xxx', // 这个xxx表示title
                        event:"checked" //其实是input事件的别名
                    },
                    props:['xxx'],
                    template: '<div>hello {{xxx}} <button @click="changeV"></button></div>',
                    methods:{
                        changeV(){
                            this.$emit('checked','world')
                        }
                    }
                }
            },
            data:{
                title:'hello'
            },
            
        });

## Vue中slot是如何实现的?什么时候使用它?
插槽 普通插槽是在 父组件中渲染的,但是作用域插槽是在子组件中渲染的,具名插槽只是做了一个映射表
// 组件的传入slot
let a1 = `<my><span slot="aaa">{{hello}}</span></my>`; // 先将组件内部的属性渲染完毕后传递给组件了

// 使用组件
let a2 = `<div><slot name="aaa"></slot></div>`;

let compiler = require("vue-template-compiler");

// 使用计算属性 先计算好 在循环
let { render: render1 } = compiler.compile(a1);
console.log(render1);

let { render: render2 } = compiler.compile(a2);
console.log(render2);

// 普通插槽的原理 就是在父组件中 将插槽的内容渲染出来 {slot:aaa, 虚拟节点}
// with(this){return _c('my',[_c('span',{attrs:{"slot":"aaa"},slot:"aaa"},[_v(_s(hello))])])}
// _t会根据具体的名字 找到对应的虚拟节点 替换掉内容
// with(this){return _c('div',[_t("aaa")],2)}  // this.$slots['aaa']

let { render: a3 } = compiler.compile(
  "<my><span slot-scope={a,b}>{{a}} {{b}}</span></my>"
);
let { render: a4 } = compiler.compile("<div><slot a=1 b=2></slot></div>");

console.log(a3, a4);

// 作用域的插槽实现原理  就是 在子组件渲染的过程中调用父组件传递的函数来渲染
// with (this) {
//   return _c("my", {
//     scopedSlots: _u([
//       {
//         key: "default",
//         fn: function ({ a, b }) { /// 父组件使用的 渲染内容 被放到了一个回调函数中
//           return _c("span", {}, [_v(_s(a) + " " + _s(b))]);
//         },
//       },// {default:fn}
//     ]),
//   });
// }
// with (this) {
//   return _c("div", [_t("default", null, { a: "1", b: "2" })], 2);
// }
// 组件的传入slot
let a1 = `<my><span slot="aaa">{{hello}}</span></my>`; // 先将组件内部的属性渲染完毕后传递给组件了

// 使用组件
let a2 = `<div><slot name="aaa"></slot></div>`;

let compiler = require("vue-template-compiler");

// 使用计算属性 先计算好 在循环
let { render: render1 } = compiler.compile(a1);
console.log(render1);

let { render: render2 } = compiler.compile(a2);
console.log(render2);

// 普通插槽的原理 就是在父组件中 将插槽的内容渲染出来 {slot:aaa, 虚拟节点}
// with(this){return _c('my',[_c('span',{attrs:{"slot":"aaa"},slot:"aaa"},[_v(_s(hello))])])}
// _t会根据具体的名字 找到对应的虚拟节点 替换掉内容
// with(this){return _c('div',[_t("aaa")],2)}  // this.$slots['aaa']

let { render: a3 } = compiler.compile(
  "<my><span slot-scope={a,b}>{{a}} {{b}}</span></my>"
);
let { render: a4 } = compiler.compile("<div><slot a=1 b=2></slot></div>");

console.log(a3, a4);

// 作用域的插槽实现原理  就是 在子组件渲染的过程中调用父组件传递的函数来渲染
// with (this) {
//   return _c("my", {
//     scopedSlots: _u([
//       {
//         key: "default",
//         fn: function ({ a, b }) { /// 父组件使用的 渲染内容 被放到了一个回调函数中
//           return _c("span", {}, [_v(_s(a) + " " + _s(b))]);
//         },
//       },// {default:fn}
//     ]),
//   });
// }
// with (this) {
//   return _c("div", [_t("default", null, { a: "1", b: "2" })], 2);
// }

Vue.use是干什么的?原理是什么?

Vue.use可以去调用插件 核心就是调用对应的install方法, 并且实现插件不依赖于具体的vue版本

组件中写name选项有哪些好处及作用?

vue里面有一个递归组件 , 在创建子组件的时候 会将组件放到自己的Sub.options上 devtools 里面可以根据name来知道对应的组件是谁 keep-alive 缓存的时候也可以指定名字缓存

## Vue事件修饰符有哪些?其实现原理是什么?
// on   $on
// nativeOn addEventListener
.stop, .prevent .native  capture self .once .passive

对于事件型:阻止型的 stop prevent  =》 e.preventDefault e.stopPropagation
就是在渲染成真实节点时候,绑定了相关的事件过后,通过ast树解析出来后,就自动加上相关的功能

对于 .once capture passive 做解析的时候 会创造映射表 实现对应的功能

native的使用方式
<a-com @click='warpClick'></a-com> 这样点击没有什么用
<a-com @click.native='warpClick'></a-com> 加了native才有用
 methods:{
            warpClick(){
                console.log(1);
            }
  }
capture的使用方式
正常情况下:\
        点击div3一层一层冒泡,先div3=》div2=》div1\
\
**使用了关键字:**\
        点击div3时,先div2=》div3=》div1
        
**passive和prevent冲突,不能同时绑定在一个监听器上。-  passive的作用通俗点说就是每次事件产生,浏览器都会去查询一下是否有preventDefault阻止该次事件的默认动作。我们加上passive就是为了告诉浏览器,不用查询了,我们没用preventDefault阻止默认动作。


## Vue中.sync修饰符的作用,用法及实现原理
目的就是为了实现数据通信同步问题 v-model可以解决但是只能解决一个数据同步问题,那么多个数据同步问题就得需要.sync语法了

<child  :foo.sync=”msg”></child>  就会被扩展为:  <child  :foo=”bar”  @update:foo=”val => bar = val”>   (@是v-on的简写)

当子组件需要更新 foo 的值的时候,他需要显示的触发一个更新事件:   this.$emit( “update:foo”, newValue );
Vue.directive('has',function(el,bindings,vnode){
  if(vnode.ctx.$store.state.xxxx ! === bindings.value){
    el.parentNode.removeChild(el)
  }
})
像这中的运用方式就是来判断是否有权限,没有权限就干掉
## keep-alive平时在哪里使用?原理是?
 路由中用的多一些 缓存组件 (LRU算法)
 
 
## mutation和action的区别
> mutation 同步的 唯一修改状态的方式 action 可以合并多个操作,实现获取数据提交给mutation , mutation中通过同步watch 来监控状态变化, action内部会被包装成promise 
> action主要是复用业务逻辑  mutation主要的作用就是更改状态
## Vue-Router有几种钩子函数,具体是什么及执行流程是怎样的?
内部原理 就是组成一个数组 用compose 将方法串行起来依次执行
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
。导航被确认
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

关于actived的文章
https://www.jianshu.com/p/eeda81293826