ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

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

Open ArthurWangCN opened 1 year ago

ArthurWangCN commented 1 year ago

简单说一说你对vuex理解?

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

我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念。

Vuex 解决状态管理的同时引入了不少概念:例如state、mutation、action等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。


vuex 为什么要区分 actions 和 mutations?

官方文档说明:

“在 mutations 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutations 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,我们将全部的改变都用同步方式实现。我们将全部的异步操作都放在 Actions 中。”

尤雨溪的回答:

区分 actions 和 mutations 并不是为了解决竞态问题,而是为了能用 devtools 追踪状态变化。

事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutations 就行。异步竞态怎么处理那是用户自己的事情。vuex 真正限制你的只有 mutations 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。

同步的意义在于这样每一个 mutations 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。

如果你开着 devtool 调用一个异步的 actions,你可以清楚地看到它所调用的 mutations 是何时被记录下来的,并且可以立刻查看它们对应的状态。

ArthurWangCN commented 1 year ago

从 template 到 render 处理过程

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

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

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


源码:

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  // ...
  const ast = isString(template) ? baseParse(template, options) : template
  // ...
  transform(ast, xxx )
  return generate(ast, xxx)
}
ArthurWangCN commented 1 year ago

Vue实例挂载的过程中发生了什么


mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  // ...
  const vnode = createVNode(
    rootComponent as ConcreteComponent,
    rootProps
  )
  // ...
  if (isHydrate && hydrate) {
    hydrate(vnode as VNode<Node, Element>, rootContainer as any)
  } else {
    render(vnode, rootContainer, isSVG)
  }
  isMounted = true
  app._container = rootContainer
  // ...
}

render.ts

const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }
ArthurWangCN commented 1 year ago

Vue 3.0的设计目标是什么,做了哪些优化

Vue3的最大设计目标是替代Vue2,为了实现这一点,Vue3在以下几个方面做了很大改进,如:易用性、框架性能、扩展性、可维护性、开发体验等

  1. 易用性方面主要是API简化,比如v-model在Vue3中变成了Vue2中v-model和sync修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成VNode的h(type, props, children),其中props不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。
  2. 开发体验方面,新组件Teleport传送门、Fragments 、Suspense等都会简化特定场景的代码编写,SFC Composition API语法糖更是极大提升我们开发体验。
  3. 扩展性方面提升如独立的reactivity模块,custom renderer API等
  4. 可维护性方面主要是Composition API,更容易编写高复用性的业务逻辑。还有对TypeScript支持的提升。
  5. 性能方面的改进也很显著,例如编译期优化、基于Proxy的响应式系统

Vue3做了哪些编译优化?

ArthurWangCN commented 1 year ago

Vue性能优化方法

ArthurWangCN commented 1 year ago

Vue组件为什么只能有一个根元素

vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。

之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom,自然应该满足这个要求。

vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。


patch方法接收单根vdom:

// 直接获取type等,没有考虑数组的可能性
const { type, ref, shapeFlag } = n2

patch方法对Fragment的处理:

mountChildren(n2.children as VNodeArrayChildren, container, ...)
ArthurWangCN commented 1 year ago

vuex的module

用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护。

可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:createStore({modules:{...}})

不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时getters、mutations和actions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace选项,此时再访问它们就要加上命名空间前缀。

很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。

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 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action

Pinia没有modules,如果想使用多个store,直接定义多个store传入不同的id即可,如:

import { defineStore } from "pinia";

export const storeA = defineStore("storeA", {...});
export const storeB = defineStore("storeB", {...});
export const storeC = defineStore("storeB", {...});
ArthurWangCN commented 1 year ago

怎么实现路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。

一般来说,对所有的路由都使用动态导入是个好主意。

给component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如: { path: '/users/:id', component: () => import('./views/UserDetails') }

结合注释 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack代码分块 vite中结合rollupOptions

路由中不能使用异步组件


component (和 components) 配置如果接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。

if (isRouteComponent(rawComponent)) {
    // __vccOpts is added by vue-class-component and contain the regular options
    const options: ComponentOptions =
      (rawComponent as any).__vccOpts || rawComponent
    const guard = options[guardType]
    guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
  } else {
    // start requesting the chunk already
    let componentPromise: Promise<
      RouteComponent | null | undefined | void
    > = (rawComponent as Lazy<RouteComponent>)()
    // ...
}
ArthurWangCN commented 1 year ago

ref和reactive异同


reactive:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // ...
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

ref:

class RefImpl<T> {
  // ...
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newVal) {
    // ...
    this._rawValue = newVal
    this._value = useDirectValue ? newVal : toReactive(newVal)
    triggerRefValue(this, newVal)
    // ...
  }
}
ArthurWangCN commented 1 year ago

watch和watchEffect异同


watchEffect定义:

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}

watch定义:

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

很明显watchEffect就是一种特殊的watch实现。