ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

50+Vue经典面试题源码级详解(二) #46

Open ArthurWangCN opened 1 year ago

ArthurWangCN commented 1 year ago

你知道哪些vue3新特性

api层面Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense

另外,Vue3.0在框架层面也有很多亮眼的改进:

ArthurWangCN commented 1 year ago

怎么定义动态路由

很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。

例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数

路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。

参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query、$route.hash 等。


响应路由参数的变化:

使用带有参数的路由时需要注意的是,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。

要对同一个组件中参数的变化做出响应的话,你可以简单地 watch $route 对象上的任意属性,在这个场景中,就是 $route.params :

// vue2
const User = {
  template: '...',
  watch: {
    $route(to, from) {
      // 对路由变化作出响应...
    }
  }
}

// vue3
const User = {
  template: '...',
  created() {
    this.$watch(
      () => this.$route.params,
      (toParams, previousParams) => {
        // 对路由变化做出响应...
      }
    )
  },
}

或者,使用 beforeRouteUpdate 导航守卫:

const User = {
  template: '...',
  beforeRouteUpdate(to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}
ArthurWangCN commented 1 year ago

写一个vue路由的思路

思路分析:

首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。


回答:

一个SPA应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:

首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事:

  1. 保存用户传入的配置项
  2. 监听hash或者popstate事件
  3. 回调里根据path匹配对应路由

将router定义成一个Vue插件,即实现install方法,内部做两件事:

  1. 实现两个全局组件:router-link和router-view,分别实现页面跳转和内容显示
  2. 定义两个全局变量:$route和$router,组件内可以访问当前路由和路由器实例

export function createRouter(options: RouterOptions): Router {
  // ...
  const router: Router = {
    // ...
    push,
    replace,
    go,
    back: () => go(-1),
    forward: () => go(1),
    // ...
    install(app: App) {
        // ... 
    }
  }
  return router
}

install方法:

install(app: App) {
  const router = this
  app.component('RouterLink', RouterLink)
  app.component('RouterView', RouterView)

  app.config.globalProperties.$router = router
  Object.defineProperty(app.config.globalProperties, '$route', {
    enumerable: true,
    get: () => unref(currentRoute),
  })
  // ...
}
ArthurWangCN commented 1 year ago

key的作用

key的作用主要是为了更高效的更新虚拟DOM

vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。

实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。


源码:

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}
ArthurWangCN commented 1 year ago

nextTick的使用和原理

nextTick是等待下一次 DOM 更新刷新的工具方法。

Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。

开发时,有两个场景我们会用到nextTick:

nextTick签名如下:function nextTick(callback?: () => void): Promise<void>

所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事。

在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。

ArthurWangCN commented 1 year ago

watch和computed的区别

  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed和methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑。
  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deep、immediate等选项。
  4. vue3中watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watch、watchEffect可以完全替代目前的watch选项,且功能更加强大。
ArthurWangCN commented 1 year ago

Vue 子组件和父组件创建和挂载顺序

父beforeCreate→ 父created→ 父beforeMounte→ 子beforCreate→ 子created→ 子beforeMount→ 父mounted

ArthurWangCN commented 1 year ago

怎么缓存当前的组件?缓存后怎么更新?

开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

<keep-alive>
  <component :is="view"></component>
</keep-alive>

结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive:

<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>

缓存后如果要获取数据,解决方案可以有以下两种:

activated(){
  this.getData() // 获取数据
}

keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。

ArthurWangCN commented 1 year ago

从0到1自己构架一个vue项目

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

  1. 目前vue3项目我会用vite或者create-vue创建项目
  2. 接下来引入必要插件:路由插件vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus和antd-vue、http工具我会选axios
  3. 其他比较常用的库有vueuse,nprogress,图标可以使用vite-svg-loader
  4. 下面是代码规范:结合prettier和eslint即可
  5. 最后是提交规范,可以使用husky,lint-staged,commitlint

目录结构我有如下习惯:

ArthurWangCN commented 1 year ago

总结的vue最佳实践有哪些

我从编码风格、性能、安全等方面说几条:

编码风格方面:

性能方面:

安全: