ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

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

Open ArthurWangCN opened 2 years ago

ArthurWangCN commented 2 years ago

怎么监听vuex数据的变化

vuex数据状态是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()。

我知道几种方法:

watch选项方式,可以以字符串形式监听 $store.state.xx;subscribe方式,可以调用 store.subscribe(cb) ,回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。

watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中。

watch方式:

const app = createApp({
  watch: {
    '$store.state.counter'() {
      console.log('counter change!');
    }
  }
})

subscribe方式:

store.subscribe((mutation, state) => {
  if (mutation.type === 'add') {
    console.log('counter change in subscribe()!');
  }
})
ArthurWangCN commented 2 years ago

router-link和router-view是如何起作用的

vue-router中两个重要组件router-link和router-view,分别起到路由导航作用和组件内容渲染作用

使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。

router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。


router-link:

return () => {
      const children = slots.default && slots.default(link)
      return props.custom
        ? children
        : h(
            'a',
            {
              'aria-current': link.isExactActive
                ? props.ariaCurrentValue
                : null,
              href: link.href,
              // this would override user added attrs but Vue will still add
              // the listener, so we end up triggering both
              onClick: link.navigate,
              class: elClass.value,
            },
            children
          )
    }

router-view:

return (
        // pass the vnode to the slot as a prop.
        // h and <component :is="..."> both accept vnodes
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
ArthurWangCN commented 2 years ago

vue-router 跳转

vue-router导航有两种方式:声明式导航和编程方式导航

声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定path、name、params等信息

如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航

实际上内部两者调用的导航函数是一样的:

routerlink点击跳转,调用的是navigate方法。navigate内部依然调用的push。

ArthurWangCN commented 2 years ago

为什么要用 Proxy 替代 defineProperty

JS中做属性拦截常见的方式有三:: defineProperty,getter/setters 和Proxies。

Vue2中使用defineProperty的原因是,2013年时只能用这种方式。由于该API存在一些局限性,比如对于数组的拦截有问题,为此vue需要专门为数组响应式做一套实现。另外不能拦截那些新增、删除属性;最后defineProperty方案在初始化时需要深度递归遍历待处理的对象才能对它进行完全拦截,明显增加了初始化的时间。

以上两点在Proxy出现之后迎刃而解,不仅可以对数组实现拦截,还能对Map、Set实现拦截;另外Proxy的拦截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用都改善了。

当然Proxy是有兼容性问题的,IE完全不支持,所以如果需要IE兼容就不合适


Proxy属性拦截的原理:利用get、set、deleteProperty这三个trap实现拦截

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {},
        set(target, key, val) {},
        deleteProperty(target, key){}
    })
}

Object.defineProperty属性拦截原理:利用get、set这两个trap实现拦截

function defineReactive(obj, key, val) {
    Object.defineReactive(obj, key, {
        get(key) {},
        set(key, val) {}
    })
}
ArthurWangCN commented 2 years ago

History模式和Hash模式有何区别

vue-router有3个模式,其中history和hash更为常用。两者差别主要在显示形式、seo和部署上。

hash模式在地址栏显示的时候是已哈希的形式:#/xxx,这种方式使用和部署简单,但是不会被搜索引擎处理,seo有问题;history模式则建议用在大部分web项目上,但是它要求应用在部署时做特殊配置,服务器需要做回退处理,否则会出现刷新页面404的问题。

底层实现上其实hash是一种特殊的history实现。

export function createWebHashHistory(base?: string): RouterHistory {
  // Make sure this implementation is fine in terms of encoding, specially for IE11
  // for `file://`, directly use the pathname and ignore the base
  // location.pathname contains an initial `/` even at the root: `https://example.com`
  base = location.host ? base || location.pathname + location.search : ''
  // allow the user to provide a `#` in the middle: `/base/#/app`
  if (!base.includes('#')) base += '#'

  if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {
    warn(
      `A hash base must end with a "#":\n"${base}" should be "${base.replace(
        /#.*$/,
        '#'
      )}".`
    )
  }
  return createWebHistory(base)
}
ArthurWangCN commented 2 years ago

什么场景下会用到嵌套路由

平时开发中,应用的有些界面是由多层级组件组合而来的,这种情况下,url各部分通常对应某个嵌套的组件,vue-router中可以使用嵌套路由表示这种关系

表现形式是在两个路由间切换时,它们有公用的视图内容。此时通常提取一个父组件,内部放上,从而形成物理上的嵌套,和逻辑上的嵌套对应起来

定义嵌套路由时使用children属性组织嵌套关系

原理上是在router-view组件内部判断当前router-view处于嵌套层级的深度,讲这个深度作为匹配组件数组matched的索引,获取对应渲染组件,渲染之

ArthurWangCN commented 2 years ago

页面刷新后vuex的state数据丢失怎么解决

vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来。

localStorage就很合适,提交mutation的时候同时存入localStorage,store中把值取出作为state的初始值即可。

这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。

类似的插件有vuex-persist、vuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化

ArthurWangCN commented 2 years ago

你觉得vuex有什么缺点

vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档

比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,getters,mutations,actions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。

之前Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合。


下面我们来看看vuex中store.state.x.y这种嵌套的路径是怎么搞出来的。

首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state。

if (!isRoot && !hot) {
    // 获取父模块state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 获取子模块名称
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
        // 把子模块state设置到父模块上
        parentState[moduleName] = module.state
    })
}
ArthurWangCN commented 2 years ago

Composition API 与 Options API 有什么不同

Composition API是一组API,包括:Reactivity API、生命周期钩子、依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件。

Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options API中mixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixins和provide/inject上。

Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益。

ArthurWangCN commented 2 years ago

vue-router中如何保护路由

vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。

路由守卫有三个级别:全局,路由独享,组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。

用户的任何导航行为都会走navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。


runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航。

return (
      runGuardQueue(guards)
        .then(() => {
          // check global guards beforeEach
          guards = []
          for (const guard of beforeGuards.list()) {
            guards.push(guardToPromiseFn(guard, to, from))
          }
          guards.push(canceledNavigationCheck)

          return runGuardQueue(guards)
        })
        // ...