ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

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

Open ArthurWangCN opened 2 years ago

ArthurWangCN commented 2 years ago

Vue组件之间通信方式

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

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

ArthurWangCN commented 2 years ago

v-if和v-for哪个优先级更高

不应该把v-for和v-if放一起。

在vue2中,v-for的优先级是高于v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件;在vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常。

通常有两种情况下导致我们这样做:

ArthurWangCN commented 2 years ago

Vue 的生命周期

每个Vue组件实例被创建后都会经过一系列初始化步骤,比如,它需要数据观测,模板编译,挂载实例到dom上,以及数据变化时更新dom。这个过程中会运行叫做生命周期钩子的函数,以便用户在特定阶段有机会添加他们自己的代码。

Vue生命周期总共可以分为8个阶段:创建前后, 载入前后, 更新前后, 销毁前后,以及一些特殊场景的生命周期(activated、deactivated、errorCaptured)。

vue3中变更了销毁前后钩子名(beforeUnmount、unmounted),新增了三个用于调试和服务端渲染场景(renderTracked、renderTriggered、serverPrefetch)。

结合实践:

vue2相关源码:

initLifeCycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjection(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')

vue3相关源码:

export function applyOptions(instance: ComponentInternalInstance) {
    // ....
}

setup中为什么没有beforeCreate和created?

setup最先执行,此时组件实例在setup内部已经创建,所以created的处理对于setup来讲明显在后面,对于开发者来说已经没有意义, 所以setup中没必要再使用beforeCreate和created。官方对 setup 中的 beforeCreate 和 created 给的解释是 not needed,也就是说不需要显式地定义它们。

ArthurWangCN commented 2 years ago

双向绑定使用和原理

渲染函数:

// <input type="text" v-model="foo">
_c('input', { 
  directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], 
  attrs: { "type": "text" }, 
  domProps: { "value": (foo) }, 
  on: { 
    "input": function ($event) { 
      if ($event.target.composing) return; 
      foo = $event.target.value 
    } 
  } 
})
ArthurWangCN commented 2 years ago

Vue中如何扩展一个组件

常见的组件扩展方法有:mixins,slots,extends等

  1. 混入mixins是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
const mymixin = {}
Vue.mixin(mymixin)
mixins: [mymixin]
  1. 插槽主要用于vue组件中的内容分发,也可以用于组件扩展。
  2. 组件选项中还有extends也可以起到扩展组件的目的
{ extends: myextends }

混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,vue3中引入的composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用,增强代码的可读性和维护性。

ArthurWangCN commented 2 years ago

子组件可以直接改变父组件的数据吗

所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告。

实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。

ArthurWangCN commented 2 years ago

Vue权限管理该怎么做

权限管理一般需求是页面权限按钮权限的管理

具体实现的时候分后端和前端两种方案:

按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。

纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!


服务端返回的路由信息如何添加到路由器中

// 前端组件名和组件映射表
const map = {
  //xx: require('@/views/xx.vue').default // 同步的⽅式
  xx: () => import('@/views/xx.vue') // 异步的⽅式
}
// 服务端返回的asyncRoutes
const asyncRoutes = [
  { path: '/xx', component: 'xx',... }
]
// 遍历asyncRoutes,将component替换为map[component]
function mapComponent(asyncRoutes) {
  asyncRoutes.forEach(route => {
    route.component = map[route.component];
    if(route.children) {
      route.children.map(child => mapComponent(child))
    }
    })
}
mapComponent(asyncRoutes)
ArthurWangCN commented 2 years ago

对vue响应式的理解

所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制

MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。

以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。

vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。

为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。


vue3相关源码:

reactive:

function createReactiveObject() {
  // ...
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

ref:

class RefImpl<T> {
  // ...
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }
  // ...
}
ArthurWangCN commented 2 years ago

对虚拟 DOM 的理解

虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构

通过引入vdom我们可以获得如下好处:

  1. 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
  2. 方便实现跨平台(比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等)

vdom如何生成:在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。

挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。


vnode定义:

export interface VNode {
  // ...
}
ArthurWangCN commented 2 years ago

了解diff算法吗

Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。

最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。

vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。

patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例:

vue3中引入的更新策略:编译期优化patchFlags、block等


源码:

const patch: PatchFn = (xxx) {
  // ...
  switch (type) {
    case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      // ...
  }
  // ...
}