vue(面试自用)

190 阅读16分钟

1:vue特性

数据驱动视图:数据的变化会驱动视图自动更新

双向数据绑定

2:Vue的优点?Vue的缺点?

优点:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开

缺点:单页面不利于seo,不支持IE8以下,首屏加载时间长

为什么说Vue是一个渐进式框架?

渐进式:通俗点讲就是,你想用啥你就用啥,咱也不强求你。你想用component就用,不用也行,你想用vuex就用,不用也可以


image.png

3:常用vue指令

image.png

4:vue事件修饰符

截屏2021-07-11 下午9.56.53.png

5:为什么data是个函数并且返回一个对象呢?

data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染

6:v-for为啥要使用key,用index作为key可以吗?

什么是Vnode,他其实就是虚拟节点,本质上是一个javascrpt对象。很多个虚拟节点组成虚拟Dom(Vnode tree)。

diff算法:新旧vnods进行对比的过程

比如在b后面插入一个F,渲染到页面

<template>
    <i v-for="item in arr">{{ item }}</i>
    <button @click='charu'>插入F<button>
<template>
    
 let arr = ['a','b','c''d]
            
 let charu = ()=>{
     arr.splice(2,0,'F')
 }

有几种diff算法,一是销毁先前的Vnods,全部重新创建,二是把c变成f,d变成c,再加一个d。还有则是直接插入。

有无key决定了用那种,有key调用patchKeyedChildre,没有则是上面第二种。

没有key的做法:

遍历长度短的vnods,新旧vnode一一做比较,patch,相同,不改变,不同,改变,然后判断新旧vnode长度,新的比较长就会创建新节点。

有key的做法: 通过key可以精准的判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更高效,减少DOM操作,提高性能

image.png

image.png

image.png 总结

用index做key也会影响性能。

image.png

7:template元素

template元素可以被当做不可见 ,当依赖的数据不发生变化,不会重新计算。变化,重新计算。

9:侦听器

watchEffect

为了根据响应式状态自动应用重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

const count = ref(0)
​
watchEffect(() => console.log(count.value))
// -> logs 0setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

const stop = watchEffect(() => {
  /* ... */
})
​
// later
stop()

清除副作用

image.png 副作用函数执行时机

image.png watch侦听器

他不会立即执行

侦听单个数据源:

侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref: // 侦听一个 getter const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } )

// 直接侦听ref const count = ref(0) watch(count, (count, prevCount) => { /* ... */ }) 侦听器还可以使用数组同时侦听多个源: const firstName = ref('') const lastName = ref('')

watch([firstName, lastName], (newValues, prevValues) => { console.log(newValues, prevValues) })

firstName.value = 'John' // logs: ["John", ""] ["", ""] lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""] 侦听响应式对象,立即执行 watch( () => state, (state, prevState) => { console.log('deep', state.attributes.name, prevState.attributes.name) }, { deep: true,immediately:true } )

10:vue生命周期

image.png image.png

11:注册组件

通过components注册的数私有组件,可以通过vue.commponent('name',component)注册全局组件。

12:组件之间传值

  • 父组件传值给子组件,子组件使用props进行接收

    //子定义属性
    //父组件
    <test :name="name"></test>
    //子组件
    props: {
        name: {
          type: String,
          default: 'zs',
        },
      },
    

    传入一个对象的所有 property

    如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 v-bind (用 v-bind 代替 :prop-name)。例如,对于一个给定的对象 post

    post: {
      id: 1,
      title: 'My Journey with Vue'
    }
    

    下面的模板:

    <blog-post v-bind="post"></blog-post>
    

    等价于:

    <blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
    

    Prop 验证

    app.component('my-component', {
      props: {
        // 基础的类型检查 (`null` 和 `undefined` 值会通过任何类型验证)
        propA: Number,
        // 多个可能的类型
        propB: [String, Number],
        // 必填的字符串
        propC: {
          type: String,
          required: true
        },
        // 带有默认值的数字
        propD: {
          type: Number,
          default: 100
        },
        // 带有默认值的对象
        propE: {
          type: Object,
          // 对象或数组的默认值必须从一个工厂函数返回
          default() {
            return { message: 'hello' }
          }
        },
        // 自定义验证函数
        propF: {
          validator(value) {
            // 这个值必须与下列字符串中的其中一个相匹配
            return ['success', 'warning', 'danger'].includes(value)
          }
        },
        // 具有默认值的函数
        propG: {
          type: Function,
          // 与对象或数组的默认值不同,这不是一个工厂函数——这是一个用作默认值的函数
          default() {
            return 'Default function'
          }
        }
      }
    })
    

    我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个要求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。

    为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证要求的对象,而不是一个字符串数组。例如:

  • 子组件传值给父组件,子组件使用$emit+事件对父组件进行传值

image.png

  • 使用组件的v-model进行传值 在子组件中
<template>
  <div class="test">
    <input type="text" v-model="values" />
  </div>
</template>

<script>
import { ref, watch } from 'vue'

export default {
  props: {
    list: {
      type: String,
      required: true,
    },
  },
  emits: ['update:list'],
  setup(props, { emit }) {
    let values = ref(props.list)

    watch(values, (newVal) => {
      emit('update:list', newVal)
    })
    return {
      values,
    }
  },
}

在父组件中

 <test v-model:list="list"></test>
 
 setup() {
    let list = ref('李四')

    let ccc = () => {
      console.log(list.value)
    }
    return {
      list,
      ccc,
    }
  },
  • 组件中可以使用$parent$children获取到父组件实例和子组件实例,进而获取数据
  • 使用$attrs$listeners,在对一些组件进行二次封装时可以方便传值,例如A->B->C
  • 使用$refs获取组件实例,进而获取数据
  • 使用Vuex进行状态管理
  • 使用eventBus进行跨组件触发事件,进而传递数据
  • 使用provideinject,官方建议我们不要用这个,我在看ElementUI源码时发现大量使用
  • 使用浏览器本地缓存,例如localStorage

13:组件样式冲突问题

默认情况下,写在.vue文件中的样式会全局生效,容易造成样式冲突问题。加Scoped解决。

产生冲突的原因:

1.单页面引用程序中,左右组组件的DOm结构,都是基于唯一的index.html页面呈现的

2.每个组件中的样式,都会影响index.html页面中的Dom元素。

当引用第三方库的时候,修改样式加/deep/

14:ref引用

image.png 可以使用ref获取一个vue组件的实例,可以用来传数据或者方法。

15:nextTick(cb)

理解nexttick,当数据更新时,dom渲染之后,会自动执行nextTick中的回调函数,vue中的渲染是异步的,通俗点说就是,同一事件循环内多次修改,会统一进行一次视图更新,这样才能节省性能嘛,比如当我们点击添加一个元素的子元素,然后获取他子元素的个数,会发现慢一拍,当我们点击,添加元素时,不会马上渲染dom,而是在下一个tick才会渲染。

nextTick使用场景 在修改数据,需要获取到最新的DOM的时候可以使用

16:keepAlive

image.png keepAlive生命周期函数

image.png

image.png 动态组件

vue提供了用于动态组件的渲染

image.png

17:插槽

插槽是vue为组件的封装者提供的能力,允许开发者在封装组件时,把不确定的,希望由用户指定的部分定义为插槽。

插槽内容

<slot> 元素作为承载分发内容的出口。它允许你像这样合成组件:

<todo-button>
  Add todo
</todo-button>

然后在 <todo-button> 的模板中,你可能有:

<!-- todo-button 组件模板 -->
<button class="btn-primary">
  <slot></slot>
</button>

符串只是开始!插槽还可以包含任何模板代码,包括 HTML:或其他组件:

具名插槽

简单来说就是给插槽添加名字

slot> 元素有一个特殊的 attribute:name。通过它可以为不同的插槽分配独立的 ID,也就能够以此来决定内容应该渲染到什么地方:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

一个不带 name<slot> 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称,v-slot可以简写为 # :

base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
​
  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>
​
  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

渲染作用域

请记住这条规则:

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。

要使 item 在父级提供的插槽内容上可用,我们可以添加一个 <slot> 元素并将其作为一个 attribute 绑定:

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item"></slot>
  </li>
</ul>

绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在,在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>

动态插槽名

动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

18:自定义指令

它的作用价值在于当开发人员在某些场景下需要对普通 DOM 元素进行操作。

Vue中,指令其实就是特殊的属性,Vue会根据指令,在背后做一些事,至于具体做什么事,Vue根据不同的指令会执行不同的操作。

Vue`指令有个明显的特点就是,都是以`v-`开头,例如:`v-text

分为全局自定义指令,局部自定义指令。

然而,在 Vue 3 中,我们为自定义指令创建了一个更具凝聚力的 API。正如你所看到的,它们与我们的组件生命周期方法有很大的不同,即使钩子的目标事件十分相似。我们现在把它们统一起来了:

  • created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
  • bind → beforeMount
  • inserted → mounted
  • beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
  • update → 移除!该钩子与 updated 有太多相似之处,因此它是多余的。请改用 updated
  • componentUpdated → updated
  • beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
  • unbind -> unmounted

最终的 API 如下:

const MyDirective = {
  created(el, binding, vnode, prevVnode) {}, // 新增 在元素的 attribute 或事件监听器被应用之前调用。
  beforeMount() {},// 当指令第一次绑定到元素并且在挂载父组件之前调用
  mounted() {},// 在绑定元素的父组件被挂载后调用
  beforeUpdate() {}, // 新增 在更新包含组件的 VNode 之前调用
  updated() {}, // 在包含组件的 VNode及其子组件的 VNode更新后调用
  beforeUnmount() {}, // 新增 在卸载绑定元素的父组件之前调用
  unmounted() {} // 当指令与元素解除绑定且父组件已卸载时,只调用一次
}

Vue3中可以通过应用实例身上的directive()注册一个全局自定义指令。

<p v-highlight="'yellow'">以亮黄色高亮显示此文本</p>
const app = Vue.createApp({})
​
app.directive('highlight', {
    
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value
  }
})

19:路由

什么是前端路由:Hash地址与组件之间的对应关系。

路由有哪些模式呢?又有什么不同呢?

  • hash模式:通过#号后面的内容的更改,触发hashchange事件,实现路由切换
  • history模式:通过pushStatereplaceState切换url,实现路由切换,需要后端配合

to#

  • 类型RouteLocationRaw

  • 详细内容

    表示目标路由的链接。当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个 string 或者是描述目标位置的对象

<!-- 字符串 -->
<router-link to="/home">Home</router-link>
<!-- 渲染结果 -->
<a href="/home">Home</a><!-- 使用 v-bind 的 JS 表达式 -->
<router-link :to="'/home'">Home</router-link><!-- 同上 -->
<router-link :to="{ path: '/home' }">Home</router-link><!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link><!-- 带查询参数,下面的结果为 `/register?plan=private` -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">
  Register
</router-link>

replace#

  • 类型boolean

  • 默认值false

  • 详细内容

    设置 replace 属性的话,当点击时,会调用 router.replace(),而不是 router.push(),所以导航后不会留下历史记录。

<router-link to="/abc" replace></router-link>

占位符

image.png 路由链接

image.png 路由重定向

image.png 路由名称

image.png 嵌套路由

image.png

image.png 动态路由

image.png 获取动态路由的值

image.png

image.png NotFound

image.png

image.png 动态添加路由

image.png 动态删除路由

image.png 路由导航守卫

image.png router-link的v-slot

image.png router-view的v-slot

image.png

20:vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

安装 Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:

import { createApp } from 'vue'
import { createStore } from 'vuex'// 创建一个新的 store 实例
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
​
const app = createApp({ /* 根组件 */ })
​
// 将 store 实例作为插件安装
app.use(store)

现在,你可以通过 store.state 来获取状态对象,并通过 store.commit 方法触发状态变更:

store.commit('increment')
​
console.log(store.state.count) // -> 1

在 Vue 组件中, 可以通过 this.$store 访问store实例。现在我们可以从组件的方法提交一个变更:

methods: {
  increment() {
    this.$store.commit('increment')
    console.log(this.$store.state.count)
  }
}

getter

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。

image.png 他可以返回一个函数,传入参数

image-20220521113442137

store.getters.totalPriceCounteGerater(2) // -> { id: 2, text: '...', done: false }

mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个 回调函数 (handler) 。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment')

你可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload)

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

store.commit({
  type: 'increment',
  amount: 10
})

使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'const store = createStore({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // 修改 state
    }
  }
})

用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。

一条重要的原则就是要记住 mutation 必须是同步函数

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。

分发Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

Actions 支持同样的载荷方式和对象方式进行分发:

/ 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})
​
// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

来看一个更加实际的购物车示例,涉及到调用异步 API分发多重 mutation

actions: {
  checkout ({ commit, state }, products) {
    // 把当前购物车的物品备份起来
    const savedCartItems = [...state.cart.added]
    // 发出结账请求,然后乐观地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接受一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

Moudle

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
​
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
​
const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
​
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

命名空间

默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

const store = createStore({
  modules: {
    account: {
      namespaced: true,
​
      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },
​
      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },
​
        // 进一步嵌套命名空间
        posts: {
          namespaced: true,
​
          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

22:动态class动态style

动态class对象:<div :class="{ 'is-active': true, 'red': isRed }"></div>

动态class数组:<div :class="['is-active', isRed ? 'red' : '' ]"></div>

动态style对象:<div :style="{ color: textColor, fontSize: '18px' }"></div>

动态style数组:<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>

23:为什么v-if和v-for不建议用在同一标签?

在Vue2中,v-for优先级是高于v-if的,咱们来看例子

<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
    {{item}}
</div>

上面的写法是v-forv-if同时存在,会先把7个元素都遍历出来,然后再一个个判断是否为3,并把3给隐藏掉,这样的坏处就是,渲染了无用的3节点,增加无用的dom操作,建议使用computed来解决这个问题:

<div v-for="item in list">
    {{item}}
</div>
​
computed() {
    list() {
        return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
    }
  }

24:不需要响应式的数据应该怎么处理?

在我们的Vue开发中,会有一些数据,从始至终都未曾改变过,这种死数据,既然不改变,那也就不需要对他做响应式处理了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的死数据,如果都进行响应式处理,那会消耗大量性能。

vue2用下面的方法,vue3直接写

// 方法一:将数据定义在data之外
data () {
    this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    return {}
 }
    
// 方法二:Object.freeze()  阻止修改现有属性的特性和值,并阻止添加新属性。
data () {
    return {
        list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
    }
 }
​

25: 直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?

原因:Vue没有对数组进行Object.defineProperty的属性劫持,所以直接arr[index] = xxx是无法更新视图的

使用数组的splice方法,arr.splice(index, 1, item)

使用Vue.$set(arr, index, value)

26:审查元素时发现data-v-xxxxx,这是啥?

这是在标记vue文件中css时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记,所以每引入一个component就会出现一个新的'data-v-xxx'标记

27:v-model语法糖如何实现?

通过v-on和v-bind实现

<div id="app">
        {{msg}}
        <!-- 将value属性和msg数据进行绑定,那么msg数据变化时也会影响到value -->
        <div><input v-bind:value='msg' v-on:input='handle' type="text"></div>
    </div>
    <script src="../vue.js"></script>
    <script type="text/javascript">
        var vm = new Vue({
            el: "#app",
            data: {
                msg: 'hello'
            },
            methods: {
                handle: function (e) {
                    //表格的value值发生变化时,msg的值也会变化  ,同时msg的值也是value值
                    this.msg = e.target.value;
                }
            }
        })
    </script>

在组件上使用v-model

在父组件中

image.png

image.png 因此,需要在子组件中申明自定义属性modelValue,以及发出一个事件update:model-value,以及可以传入值

image.png

28:vue2和vue3

✨ 开篇

RT,我们为什么要上 Vue3?使用 Vue3 影响我开法拉利吗? 最近在 Vue3 发布了3.2 大版本之后,掘金上关于 Vue3 的文章越来越多,本来想着让子弹再飞一会,但最近公司上了 Vue3 的项目,自己也跟着学了起来。

昨天还是 Vue2 的 Options API 的忠实信徒,结果今天搞了 Vue3 的 Composition API 之后,直呼 Vue3 真香!

接下来,我们从分析 Vue2 优缺点入手,以及结合图片和用例来了解 Vue3 的优势。

Vue2 ⚔ Vue3

Vue2

优点

不可否认 Vue2 在取得的成功,在 Github 的 Front-End 分类的排行榜上也能看到 Vue 仓库的排名是第一,粉丝足够多,活跃用户足够多

image.png

image.png

比 React 足足多了 12k+ star

从我的角度来看,最大的功臣莫过于这三个:

  • 响应式数据
  • Options API
  • SFC

除去这三位大将,不可或缺 Vuex/Vue-Router 这两位功臣,以及丰富的周边插件和活跃的社区。

缺点

在一个组件仅承担单一逻辑的时候,使用 Options API 来书写组件是很清晰的。

但是在我们实际的业务场景中,一个父组件总是要包含多个子组件,父组件需要给子组件传值、处理子组件事件、直接操作子组件以及处理各种各样的调接口的逻辑,这时候我们的父组件的逻辑就会变得复杂。

我们从代码维护者的角度出发,假设这个时候有 10 个函数方法,自然我们要把他们放入到methods中,而这个 10 个函数方法又分别操作 10 个数据,这个 10 个数据又分别需要进行Watch操作。

这时候,我们根据Vue2Options API的写法,就写完了 10 个method、10 个data、10 个watch,我们就将本来 10 个的函数方法,分割在 30 个不同的地方。

这时候父组件的代码,对代码维护者来说是很不友好的。

可能有人说,这么写可以增加代码量,老板夸我牛逼!哈哈哈哈 😂

Vue3

优点

自由,自由,还是 TM 的自由!

  • 更强的性能,更好的 tree shaking
  • Composition API + setup
  • 更好地支持 TypeScript

可能的缺点

Vue3: 让你自由过了火

使用Composition APIsetup这个舞台上尽情的表演之后,可能存在一个问题:那就是如何优雅地组织代码?

代码不能优雅的组织,在代码量上去之后,也一样很难维护。

SFC 写法变化

Options API对比Composition API.png

Vue2 一个普通的 Vue 文件

<template>
  <div>
    <p>{{ person.name }}</p>
    <p>{{ car.name }}</p>
  </div>
</template><script>
export default {
  name: "Person",
​
  data() {
    return {
      person: {
        name: "小明",
        sex: "male",
      },
      car: {
        name: "宝马",
        price: "40w",
      }
    };
  },
​
  watch:{
      'person.name': (value) => {
          console.log(`名字被修改了, 修改为 ${value}`)
      },
      'person.sex': (value) => {
          console.log(`性别被修改了, 修改为 ${value}`)
      }
  },
​
  methods: {
    changePersonName() {
      this.person.name = "小浪";
    },
​
    changeCarPrice() {
      this.car.price = "80w";
    }
  },
};
</script>
复制代码

采用 Vue3 Composition API 进行重写

<template>
  <p>{{ person.name }}</p>
  <p>{{ car.name }}</p>
</template><script lang="ts" setup>
import { reactive, watch } from "vue";
​
// person的逻辑
const person = reactive<{ name: string; sex: string }>({
  name: "小明",
  sex: "male",
});
watch(
  () => [person.name, person.sex],
  ([nameVal, sexVal]) => {
    console.log(`名字被修改了, 修改为 ${nameVal}`);
    console.log(`名字被修改了, 修改为 ${sexVal}`);
  }
);
function changePersonName() {
  person.name = "小浪";
}
​
// car的逻辑
const car = reactive<{ name: string; price: string }>({
  name: "宝马",
  price: "40w",
});
function changeCarPrice() {
  car.price = "80w";
}
</script>
复制代码

采用 Vue3 自定义 Hook 的方式,进一步拆分

<template>
  <p>{{ person.name }}</p>
  <p>{{ car.name }}</p>
  <p>{{ animal.name }}</p>
</template>
​
<script lang="ts" setup>
import { usePerson, useCar, useAnimal } from "./hooks";
​
const { person, changePersonName } = usePerson();
​
const { car } = useCar();
</script>
复制代码
// usePerson.ts
import { reactive, watch } from "vue";
​
export default function usePerson() {
  const person = reactive<{ name: string; sex: string }>({
    name: "小明",
    sex: "male",
  });
  watch(
    () => [person.name, person.sex],
    ([nameVal, sexVal]) => {
      console.log(`名字被修改了, 修改为 ${nameVal}`);
      console.log(`名字被修改了, 修改为 ${sexVal}`);
    }
  );
  function changePersonName() {
    person.name = "小浪";
  }
  return {
    person,
    changePersonName,
  };
}
复制代码
// useCar.ts
import { reactive } from "vue";
​
export default function useCar() {
  const car = reactive<{ name: string; price: string }>({
    name: "宝马",
    price: "40w",
  });
  function changeCarPrice() {
    car.price = "80w";
  }
  return {
    car,
    changeCarPrice,
  };
}
复制代码

对比完之后,我们会发现,Vue3 可以让我们更好组织代码。personcar的逻辑都被单独放置在一块

仅仅是代码组织的优势吗?不不不,我们再看看模板中的一些变化

  • <template>标签中起始便签可以不用<div>标签,因为Vue3提供了片段的能力,使用Vue-detools中查看,可以看到有fragment的标记 image.png
  • Watch也不需要罗列多个了,Vue3中支持侦听多个源
  • 很自然的使用TypeScript,类型约束更加简单

性能方面

Vue3 主要在这几个方面进行了提升

  1. 编译阶段。对 diff 算法优化、静态提升等等
  2. 响应式系统。Proxy()替代Object.defineProperty()监听对象。监听一个对象,不需要再深度遍历,Proxy()就可以劫持整个对象
  3. 体积包减少。Compostion API 的写法,可以更好的进行 tree shaking,减少上下文没有引入的代码,减少打包后的文件体积
  4. 新增片段特性。Vue 文件的<template>标签内,不再需要强制声明一个的<div>标签,节省额外的节点开销

29:vue2和vue3劫持数据的不同之处

vue2使用Object.defineProperty(),当我们通过数组的下标去修改数组值,或者向对象添加添加属性时,对应的视图都无法进行更新,这是因为我们所添加的数据不是响应式的,无法响应视图的变化

  1. Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  2. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。

我们可以通过 set来实现。对于数组可以通过this.set来实现。对于数组可以通过this.set(this.arr,0,‘xx’)来去修改数组下标对应的值,对于对象可以通过this.$set(this.obj,‘a’,‘xxx’)向对象新增属性a

vue3使用proxy代理,Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性