写点什么

前端一面高频 vue 面试题(边面边更)

作者:bb_xiaxia1998
  • 2023-02-06
    浙江
  • 本文字数:23714 字

    阅读完需:约 78 分钟

Vue 组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信


组件传参的各种方式



组件通信常用方式有以下几种


  • props / $emit 适用 父子组件通信

  • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的

  • ref$parent / $children(vue3废弃) 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法

  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

  • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件

  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

  • provide / inject 适用于 隔代组件通信

  • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系

  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用

  • Vuex 适用于 父子、隔代、兄弟组件通信

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。


根据组件之间关系讨论组件通信最为清晰有效


  • 父子组件:props/$emit/$parent/ref

  • 兄弟组件:$parent/eventbus/vuex

  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root


下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯


1. 父子组件通信


使用props,父组件可以使用props向子组件传递数据。


父组件vue模板father.vue:


<template>  <child :msg="message"></child></template>
<script>import child from './child.vue';export default { components: { child }, data () { return { message: 'father message'; } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <div>{{msg}}</div></template>
<script>export default { props: { msg: { type: String, required: true } }}</script>
复制代码


回调函数(callBack)


父传子:将父组件里定义的method作为props传入子组件


// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: {    changeMessage(){        this.message = 'test'    }}
复制代码


// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']
复制代码


子组件向父组件通信


父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件


父组件vue模板father.vue:


<template>    <child @msgFunc="func"></child></template>
<script>import child from './child.vue';export default { components: { child }, methods: { func (msg) { console.log(msg); } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <button @click="handleClick">点我</button></template>
<script>export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } }}</script>
复制代码


2. provide / inject 跨级访问祖先组件的数据


父组件通过使用provide(){return{}}提供需要传递的数据


export default {  data() {    return {      title: '我是父组件',      name: 'poetry'    }  },  methods: {    say() {      alert(1)    }  },  // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法  provide() {    return {      message: '我是祖先组件提供的数据',      name: this.name, // 传递属性      say: this.say    }  }}
复制代码


子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数


<template>  <p>曾孙组件</p>  <p>{{message}}</p></template><script>export default {  // inject 注入/接收祖先组件传递的所需要的数据即可   //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}  inject: [ "message","say"],  mounted() {    this.say();  },};</script>
复制代码


3. children 获取父组件实例和子组件实例的集合


  • this.$parent 可以直接访问该组件的父实例或组件

  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的


<!-- parent.vue --><template><div>  <child1></child1>     <child2></child2>   <button @click="clickChild">$children方式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default {  data(){    return {      total: 108    }  },  components: {    child1,    child2    },  methods: {    funa(e){      console.log("index",e)    },    clickChild(){      console.log(this.$children[0].msg);      console.log(this.$children[1].msg);    }  }}</script>
复制代码


<!-- child1.vue --><template>  <div>    <button @click="parentClick">点击访问父组件</button>  </div></template><script>export default {  data(){    return {      msg:"child1"    }  },  methods: {    // 访问父组件数据    parentClick(){      this.$parent.funa("xx")      console.log(this.$parent.total);    }  }}</script>
复制代码


<!-- child2.vue --><template>  <div>    child2  </div></template><script>export default {  data(){    return {     msg: 'child2'    }  }}</script>
复制代码


4. listeners 多级组件通信


$attrs 包含了从父组件传过来的所有props属性


// 父组件Parent.vue:<Child :name="name" :age="age"/>
// 子组件Child.vue:<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>
复制代码


$listeners包含了父组件监听的所有事件


// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>
复制代码


5. ref 父子组件通信


// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){    console.log(this.$refs.childComp.age);    this.$refs.childComp.changeAge()}
// 子组件Child.vue:data(){ return{ age:20 }},methods(){ changeAge(){ this.age=15 }}
复制代码


6. 非父子, 兄弟组件之间通信


vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js可以是这样:


// Bus.js
// 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } }
// main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
复制代码


<template>    <button @click="toBus">子组件传给兄弟组件</button></template>
<script>export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } }}</script>
复制代码


另一个组件也在钩子函数中监听on事件


export default {  data() {    return {      message: ''    }  },  mounted() {    this.$bus.$on('foo', (msg) => {      this.message = msg    })  }}
复制代码


7. $root 访问根组件中的属性或方法


  • 作用:访问根组件中的属性或方法

  • 注意:是根组件,不是父组件。$root只对根组件有用


var vm = new Vue({  el: "#app",  data() {    return {      rootInfo:"我是根元素的属性"    }  },  methods: {    alerts() {      alert(111)    }  },  components: {    com1: {      data() {        return {          info: "组件1"        }      },      template: "<p>{{ info }} <com2></com2></p>",      components: {        com2: {          template: "<p>我是组件1的子组件</p>",          created() {            this.$root.alerts()// 根组件方法            console.log(this.$root.rootInfo)// 我是根元素的属性          }        }      }    }  }});
复制代码


8. vuex


  • 适用场景: 复杂关系的组件数据传递

  • Vuex 作用相当于一个用来存储共享变量的容器



  • state用来存放共享变量的地方

  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值

  • mutations用来存放修改state的方法。

  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作


小结


  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref

  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递

  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject

  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

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

  • 根实例对象data可以是对象也可以是函数 (根实例是单例),不会产生数据污染情况

  • 组件实例对象data必须为函数 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数,


简版理解


// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类// Vue.extend 根据用户定义产生一个新的类function Vue() {}function Sub() { // 会将data存起来    this.data = this.constructor.options.data();}Vue.extend = function(options) {    Sub.options = options; // 静态属性    return Sub;}let Child = Vue.extend({    data:()=>( { name: 'zf' })});
// 两个组件就是两个实例, 希望数据互不感染let child1 = new Child();let child2 = new Child();
console.log(child1.data.name);child1.data.name = 'poetry';console.log(child2.data.name);
// 根不需要 任何的合并操作 根才有vm属性 所以他可以是函数和对象 但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数
复制代码


相关源码


// 源码位置 src/core/global-api/extend.jsexport function initExtend (Vue: GlobalAPI) {  Vue.extend = function (extendOptions: Object): Function {    extendOptions = extendOptions || {}    const Super = this    const SuperId = Super.cid    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})    if (cachedCtors[SuperId]) {      return cachedCtors[SuperId]    }
const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) }
const Sub = function VueComponent (options) { this._init(options) } // 子类继承大Vue父类的原型 Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super
// For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) }
// allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use
// create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx }
// keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options)
// cache constructor cachedCtors[SuperId] = Sub return Sub }}
复制代码

Vue 修饰符有哪些

事件修饰符


  • .stop 阻止事件继续传播

  • .prevent 阻止标签默认行为

  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理

  • .self 只当在 event.target 是当前元素自身时触发处理函数

  • .once 事件将只会触发一次

  • .passive 告诉浏览器你不想阻止事件的默认行为


v-model 的修饰符


  • .lazy 通过这个修饰符,转变为在 change 事件再同步

  • .number 自动将用户的输入值转化为数值类型

  • .trim 自动过滤用户输入的首尾空格


键盘事件的修饰符


  • .enter

  • .tab

  • .delete (捕获“删除”和“退格”键)

  • .esc

  • .space

  • .up

  • .down

  • .left

  • .right


系统修饰键


  • .ctrl

  • .alt

  • .shift

  • .meta


鼠标按钮修饰符


  • .left

  • .right

  • .middle

理解 Vue 运行机制全局概览

全局概览

首先我们来看一下笔者画的内部流程图。



大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js内部运行机制会有一个大概的认识。

初始化及挂载


new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「 响应式 」以及「 依赖收集 」,后面会详细讲到,这里只要有一个印象即可。


初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「 编译 」步骤。

编译

compile 编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function。



1. parse


parse 会用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST。


2. optimize


optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。


3. generate


generate 是将 AST 转化成 render function字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。


  • 在经历过 parseoptimizegenerate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

响应式

接下来也就是 Vue.js 响应式核心部分。



这里的 gettersetter 已经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。


  • render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「 依赖收集 」,「 依赖收集 」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Depsubs 中。形成如下所示的这样一个关系。



在修改对象的值的时候,会触发对应的 settersetter 通知之前「 依赖收集 」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略,这个我们后面再讲。

Virtual DOM

我们知道,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。


比如说下面这样一个例子:


{    tag: 'div',                 /*说明这是一个div标签*/    children: [                 /*存放该标签的子节点*/        {            tag: 'a',           /*说明这是一个a标签*/            text: 'click me'    /*标签的内容*/        }    ]}
复制代码


渲染后可以得到


<div>    <a>click me</a></div>
复制代码


这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。

更新视图


  • 前面我们说到,在修改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来修改对应的视图,那么最终是如何更新视图的呢?

  • 当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「 浪费 」。

  • 那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「 patch 」了。我们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「 差异 」。最后我们只需要将这些「 差异 」的对应 DOM 进行修改即可。

再看全局


回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?

Vue 路由 hash 模式和 history 模式

1. hash模式


早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL# 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'


https://interview2.poetries.top#search
复制代码


hash 路由模式的实现主要是基于下面几个特性


  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;

  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URLhash 值;

  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)


window.addEventListener("hashchange", funcRef, false);
复制代码


每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了


特点 :兼容性好但是不美观


2. history模式


history采用HTML5的新特性;且提供了两个新方法: pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更


window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
复制代码


这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。


history 路由模式的实现主要基于存在下面几个特性:


  • pushStaterepalceState 两个 API 来操作实现 URL 的变化 ;

  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。


特点 :虽然美观,但是刷新会出现 404 需要后端进行配置

ref 和 reactive 异同

这是Vue3数据响应式中非常重要的两个概念,跟我们写代码关系也很大


const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
const obj = reactive({ count: 0 })obj.count++
复制代码


  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象

  • 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式

  • 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题

  • ref返回的响应式数据在 JS 中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.valueref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref 对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。

  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作,从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式


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

Vue.observable 你有了解过吗?说说看

一、Observable 是什么

Observable 翻译过来我们可以理解成可观察的


我们先来看一下其在Vue中的定义


Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象


返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器


Vue.observable({ count : 1})
复制代码


其作用等同于


new vue({ count : 1})
复制代码


Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象


Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

二、使用场景

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择


创建一个js文件


// 引入vueimport Vue from 'vue// 创建state对象,使用observable让state对象可响应export let state = Vue.observable({  name: '张三',  'age': 38})// 创建对应的方法export let mutations = {  changeName(name) {    state.name = name  },  setAge(age) {    state.age = age  }}
复制代码


.vue文件中直接使用即可


<template>  <div>    姓名:{{ name }}    年龄:{{ age }}    <button @click="changeName('李四')">改变姓名</button>    <button @click="setAge(18)">改变年龄</button>  </div></template>import { state, mutations } from '@/storeexport default {  // 在计算属性中拿到值  computed: {    name() {      return state.name    },    age() {      return state.age    }  },  // 调用mutations里面的方法,更新数据  methods: {    changeName: mutations.changeName,    setAge: mutations.setAge  }}
复制代码

三、原理分析

源码位置:src\core\observer\index.js


export function observe (value: any, asRootData: ?boolean): Observer | void {  if (!isObject(value) || value instanceof VNode) {    return  }  let ob: Observer | void  // 判断是否存在__ob__响应式属性  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {    ob = value.__ob__  } else if (    shouldObserve &&    !isServerRendering() &&    (Array.isArray(value) || isPlainObject(value)) &&    Object.isExtensible(value) &&    !value._isVue  ) {    // 实例化Observer响应式对象    ob = new Observer(value)  }  if (asRootData && ob) {    ob.vmCount++  }  return ob}
复制代码


Observer


export class Observer {    value: any;    dep: Dep;    vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { // 实例化对象是一个对象,进入walk方法 this.walk(value) }}
复制代码


walk函数


walk (obj: Object) {    const keys = Object.keys(obj)    // 遍历key,通过defineReactive创建响应式对象    for (let i = 0; i < keys.length; i++) {        defineReactive(obj, keys[i])    }}
复制代码


defineReactive方法


export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: ?Function,  shallow?: boolean) {  const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
// cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] }
let childOb = !shallow && observe(val) // 接下来调用Object.defineProperty()给对象定义响应式属性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 对观察者watchers进行通知,state就成了全局响应式对象 dep.notify() } })}
复制代码

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 ,可直接监听对象数组的变化

Vue template 到 render 的过程

vue 的模版编译过程主要如下:template -> ast -> render 函数


vue 在模版编译版本的码中会执行 compileToFunctions 将 template 转化为 render 函数:


// 将模板编译为render函数const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
复制代码


CompileToFunctions 中的主要逻辑如下∶ (1)调用 parse 方法将 template 转化为 ast(抽象语法树)


constast = parse(template.trim(), options)
复制代码


  • parse 的目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。

  • 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。


AST 元素节点总共三种类型:type 为 1 表示普通元素、2 为表达式、3 为纯文本


(2)对静态节点做优化


optimize(ast,options)
复制代码


这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化


深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用。


(3)生成代码


const code = generate(ast, options)
复制代码


generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``) 生成 render 函数。

Vue 模版编译原理

vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。


  • 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。

  • 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。

  • 生成阶段:将最终的 AST 转化为 render 函数字符串。

对前端路由的理解

在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。这个体验并不好,不过在最初也是无奈之举——用户只有在刷新页面的情况下,才可以重新去请求数据。


后来,改变发生了——Ajax 出现了,它允许人们在不刷新页面的情况下发起请求;与之共生的,还有“不刷新页面即可更新页面内容”这种需求。在这样的背景下,出现了 SPA(单页面应用)。


SPA 极大地提升了用户体验,它允许页面在不刷新的情况下更新页面内容,使内容的切换更加流畅。但是在 SPA 诞生之初,人们并没有考虑到“定位”这个问题——在内容切换前后,页面的 URL 都是一样的,这就带来了两个问题:


  • SPA 其实并不知道当前的页面“进展到了哪一步”。可能在一个站点下经过了反复的“前进”才终于唤出了某一块内容,但是此时只要刷新一下页面,一切就会被清零,必须重复之前的操作、才可以重新对内容进行定位——SPA 并不会“记住”你的操作。

  • 由于有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,搜索引擎无法收集全面的信息


为了解决这个问题,前端路由出现了。


前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。


那么如何实现这个目的呢?首先要解决两个问题:


  • 当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为我们的 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。

  • 单页面应用对服务端来说,就是一个 URL、一套资源,那么如何做到用“不同的 URL”来映射不同的视图内容呢?


从这两个问题来看,服务端已经完全救不了这个场景了。所以要靠咱们前端自力更生,不然怎么叫“前端路由”呢?作为前端,可以提供这样的解决思路:


  • 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化掉。

  • 感知 URL 的变化。这里不是说要改造 URL、凭空制造出 N 个 URL 来。而是说 URL 还是那个 URL,只不过我们可以给它做一些微小的处理——这些处理并不会影响 URL 本身的性质,不会影响服务器对它的识别,只有我们前端感知的到。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。

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

Vue 的编译过程就是将 template 转化为 render 函数的过程


  • 解析生成 AST 树template模板转化成AST语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理

  • 标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作

  • Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用

  • 等待后续节点更新,如果是静态的,不会在比较children

  • 代码生成 编译的最后一步是将优化后的AST树转换为可执行的代码


回答范例


思路


  • 引入vue编译器概念

  • 说明编译器的必要性

  • 阐述编译器工作流程


回答范例


  1. Vue中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为js中可执行的render函数。

  2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。

  3. Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为 抽象语法树 AST ,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数


可能的追问


  1. Vue中编译器何时执行?



new Vue()之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 propsmethodsdatacomputedwatch等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式」以及「依赖收集」


  • 初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤

  • compile编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function


  1. React有没有编译器?


react 使用babelJSX语法解析


<div id="app"></div><script>    let vm = new Vue({        el: '#app',        template: `<div>            // <span>hello world</span> 是静态节点            <span>hello world</span>                // <p>{{name}}</p> 是动态节点            <p>{{name}}</p>        </div>`,        data() {          return { name: 'test' }        }    });</script>
复制代码


源码分析


export function compileToFunctions(template) {  // 我们需要把html字符串变成render函数  // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法  // 很多库都运用到了ast 比如 webpack babel eslint等等  let ast = parse(template);  // 2.优化静态节点:对ast树进行标记,标记静态节点    if (options.optimize !== false) {      optimize(ast, options);    }
// 3.通过ast 重新生成代码 // 我们最后生成的代码需要和render函数一样 // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))) // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本 let code = generate(ast); // 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值 let renderFn = new Function(`with(this){return ${code}}`); return renderFn;}
复制代码

Vue 的生命周期方法有哪些

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

  2. Vue生命周期总共分为 8 个阶段创建前/后载入前/后更新前/后销毁前/后


beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activated deactivated



其他几个生命周期



  1. 要掌握每个生命周期内部可以做什么事


  • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务

  • created 组件初始化完毕,可以访问各种数据,获取接口数据等

  • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上

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

  • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。

  • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。

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

  • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消

  • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器


<div id="app">{{name}}</div><script>    const vm = new Vue({        data(){            return {name:'poetries'}        },        el: '#app',        beforeCreate(){            // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。            console.log('beforeCreate');        },        created(){            // 属性和方法的运算, watch/event 事件回调。这里没有$el            console.log('created')        },        beforeMount(){            // 相关的 render 函数首次被调用。            console.log('beforeMount')        },        mounted(){            // 被新创建的 vm.$el 替换            console.log('mounted')        },        beforeUpdate(){            //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。            console.log('beforeUpdate')        },        updated(){            //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。            console.log('updated')        },        beforeDestroy(){            // 实例销毁之前调用 实例仍然完全可用            console.log('beforeDestroy')        },        destroyed(){             // 所有东西都会解绑定,所有的事件监听器会被移除            console.log('destroyed')        }    });    setTimeout(() => {        vm.name = 'poetry';        setTimeout(() => {            vm.$destroy()          }, 1000);    }, 1000);</script>
复制代码


  1. 组合式 API 生命周期钩子


你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。


下表包含如何在 setup() 内部调用生命周期钩子:



因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写


export default {  setup() {    // mounted    onMounted(() => {      console.log('Component is mounted!')    })  }}
复制代码


setupcreated谁先执行?


  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好

  • setup:在beforeCreatecreated之间执行

  • created:组件被创建出来,组件的methodsdata已经初始化好了


由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined


import { ref } from "vue"export default {  // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去  setup(){    let count = ref(1)    function myFn(){      count.value +=1    }    return {count,myFn}  },
}
复制代码


  1. 其他问题


  • 什么是 vue 生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载 Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

  • vue 生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个 Vue 实例的过程时更容易形成好的逻辑。

  • vue 生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted

  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created

  • DOM 渲染在哪个周期中就已经完成?mounted中,

  • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted




### 说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢
#### 一、为什么要划分
使用`vue`构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高
在划分项目结构的时候,需要遵循一些基本的原则:
* 文件夹和文件夹内部文件的语义一致性* 单一入口/出口* 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用* 公共的文件应该以绝对路径的方式从根目录引用* `/src` 外的文件不应该被引入
**文件夹和文件夹内部文件的语义一致性**
我们的目录结构都会有一个文件夹是按照路由模块来划分的,如`pages`文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹
这样做的好处在于一眼就从 `pages`文件夹看出这个项目的路由有哪些
**单一入口/出口**
举个例子,在`pages`文件夹里面存在一个`seller`文件夹,这时候`seller` 文件夹应该作为一个独立的模块由外部引入,并且 `seller/index.js` 应该作为外部引入 seller 模块的唯一入口
```javascript// 错误用法import sellerReducer from 'src/pages/seller/reducer'
// 正确用法import { reducer as sellerReducer } from 'src/pages/seller'
复制代码


这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点


就近原则,紧耦合的文件应该放到一起,且应以相对路径引用


使用相对路径可以保证模块内部的独立性


// 正确用法import styles from './index.module.scss'// 错误用法import styles from 'src/pages/seller/index.module.scss'
复制代码


举个例子


假设我们现在的 seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller


如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更。


但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改


公共的文件应该以绝对路径的方式从根目录引用


公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components


在使用到的页面中,采用绝对路径的形式引用


// 错误用法import Input from '../../components/input'// 正确用法import Input from 'src/components/input'
复制代码


同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换


再加上绝对路径有全局的语义,相对路径有独立模块的语义


src 外的文件不应该被引入


vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js,css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件


这样的好处是方便划分项目代码文件和配置文件

二、目录结构

单页面目录结构


project│  .browserslistrc│  .env.production│  .eslintrc.js│  .gitignore│  babel.config.js│  package-lock.json│  package.json│  README.md│  vue.config.js│  yarn-error.log│  yarn.lock├─public│      favicon.ico│      index.html|-- src    |-- components        |-- input            |-- index.js            |-- index.module.scss    |-- pages        |-- seller            |-- components                |-- input                    |-- index.js                    |-- index.module.scss            |-- reducer.js            |-- saga.js            |-- index.js            |-- index.module.scss        |-- buyer            |-- index.js        |-- index.js
复制代码


多页面目录结构


my-vue-test:.│  .browserslistrc│  .env.production│  .eslintrc.js│  .gitignore│  babel.config.js│  package-lock.json│  package.json│  README.md│  vue.config.js│  yarn-error.log│  yarn.lock├─public│      favicon.ico│      index.html└─src    ├─apis //接口文件根据页面或实例模块化    │      index.js    │      login.js    ├─components //全局公共组件    │  └─header    │          index.less    │          index.vue    ├─config //配置(环境变量配置不同passid等)    │      env.js    │      index.js    ├─contant //常量    │      index.js    ├─images //图片    │      logo.png    ├─pages //多页面vue项目,不同的实例    │  ├─index //主实例    │  │  │  index.js    │  │  │  index.vue    │  │  │  main.js    │  │  │  router.js    │  │  │  store.js    │  │  │    │  │  ├─components //业务组件    │  │  └─pages //此实例中的各个路由    │  │      ├─amenu    │  │      │      index.vue    │  │      │    │  │      └─bmenu    │  │              index.vue    │  │    │  └─login //另一个实例    │          index.js    │          index.vue    │          main.js    ├─scripts //包含各种常用配置,工具函数    │  │  map.js    │  │    │  └─utils    │          helper.js    ├─store //vuex仓库    │  │  index.js    │  │    │  ├─index    │  │      actions.js    │  │      getters.js    │  │      index.js    │  │      mutation-types.js    │  │      mutations.js    │  │      state.js    │  │    │  └─user    │          actions.js    │          getters.js    │          index.js    │          mutation-types.js    │          mutations.js    │          state.js    └─styles //样式统一配置        │  components.less        ├─animation        │      index.less        │      slide.less        ├─base        │      index.less        │      style.less        │      var.less        │      widget.less        └─common                index.less                reset.less                style.less                transition.less
复制代码


小结


项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用

delete 和 Vue.delete 删除数组的区别

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。

  • Vue.delete 直接删除了数组 改变了数组的键值。

从 0 到 1 自己构架一个 vue 项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织

综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可


思路


  • 构建项目,创建项目基本结构

  • 引入必要的插件:

  • 代码规范:prettiereslint

  • 提交规范:husky,lint-staged`

  • 其他常用:svg-loadervueusenprogress

  • 常见目录结构


回答范例


  1. 0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件

  2. 目前vue3项目我会用vite或者create-vue创建项目

  3. 接下来引入必要插件:路由插件vue-router、状态管理vuex/piniaui库我比较喜欢element-plus 和antd-vuehttp工具我会选axios

  4. 其他比较常用的库有vueusenprogress,图标可以使用vite-svg-loader

  5. 下面是代码规范:结合prettiereslint即可

  6. 最后是提交规范,可以使用huskylint-stagedcommitlint

  7. 目录结构我有如下习惯: .vscode:用来放项目中的 vscode 配置


  • plugins:用来放 vite 插件的 plugin 配置

  • public:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下

  • src:用来放项目代码文件

  • api:用来放http的一些接口配置

  • assets:用来放一些 CSS 之类的静态资源

  • components:用来放项目通用组件

  • layout:用来放项目的布局

  • router:用来放项目的路由配置

  • store:用来放状态管理Pinia的配置

  • utils:用来放项目中的工具方法类

  • views:用来放项目的页面文件

v-on 可以监听多个方法吗?

可以监听多个方法


<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />
复制代码


v-on 常用修饰符


  • .stop 该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation() 方法

  • .prevent 该修饰符会阻止当前事件的默认行为。同理于调用 event.preventDefault() 方法

  • .self 该指令只当事件是从事件绑定的元素本身触发时才触发回调

  • .once 该修饰符表示绑定的事件只会被触发一次

v-once 的使用场景有哪些

分析


v-onceVue中内置指令,很有用的API,在优化方面经常会用到


体验


仅渲染元素和组件一次,并且跳过未来更新


<!-- single element --><span v-once>This will never change: {{msg}}</span><!-- the element have children --><div v-once>  <h1>comment</h1>  <p>{{msg}}</p></div><!-- component --><my-component v-once :comment="msg"></my-component><!-- `v-for` directive --><ul>  <li v-for="i in list" v-once>{{i}}</li></ul>
复制代码


回答范例


  • v-oncevue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新

  • 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段

  • 我们只需要作用的组件或元素上加上v-once即可

  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了

  • 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算


原理


下面例子使用了v-once


<script setup>import { ref } from 'vue'const msg = ref('Hello World!')</script><template>  <h1 v-once>{{ msg }}</h1>  <input v-model="msg"></template>
复制代码


我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:


// ...return (_ctx, _cache) => {  return (_openBlock(), _createElementBlock(_Fragment, null, [    // 从缓存获取vnode    _cache[0] || (      _setBlockTracking(-1),      _cache[0] = _createElementVNode("h1", null, [        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)      ]),      _setBlockTracking(1),      _cache[0]    ),// ...
复制代码

Class 与 Style 如何动态绑定

Class 可以通过对象语法和数组语法进行动态绑定


对象语法:


<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: { isActive: true, hasError: false}
复制代码


数组语法:


<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: { activeClass: 'active', errorClass: 'text-danger'}
复制代码


Style 也可以通过对象语法和数组语法进行动态绑定


对象语法:


<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: { activeColor: 'red', fontSize: 30}
复制代码


数组语法:


<div v-bind:style="[styleColor, styleSize]"></div>
data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' }}
复制代码

vue 中使用了哪些设计模式

  • 工厂模式 传入参数即可创建实例:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode

  • 单例模式 整个程序有且仅有一个实例:vuexvue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉

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

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

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

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


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
前端一面高频vue面试题(边面边更)_Vue_bb_xiaxia1998_InfoQ写作社区