Vue3有理解过吗?能说说跟vue2的区别吗?

1. 哪些变动

从上图中,咱们能够概览Vue3的新个性,如下:

  • 速度更快
  • 体积缩小
  • 更易保护
  • 更靠近原生
  • 更易使用

1.1 速度更快

vue3相比vue2

  • 重写了虚构Dom实现
  • 编译模板的优化
  • 更高效的组件初始化
  • undate性能进步1.3~2倍
  • SSR速度进步了2~3倍

1.2 体积更小

通过webpacktree-shaking性能,能够将无用模块“剪辑”,仅打包须要的

可能tree-shaking,有两大益处:

  • 对开发人员,可能对vue实现更多其余的性能,而不用担心整体体积过大
  • 对使用者,打包进去的包体积变小了

vue能够开发出更多其余的性能,而不用担心vue打包进去的整体体积过多

1.3 更易保护

compositon Api

  • 可与现有的Options API一起应用
  • 灵便的逻辑组合与复用
  • Vue3模块能够和其余框架搭配应用

更好的Typescript反对

VUE3是基于typescipt编写的,能够享受到主动的类型定义提醒

1.4 编译器重写

1.5 更靠近原生

能够自定义渲染 API

1.6 更易使用

响应式 Api 裸露进去

轻松辨认组件从新渲染起因

2. Vue3新增个性

Vue 3 中须要关注的一些新性能包含:

  • framents
  • Teleport
  • composition Api
  • createRenderer

2.1 framents

Vue3.x 中,组件当初反对有多个根节点

<!-- Layout.vue --><template>  <header>...</header>  <main v-bind="$attrs">...</main>  <footer>...</footer></template>

2.2 Teleport

Teleport 是一种可能将咱们的模板挪动到 DOMVue app 之外的其余地位的技术,就有点像哆啦A梦的“任意门”

vue2中,像 modals,toast 等这样的元素,如果咱们嵌套在 Vue 的某个组件外部,那么解决嵌套组件的定位、z-index 和款式就会变得很艰难

通过Teleport,咱们能够在组件的逻辑地位写模板代码,而后在 Vue 利用范畴之外渲染它

<button @click="showToast" class="btn">关上 toast</button><!-- to 属性就是指标地位 --><teleport to="#teleport-target">    <div v-if="visible" class="toast-wrap">        <div class="toast-msg">我是一个 Toast 文案</div>    </div></teleport>

2.3 createRenderer

通过createRenderer,咱们可能构建自定义渲染器,咱们可能将 vue 的开发模型扩大到其余平台

咱们能够将其生成在canvas画布上

对于createRenderer,咱们理解下根本应用,就不开展讲述了

import { createRenderer } from '@vue/runtime-core'const { render, createApp } = createRenderer({  patchProp,  insert,  remove,  createElement,  // ...})export { render, createApp }export * from '@vue/runtime-core'

2.4 composition Api

composition Api,也就是组合式api,通过这种模式,咱们可能更加容易保护咱们的代码,将雷同性能的变量进行一个集中式的治理

对于compositon api的应用,这里以下图开展

简略应用:

export default {    setup() {        const count = ref(0)        const double = computed(() => count.value * 2)        function increment() {            count.value++        }        onMounted(() => console.log('component mounted!'))        return {            count,            double,            increment        }    }}

3. 非兼容变更

3.1 Global API

  • 全局 Vue API 已更改为应用应用程序实例
  • 全局和外部 API 曾经被重构为可 tree-shakable

3.2 模板指令

  • 组件上 v-model 用法已更改
  • <template v-for>和 非 v-for节点上key用法已更改
  • 在同一元素上应用的 v-ifv-for 优先级已更改
  • v-bind="object" 当初排序敏感
  • v-for 中的 ref 不再注册 ref 数组

3.3 组件

  • 只能应用一般函数创立性能组件
  • functional 属性在单文件组件 (SFC)
  • 异步组件当初须要 defineAsyncComponent 办法来创立

3.4 渲染函数

  • 渲染函数API扭转
  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数裸露
  • 自定义指令 API 已更改为与组件生命周期统一
  • 一些转换class被重命名了:

    • v-enter -> v-enter-from
    • v-leave -> v-leave-from
  • 组件 watch 选项和实例办法 $watch不再反对点分隔字符串门路,请改用计算函数作为参数
  • Vue 2.x 中,利用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 当初应用应用程序容器的 innerHTML

3.5 其余小扭转

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
  • [prop default工厂函数不再有权拜访 this 是上下文
  • 自定义指令 API 已更改为与组件生命周期统一
  • data 应始终申明为函数
  • 来自 mixindata 选项当初可简略地合并
  • attribute 强制策略已更改
  • 一些过渡 class 被重命名
  • 组建 watch 选项和实例办法 $watch不再反对以点分隔的字符串门路。请改用计算属性函数作为参数。
  • <template> 没有非凡指令的标记 (v-if/else-if/elsev-forv-slot) 当初被视为一般元素,并将生成原生的 <template> 元素,而不是渲染其外部内容。
  • Vue 2.x 中,利用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 当初应用利用容器的 innerHTML,这意味着容器自身不再被视为模板的一部分。

3.6 移除 API

  • keyCode 反对作为 v-on 的修饰符
  • $on$off$once 实例办法
  • 过滤filter
  • 内联模板 attribute
  • $destroy 实例办法。用户不应再手动治理单个Vue 组件的生命周期。

vue-router守卫

导航守卫 router.beforeEach 全局前置守卫
  • to: Route: 行将要进入的指标(路由对象)
  • from: Route: 以后导航正要来到的路由
  • next: Function: 肯定要调用该办法来 resolve 这个钩子。(肯定要用这个函数能力去到下一个路由,如果不必就拦挡)
  • 执行成果依赖 next 办法的调用参数。
  • next(): 进行管道中的下一个钩子。如果全副钩子执行完了,则导航的状态就是 confirmed (确认的)。
  • next(false):勾销进入路由,url地址重置为from路由地址(也就是将要来到的路由地址)
// main.js 入口文件import router from './router'; // 引入路由router.beforeEach((to, from, next) => {   next();});router.beforeResolve((to, from, next) => {  next();});router.afterEach((to, from) => {  console.log('afterEach 全局后置钩子');});
路由独享的守卫 你能够在路由配置上间接定义 beforeEnter 守卫
const router = new VueRouter({  routes: [    {      path: '/foo',      component: Foo,      beforeEnter: (to, from, next) => {        // ...      }    }  ]})
组件内的守卫你能够在路由组件内间接定义以下路由导航守卫
const Foo = {  template: `...`,  beforeRouteEnter (to, from, next) {    // 在渲染该组件的对应路由被 confirm 前调用    // 不!能!获取组件实例 `this`    // 因为当守卫执行前,组件实例还没被创立  },  beforeRouteUpdate (to, from, next) {    // 在以后路由扭转,然而该组件被复用时调用    // 举例来说,对于一个带有动静参数的门路 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,    // 因为会渲染同样的 Foo 组件,因而组件实例会被复用。而这个钩子就会在这个状况下被调用。    // 能够拜访组件实例 `this`  },  beforeRouteLeave (to, from, next) {    // 导航来到该组件的对应路由时调用,咱们用它来禁止用户来到    // 能够拜访组件实例 `this`    // 比方还未保留草稿,或者在用户来到前,    将setInterval销毁,避免来到之后,定时器还在调用。  }}

defineProperty和proxy的区别

Vue 在实例初始化时遍历 data 中的所有属性,并应用 Object.defineProperty 把这些属性全副转为 getter/setter。这样当追踪数据发生变化时,setter 会被主动调用。

Object.defineProperty 是 ES5 中一个无奈 shim 的个性,这也就是 Vue 不反对 IE8 以及更低版本浏览器的起因。

然而这样做有以下问题:

  1. 增加或删除对象的属性时,Vue 检测不到。因为增加或删除的对象没有在初始化进行响应式解决,只能通过$set 来调用Object.defineProperty()解决。
  2. 无奈监控到数组下标和长度的变动。

Vue3 应用 Proxy 来监控数据的变动。Proxy 是 ES6 中提供的性能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。绝对于Object.defineProperty(),其有以下特点:

  1. Proxy 间接代理整个对象而非对象属性,这样只需做一层代理就能够监听同级构造下的所有属性变动,包含新增属性和删除属性。
  2. Proxy 能够监听数组的变动。

ref和reactive异同

这是Vue3数据响应式中十分重要的两个概念,跟咱们写代码关系也很大

const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1const obj = reactive({ count: 0 })obj.count++
  • ref接管外部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
  • 从定义上看ref通常用于解决单值的响应式,reactive用于解决对象类型的数据响应式
  • 两者均是用于结构响应式数据,然而ref次要解决原始值的响应式问题
  • ref返回的响应式数据在JS中应用须要加上.value能力拜访其值,在视图中应用会主动脱ref,不须要.valueref能够接管对象或数组等非原始值,但外部仍然是reactive实现响应式;reactive外部如果接管Ref对象会主动脱ref;应用开展运算符(...)开展reactive返回的响应式对象会使其失去响应性,能够联合toRefs()将值转换为Ref对象之后再开展。
  • reactive外部应用Proxy代理传入对象并拦挡该对象各种操作,从而实现响应式。ref外部封装一个RefImpl类,并设置get value/set value,拦挡用户对值的拜访,从而实现响应式

参考:前端vue面试题具体解答

异步组件是什么?应用场景有哪些?

剖析

因为异步路由的存在,咱们应用异步组件的次数比拟少,因而还是有必要两者的不同。

体验

大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们

import { defineAsyncComponent } from 'vue'// defineAsyncComponent定义异步组件,返回一个包装组件。包装组件依据加载器的状态决定渲染什么内容const AsyncComp = defineAsyncComponent(() => {  // 加载函数返回Promise  return new Promise((resolve, reject) => {    // ...能够从服务器加载组件    resolve(/* loaded component */)  })})// 借助打包工具实现ES模块动静导入const AsyncComp = defineAsyncComponent(() =>  import('./components/MyComponent.vue'))

答复范例

  1. 在大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们。
  2. 咱们不仅能够在路由切换时懒加载组件,还能够在页面组件中持续应用异步组件,从而实现更细的宰割粒度。
  3. 应用异步组件最简略的形式是间接给defineAsyncComponent指定一个loader函数,联合ES模块动静导入函数import能够疾速实现。咱们甚至能够指定loadingComponenterrorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还能够联合Suspense组件应用异步组件。
  4. 异步组件容易和路由懒加载混同,实际上不是一个货色。异步组件不能被用于定义懒加载路由上,解决它的是vue框架,解决路由组件加载的是vue-router。然而能够在懒加载的路由组件中应用异步组件

computed和watch有什么区别?

computed:

  1. computed是计算属性,也就是计算值,它更多用于计算值的场景
  2. computed具备缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值扭转之后,下一次获取computed的值时才会从新调用对应的getter来计算
  3. computed实用于计算比拟耗费性能的计算场景

watch:

  1. 更多的是「察看」的作用,相似于某些数据的监听回调,用于察看props $emit或者本组件的值,当数据变动时来执行回调进行后续操作
  2. 无缓存性,页面从新渲染时值不变动也会执行

小结:

  1. 当咱们要进行数值计算,而且依赖于其余数据,那么把这个数据设计为computed
  2. 如果你须要在某个数据变动时做一些事件,应用watch来察看这个数据变动

$route$router的区别

  • $route是“路由信息对象”,包含pathparamshashqueryfullPathmatchedname等路由信息参数。
  • $router是“路由实例”对象包含了路由的跳转办法,钩子函数等

vue2.x具体

1. 剖析

首先找到vue的构造函数

源码地位:src\core\instance\index.js

function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  this._init(options)}

options是用户传递过去的配置项,如data、methods等罕用的办法

vue构建函数调用_init办法,但咱们发现本文件中并没有此办法,但认真能够看到文件下方定定义了很多初始化办法

initMixin(Vue);     // 定义 _initstateMixin(Vue);    // 定义 $set $get $delete $watch 等eventsMixin(Vue);   // 定义事件  $on  $once $off $emitlifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroyrenderMixin(Vue);   // 定义 _render 返回虚构dom

首先能够看initMixin办法,发现该办法在Vue原型上定义了_init办法

源码地位:src\core\instance\init.js

Vue.prototype._init = function (options?: Object) {    const vm: Component = this    // a uid    vm._uid = uid++    let startTag, endTag    /* istanbul ignore if */    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {      startTag = `vue-perf-start:${vm._uid}`      endTag = `vue-perf-end:${vm._uid}`      mark(startTag)    }    // a flag to avoid this being observed    vm._isVue = true    // merge options    // 合并属性,判断初始化的是否是组件,这里合并次要是 mixins 或 extends 的办法    if (options && options._isComponent) {      // optimize internal component instantiation      // since dynamic options merging is pretty slow, and none of the      // internal component options needs special treatment.      initInternalComponent(vm, options)    } else { // 合并vue属性      vm.$options = mergeOptions(        resolveConstructorOptions(vm.constructor),        options || {},        vm      )    }    /* istanbul ignore else */    if (process.env.NODE_ENV !== 'production') {      // 初始化proxy拦截器      initProxy(vm)    } else {      vm._renderProxy = vm    }    // expose real self    vm._self = vm    // 初始化组件生命周期标记位    initLifecycle(vm)    // 初始化组件事件侦听    initEvents(vm)    // 初始化渲染办法    initRender(vm)    callHook(vm, 'beforeCreate')    // 初始化依赖注入内容,在初始化data、props之前    initInjections(vm) // resolve injections before data/props    // 初始化props/data/method/watch/methods    initState(vm)    initProvide(vm) // resolve provide after data/props    callHook(vm, 'created')    /* istanbul ignore if */    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {      vm._name = formatComponentName(vm, false)      mark(endTag)      measure(`vue ${vm._name} init`, startTag, endTag)    }    // 挂载元素    if (vm.$options.el) {      vm.$mount(vm.$options.el)    }  }

仔细阅读下面的代码,咱们失去以下论断:

  • 在调用beforeCreate之前,数据初始化并未实现,像dataprops这些属性无法访问到
  • 到了created的时候,数据曾经初始化实现,可能拜访dataprops这些属性,但这时候并未实现dom的挂载,因而无法访问到dom元素
  • 挂载办法是调用vm.$mount办法

initState办法是实现props/data/method/watch/methods的初始化

源码地位:src\core\instance\state.js

export function initState (vm: Component) {  // 初始化组件的watcher列表  vm._watchers = []  const opts = vm.$options  // 初始化props  if (opts.props) initProps(vm, opts.props)  // 初始化methods办法  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    // 初始化data      initData(vm)  } else {    observe(vm._data = {}, true /* asRootData */)  }  if (opts.computed) initComputed(vm, opts.computed)  if (opts.watch && opts.watch !== nativeWatch) {    initWatch(vm, opts.watch)  }}

咱们和这里次要看初始化data的办法为initData,它与initState在同一文件上

function initData (vm: Component) {  let data = vm.$options.data  // 获取到组件上的data  data = vm._data = typeof data === 'function'    ? getData(data, vm)    : data || {}  if (!isPlainObject(data)) {    data = {}    process.env.NODE_ENV !== 'production' && warn(      'data functions should return an object:\n' +      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',      vm    )  }  // proxy data on instance  const keys = Object.keys(data)  const props = vm.$options.props  const methods = vm.$options.methods  let i = keys.length  while (i--) {    const key = keys[i]    if (process.env.NODE_ENV !== 'production') {      // 属性名不能与办法名反复      if (methods && hasOwn(methods, key)) {        warn(          `Method "${key}" has already been defined as a data property.`,          vm        )      }    }    // 属性名不能与state名称反复    if (props && hasOwn(props, key)) {      process.env.NODE_ENV !== 'production' && warn(        `The data property "${key}" is already declared as a prop. ` +        `Use prop default value instead.`,        vm      )    } else if (!isReserved(key)) { // 验证key值的合法性      // 将_data中的数据挂载到组件vm上,这样就能够通过this.xxx拜访到组件上的数据      proxy(vm, `_data`, key)    }  }  // observe data  // 响应式监听data是数据的变动  observe(data, true /* asRootData */)}

仔细阅读下面的代码,咱们能够失去以下论断:

  • 初始化程序:propsmethodsdata
  • data定义的时候可选择函数模式或者对象模式(组件只能为函数模式)

对于数据响应式在这就不开展具体阐明

上文提到挂载办法是调用vm.$mount办法

源码地位:

Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  // 获取或查问元素  el = el && query(el)  /* istanbul ignore if */  // vue 不容许间接挂载到body或页面文档上  if (el === document.body || el === document.documentElement) {    process.env.NODE_ENV !== 'production' && warn(      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`    )    return this  }  const options = this.$options  // resolve template/el and convert to render function  if (!options.render) {    let template = options.template    // 存在template模板,解析vue模板文件    if (template) {      if (typeof template === 'string') {        if (template.charAt(0) === '#') {          template = idToTemplate(template)          /* istanbul ignore if */          if (process.env.NODE_ENV !== 'production' && !template) {            warn(              `Template element not found or is empty: ${options.template}`,              this            )          }        }      } else if (template.nodeType) {        template = template.innerHTML      } else {        if (process.env.NODE_ENV !== 'production') {          warn('invalid template option:' + template, this)        }        return this      }    } else if (el) {      // 通过选择器获取元素内容      template = getOuterHTML(el)    }    if (template) {      /* istanbul ignore if */      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {        mark('compile')      }      /**       *  1.将temmplate解析ast tree       *  2.将ast tree转换成render语法字符串       *  3.生成render办法       */      const { render, staticRenderFns } = compileToFunctions(template, {        outputSourceRange: process.env.NODE_ENV !== 'production',        shouldDecodeNewlines,        shouldDecodeNewlinesForHref,        delimiters: options.delimiters,        comments: options.comments      }, this)      options.render = render      options.staticRenderFns = staticRenderFns      /* istanbul ignore if */      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {        mark('compile end')        measure(`vue ${this._name} compile`, 'compile', 'compile end')      }    }  }  return mount.call(this, el, hydrating)}

浏览下面代码,咱们能失去以下论断:

  • 不要将根元素放到body或者html
  • 能够在对象中定义template/render或者间接应用templateel示意元素选择器
  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

template的解析步骤大抵分为以下几步:

  • html文档片段解析成ast描述符
  • ast描述符解析成字符串
  • 生成render函数

生成render函数,挂载到vm上后,会再次调用mount办法

源码地位:src\platforms\web\runtime\index.js

// public mount methodVue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  // 渲染组件  return mountComponent(this, el, hydrating)}

调用mountComponent渲染组件

export function mountComponent (  vm: Component,  el: ?Element,  hydrating?: boolean): Component {  vm.$el = el  // 如果没有获取解析的render函数,则会抛出正告  // render是解析模板文件生成的  if (!vm.$options.render) {    vm.$options.render = createEmptyVNode    if (process.env.NODE_ENV !== 'production') {      /* istanbul ignore if */      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||        vm.$options.el || el) {        warn(          'You are using the runtime-only build of Vue where the template ' +          'compiler is not available. Either pre-compile the templates into ' +          'render functions, or use the compiler-included build.',          vm        )      } else {        // 没有获取到vue的模板文件        warn(          'Failed to mount component: template or render function not defined.',          vm        )      }    }  }  // 执行beforeMount钩子  callHook(vm, 'beforeMount')  let updateComponent  /* istanbul ignore if */  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {    updateComponent = () => {      const name = vm._name      const id = vm._uid      const startTag = `vue-perf-start:${id}`      const endTag = `vue-perf-end:${id}`      mark(startTag)      const vnode = vm._render()      mark(endTag)      measure(`vue ${name} render`, startTag, endTag)      mark(startTag)      vm._update(vnode, hydrating)      mark(endTag)      measure(`vue ${name} patch`, startTag, endTag)    }  } else {    // 定义更新函数    updateComponent = () => {      // 理论调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render      vm._update(vm._render(), hydrating)    }  }  // we set this to vm._watcher inside the watcher's constructor  // since the watcher's initial patch may call $forceUpdate (e.g. inside child  // component's mounted hook), which relies on vm._watcher being already defined  // 监听以后组件状态,当有数据变动时,更新组件  new Watcher(vm, updateComponent, noop, {    before () {      if (vm._isMounted && !vm._isDestroyed) {        // 数据更新引发的组件更新        callHook(vm, 'beforeUpdate')      }    }  }, true /* isRenderWatcher */)  hydrating = false  // manually mounted instance, call mounted on self  // mounted is called for render-created child components in its inserted hook  if (vm.$vnode == null) {    vm._isMounted = true    callHook(vm, 'mounted')  }  return vm}

浏览下面代码,咱们失去以下论断:

  • 会触发boforeCreate钩子
  • 定义updateComponent渲染页面视图的办法
  • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

updateComponent办法次要执行在vue初始化时申明的renderupdate办法

render的作用次要是生成vnode

源码地位:src\core\instance\render.js

// 定义vue 原型上的render办法Vue.prototype._render = function (): VNode {    const vm: Component = this    // render函数来自于组件的option    const { render, _parentVnode } = vm.$options    if (_parentVnode) {        vm.$scopedSlots = normalizeScopedSlots(            _parentVnode.data.scopedSlots,            vm.$slots,            vm.$scopedSlots        )    }    // set parent vnode. this allows render functions to have access    // to the data on the placeholder node.    vm.$vnode = _parentVnode    // render self    let vnode    try {        // There's no need to maintain a stack because all render fns are called        // separately from one another. Nested component's render fns are called        // when parent component is patched.        currentRenderingInstance = vm        // 调用render办法,本人的独特的render办法, 传入createElement参数,生成vNode        vnode = render.call(vm._renderProxy, vm.$createElement)    } catch (e) {        handleError(e, vm, `render`)        // return error render result,        // or previous vnode to prevent render error causing blank component        /* istanbul ignore else */        if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {            try {                vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)            } catch (e) {                handleError(e, vm, `renderError`)                vnode = vm._vnode            }        } else {            vnode = vm._vnode        }    } finally {        currentRenderingInstance = null    }    // if the returned array contains only a single node, allow it    if (Array.isArray(vnode) && vnode.length === 1) {        vnode = vnode[0]    }    // return empty vnode in case the render function errored out    if (!(vnode instanceof VNode)) {        if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {            warn(                'Multiple root nodes returned from render function. Render function ' +                'should return a single root node.',                vm            )        }        vnode = createEmptyVNode()    }    // set parent    vnode.parent = _parentVnode    return vnode}

_update次要性能是调用patch,将vnode转换为实在DOM,并且更新到页面中

源码地位:src\core\instance\lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {    const vm: Component = this    const prevEl = vm.$el    const prevVnode = vm._vnode    // 设置以后激活的作用域    const restoreActiveInstance = setActiveInstance(vm)    vm._vnode = vnode    // Vue.prototype.__patch__ is injected in entry points    // based on the rendering backend used.    if (!prevVnode) {      // initial render      // 执行具体的挂载逻辑      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)    } else {      // updates      vm.$el = vm.__patch__(prevVnode, vnode)    }    restoreActiveInstance()    // update __vue__ reference    if (prevEl) {      prevEl.__vue__ = null    }    if (vm.$el) {      vm.$el.__vue__ = vm    }    // if parent is an HOC, update its $el as well    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {      vm.$parent.$el = vm.$el    }    // updated hook is called by the scheduler to ensure that children are    // updated in a parent's updated hook.  }

2. 论断

  • new Vue的时候调用会调用_init办法

    • 定义 $set$get$delete$watch 等办法
    • 定义 $on$off$emit$off等事件
    • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候次要是通过mountComponent办法
  • 定义updateComponent更新函数
  • 执行render生成虚构DOM
  • _update将虚构DOM生成实在DOM构造,并且渲染到页面中

说说 vue 内置指令

你感觉vuex有什么毛病

剖析

相较于reduxvuex曾经相当简便好用了。但模块的应用比拟繁琐,对ts反对也不好。

体验

应用模块:用起来比拟繁琐,应用模式也不对立,基本上得不到类型零碎的任何反对

const store = createStore({  modules: {    a: moduleA  }})store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState应用store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的store.getters['a/c'] // -> 有namespaced时要加path,应用模式又和state不一样store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutationstore.commit('a/d') // -> 有namespaced时要加path,配合mapMutations应用感觉也没简化

答复范例

  1. vuex利用响应式,应用起来曾经相当方便快捷了。然而在应用过程中感觉模块化这一块做的过于简单,用的时候容易出错,还要常常查看文档
  2. 比方:拜访state时要带上模块key,内嵌模块的话会很长,不得不配合mapState应用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的path来匹配,应用模式不对立,容易出错;对ts的反对也不敌对,在应用模块时没有代码提醒。
  3. 之前Vue2我的项目中用过vuex-module-decorators的解决方案,尽管类型反对上有所改善,但又要学一套新货色,减少了学习老本。pinia呈现之后应用体验好了很多,Vue3 + pinia会是更好的组合

原理

上面咱们来看看vuexstore.state.x.y这种嵌套的门路是怎么搞进去的

首先是子模块装置过程:父模块状态parentState下面设置了子模块名称moduleName,值为以后模块state对象。放在下面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y装置时就是:store.state['x']['y'] = moduleY.state
//源码地位 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115if (!isRoot && !hot) {    // 获取父模块state    const parentState = getNestedState(rootState, path.slice(0, -1))    // 获取子模块名称    const moduleName = path[path.length - 1]    store._withCommit(() => {        // 把子模块state设置到父模块上        parentState[moduleName] = module.state    })}

v-show 与 v-if 有什么区别?

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的 “display” 属性进行切换。

所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。

Vue computed 实现

  • 建设与其余属性(如:dataStore)的分割;
  • 属性扭转后,告诉计算属性从新计算
实现时,次要如下
  • 初始化 data, 应用 Object.defineProperty 把这些属性全副转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,应用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性从新计算。
  • 若呈现以后 computed 计算属性嵌套其余 computed 计算属性时,先进行其余的依赖收集

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?

1)Vue为什么要用vm.$set() 解决对象新增属性不能响应的问题

  1. Vue应用了Object.defineProperty实现双向数据绑定
  2. 在初始化实例时对属性执行 getter/setter 转化
  3. 属性必须在data对象上存在能力让Vue将它转换为响应式的(这也就造成了Vue无奈检测到对象属性的增加或删除)

所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)

2)接下来咱们看看框架自身是如何实现的呢?

Vue 源码地位:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {  // target 为数组    if (Array.isArray(target) && isValidArrayIndex(key)) {    // 批改数组的长度, 防止索引>数组长度导致splcie()执行有误    target.length = Math.max(target.length, key)    // 利用数组的splice变异办法触发响应式      target.splice(key, 1, val)    return val  }  // key 曾经存在,间接批改属性值    if (key in target && !(key in Object.prototype)) {    target[key] = val    return val  }  const ob = (target: any).__ob__  // target 自身就不是响应式数据, 间接赋值  if (!ob) {    target[key] = val    return val  }  // 对属性进行响应式解决  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}

咱们浏览以上源码可知,vm.$set 的实现原理是:

  1. 如果指标是数组,间接应用数组的 splice 办法触发相应式;
  2. 如果指标是对象,会先判读属性是否存在、对象是否是响应式,
  3. 最终如果要对属性进行响应式解决,则是通过调用 defineReactive 办法进行响应式解决
defineReactive 办法就是 Vue 在初始化对象时,给对象属性采纳 Object.defineProperty 动静增加 getter 和 setter 的性能所调用的办法

Vue的diff算法详细分析

1. 是什么

diff 算法是一种通过同层的树节点进行比拟的高效算法

其有两个特点:

  • 比拟只会在同层级进行, 不会跨层级比拟
  • 在diff比拟的过程中,循环从两边向两头比拟

diff 算法在很多场景下都有利用,在 vue 中,作用于虚构 dom 渲染成实在 dom 的新旧 VNode 节点比拟

2. 比拟形式

diff整体策略为:深度优先,同层比拟

  1. 比拟只会在同层级进行, 不会跨层级比拟

  1. 比拟的过程中,循环从两边向两头收拢

上面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

第一次循环后,发现旧节点D与新节点D雷同,间接复用旧节点D作为diff后的第一个实在节点,同时旧节点endIndex挪动到C,新节点的 startIndex 挪动到了 C

第二次循环后,同样是旧节点的开端和新节点的结尾(都是 C)雷同,同理,diff 后创立了 C 的实在节点插入到第一次创立的 D 节点前面。同时旧节点的 endIndex 挪动到了 B,新节点的 startIndex 挪动到了 E

第三次循环中,发现E没有找到,这时候只能间接创立新的实在节点 E,插入到第二次创立的 C 节点之后。同时新节点的 startIndex 挪动到了 A。旧节点的 startIndexendIndex 都放弃不动

第四次循环中,发现了新旧节点的结尾(都是 A)雷同,于是 diff 后创立了 A 的实在节点,插入到前一次创立的 E 节点前面。同时旧节点的 startIndex 挪动到了 B,新节点的startIndex 挪动到了 B

第五次循环中,情景同第四次循环一样,因而 diff 后创立了 B 实在节点 插入到前一次创立的 A 节点前面。同时旧节点的 startIndex挪动到了 C,新节点的 startIndex 挪动到了 F

新节点的 startIndex 曾经大于 endIndex 了,须要创立 newStartIdxnewEndIdx 之间的所有节点,也就是节点F,间接创立 F 节点对应的实在节点放到 B 节点前面

3. 原理剖析

当数据产生扭转时,set办法会调用Dep.notify告诉所有订阅者Watcher,订阅者就会调用patch给实在的DOM打补丁,更新相应的视图

源码地位:src/core/vdom/patch.js

function patch(oldVnode, vnode, hydrating, removeOnly) {    if (isUndef(vnode)) { // 没有新节点,间接执行destory钩子函数        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)        return    }    let isInitialPatch = false    const insertedVnodeQueue = []    if (isUndef(oldVnode)) {        isInitialPatch = true        createElm(vnode, insertedVnodeQueue) // 没有旧节点,间接用新节点生成dom元素    } else {        const isRealElement = isDef(oldVnode.nodeType)        if (!isRealElement && sameVnode(oldVnode, vnode)) {            // 判断旧节点和新节点本身一样,统一执行patchVnode            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)        } else {            // 否则间接销毁及旧节点,依据新节点生成dom元素            if (isRealElement) {                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {                    oldVnode.removeAttribute(SSR_ATTR)                    hydrating = true                }                if (isTrue(hydrating)) {                    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {                        invokeInsertHook(vnode, insertedVnodeQueue, true)                        return oldVnode                    }                }                oldVnode = emptyNodeAt(oldVnode)            }            return vnode.elm        }    }}

patch函数前两个参数位为oldVnodeVnode ,别离代表新的节点和之前的旧节点,次要做了四个判断:

  • 没有新节点,间接触发旧节点的destory钩子
  • 没有旧节点,阐明是页面刚开始初始化的时候,此时,基本不须要比拟了,间接全是新建,所以只调用 createElm
  • 旧节点和新节点本身一样,通过 sameVnode 判断节点是否一样,一样时,间接调用 patchVnode去解决这两个节点
  • 旧节点和新节点本身不一样,当两个节点不一样的时候,间接创立新节点,删除旧节点

上面次要讲的是patchVnode局部

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {    // 如果新旧节点统一,什么都不做    if (oldVnode === vnode) {      return    }    // 让vnode.el援用到当初的实在dom,当el批改时,vnode.el会同步变动    const elm = vnode.elm = oldVnode.elm    // 异步占位符    if (isTrue(oldVnode.isAsyncPlaceholder)) {      if (isDef(vnode.asyncFactory.resolved)) {        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)      } else {        vnode.isAsyncPlaceholder = true      }      return    }    // 如果新旧都是动态节点,并且具备雷同的key    // 当vnode是克隆节点或是v-once指令管制的节点时,只须要把oldVnode.elm和oldVnode.child都复制到vnode上    // 也不必再有其余操作    if (isTrue(vnode.isStatic) &&      isTrue(oldVnode.isStatic) &&      vnode.key === oldVnode.key &&      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))    ) {      vnode.componentInstance = oldVnode.componentInstance      return    }    let i    const data = vnode.data    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {      i(oldVnode, vnode)    }    const oldCh = oldVnode.children    const ch = vnode.children    if (isDef(data) && isPatchable(vnode)) {      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)    }    // 如果vnode不是文本节点或者正文节点    if (isUndef(vnode.text)) {      // 并且都有子节点      if (isDef(oldCh) && isDef(ch)) {        // 并且子节点不完全一致,则调用updateChildren        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)        // 如果只有新的vnode有子节点      } else if (isDef(ch)) {        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')        // elm曾经援用了老的dom节点,在老的dom节点上增加子节点        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)        // 如果新vnode没有子节点,而vnode有子节点,间接删除老的oldCh      } else if (isDef(oldCh)) {        removeVnodes(elm, oldCh, 0, oldCh.length - 1)        // 如果老节点是文本节点      } else if (isDef(oldVnode.text)) {        nodeOps.setTextContent(elm, '')      }      // 如果新vnode和老vnode是文本节点或正文节点      // 然而vnode.text != oldVnode.text时,只须要更新vnode.elm的文本内容就能够    } else if (oldVnode.text !== vnode.text) {      nodeOps.setTextContent(elm, vnode.text)    }    if (isDef(data)) {      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)    }  }

patchVnode次要做了几个判断:

  • 新节点是否是文本节点,如果是,则间接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则解决比拟更新子节点
  • 只有新节点有子节点,旧节点没有,那么不必比拟了,所有节点都是全新的,所以间接全副新建就好了,新建是指创立出所有新DOM,并且增加进父节点
  • 只有旧节点有子节点而新节点没有,阐明更新后的页面,旧节点全副都不见了,那么要做的,就是把所有的旧节点删除,也就是间接把DOM 删除

子节点不完全一致,则调用updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {    let oldStartIdx = 0 // 旧头索引    let newStartIdx = 0 // 新头索引    let oldEndIdx = oldCh.length - 1 // 旧尾索引    let newEndIdx = newCh.length - 1 // 新尾索引    let oldStartVnode = oldCh[0] // oldVnode的第一个child    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最初一个child    let newStartVnode = newCh[0] // newVnode的第一个child    let newEndVnode = newCh[newEndIdx] // newVnode的最初一个child    let oldKeyToIdx, idxInOld, vnodeToMove, refElm    // removeOnly is a special flag used only by <transition-group>    // to ensure removed elements stay in correct relative positions    // during leaving transitions    const canMove = !removeOnly    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证实diff完了,循环完结    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {      // 如果oldVnode的第一个child不存在      if (isUndef(oldStartVnode)) {        // oldStart索引右移        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left      // 如果oldVnode的最初一个child不存在      } else if (isUndef(oldEndVnode)) {        // oldEnd索引左移        oldEndVnode = oldCh[--oldEndIdx]      // oldStartVnode和newStartVnode是同一个节点      } else if (sameVnode(oldStartVnode, newStartVnode)) {        // patch oldStartVnode和newStartVnode, 索引左移,持续循环        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)        oldStartVnode = oldCh[++oldStartIdx]        newStartVnode = newCh[++newStartIdx]      // oldEndVnode和newEndVnode是同一个节点      } else if (sameVnode(oldEndVnode, newEndVnode)) {        // patch oldEndVnode和newEndVnode,索引右移,持续循环        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)        oldEndVnode = oldCh[--oldEndIdx]        newEndVnode = newCh[--newEndIdx]      // oldStartVnode和newEndVnode是同一个节点      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right        // patch oldStartVnode和newEndVnode        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)        // 如果removeOnly是false,则将oldStartVnode.eml挪动到oldEndVnode.elm之后        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))        // oldStart索引右移,newEnd索引左移        oldStartVnode = oldCh[++oldStartIdx]        newEndVnode = newCh[--newEndIdx]      // 如果oldEndVnode和newStartVnode是同一个节点      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left        // patch oldEndVnode和newStartVnode        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)        // 如果removeOnly是false,则将oldEndVnode.elm挪动到oldStartVnode.elm之前        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)        // oldEnd索引左移,newStart索引右移        oldEndVnode = oldCh[--oldEndIdx]        newStartVnode = newCh[++newStartIdx]      // 如果都不匹配      } else {        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)        // 尝试在oldChildren中寻找和newStartVnode的具备雷同的key的Vnode        idxInOld = isDef(newStartVnode.key)          ? oldKeyToIdx[newStartVnode.key]          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)        // 如果未找到,阐明newStartVnode是一个新的节点        if (isUndef(idxInOld)) { // New element          // 创立一个新Vnode          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)        // 如果找到了和newStartVnodej具备雷同的key的Vnode,叫vnodeToMove        } else {          vnodeToMove = oldCh[idxInOld]          /* istanbul ignore if */          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {            warn(              'It seems there are duplicate keys that is causing an update error. ' +              'Make sure each v-for item has a unique key.'            )          }          // 比拟两个具备雷同的key的新节点是否是同一个节点          //不设key,newCh和oldCh只会进行头尾两端的互相比拟,设key后,除了头尾两端的比拟外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key能够更高效的利用dom。          if (sameVnode(vnodeToMove, newStartVnode)) {            // patch vnodeToMove和newStartVnode            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)            // 革除            oldCh[idxInOld] = undefined            // 如果removeOnly是false,则将找到的和newStartVnodej具备雷同的key的Vnode,叫vnodeToMove.elm            // 挪动到oldStartVnode.elm之前            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)          // 如果key雷同,然而节点不雷同,则创立一个新的节点          } else {            // same key but different element. treat as new element            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)          }        }        // 右移        newStartVnode = newCh[++newStartIdx]      }    }

while循环次要解决了以下五种情景:

  • 当新老 VNode 节点的 start 雷同时,间接 patchVnode ,同时新老 VNode 节点的开始索引都加 1
  • 当新老 VNode 节点的 end雷同时,同样间接 patchVnode ,同时新老 VNode 节点的完结索引都减 1
  • 当老 VNode 节点的 start 和新 VNode 节点的 end 雷同时,这时候在 patchVnode 后,还须要将以后实在 dom 节点挪动到 oldEndVnode 的前面,同时老 VNode 节点开始索引加 1,新 VNode 节点的完结索引减 1
  • 当老 VNode 节点的 end 和新 VNode 节点的 start 雷同时,这时候在 patchVnode 后,还须要将以后实在 dom 节点挪动到 oldStartVnode 的后面,同时老 VNode 节点完结索引减 1,新 VNode 节点的开始索引加 1
  • 如果都不满足以上四种情景,那阐明没有雷同的节点能够复用,则会分为以下两种状况:

    • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 统一 key 的旧的 VNode 节点,再进行patchVnode,同时将这个实在 dom挪动到 oldStartVnode 对应的实在 dom 的后面
    • 调用 createElm 创立一个新的 dom 节点放到以后 newStartIdx 的地位

小结

  • 当数据产生扭转时,订阅者watcher就会调用patch给实在的DOM打补丁
  • 通过isSameVnode进行判断,雷同则调用patchVnode办法
  • patchVnode做了以下操作:

    • 找到对应的实在dom,称为el
    • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
    • 如果oldVnode有子节点而VNode没有,则删除el子节点
    • 如果oldVnode没有子节点而VNode有,则将VNode的子节点实在化后增加到el
    • 如果两者都有子节点,则执行updateChildren函数比拟子节点
  • updateChildren次要做了以下操作:

    • 设置新旧VNode的头尾指针
    • 新旧头尾指针进行比拟,循环向两头聚拢,依据状况调用patchVnode进行patch反复流程、调用createElem创立一个新节点,从哈希表寻找 key统一的VNode 节点再分状况操作

vuex是什么?怎么应用?哪种性能场景应用它?

Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源寄存地,对应于个别 vue 对象外面的 data 外面寄存的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据产生扭转,依赖这相数据的组件也会产生更新它通过 mapState 把全局的 stategetters 映射到以后组件的 computed 计算属性
  • vuex 个别用于中大型 web 单页利用中对利用的状态进行治理,对于一些组件间关系较为简单的小型利用,应用 vuex 的必要性不是很大,因为齐全能够用组件 prop 属性或者事件来实现父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 应用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据齐全独立于组件,因而将组件间共享的数据置于 State 中能无效解决多层级组件嵌套的跨组件通信问题
vuexState 在单页利用的开发中自身具备一个“数据库”的作用,能够将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,个别什么样的数据会放在 State 中呢? 目前次要有两种数据会应用 vuex 进行治理:
  • 组件之间全局共享的数据
  • 通过后端异步申请的数据

包含以下几个模块

  • stateVuex 应用繁多状态树,即每个利用将仅仅蕴含一个store 实例。外面寄存的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据产生扭转,依赖这相数据的组件也会产生更新。它通过 mapState 把全局的 stategetters 映射到以后组件的 computed 计算属性
  • mutations:更改Vuexstore中的状态的惟一办法是提交mutation
  • gettersgetter 能够对 state 进行计算操作,它就是 store 的计算属性尽管在组件内也能够做计算属性,然而 getters 能够在多给件之间复用如果一个状态只在一个组件内应用,是能够不必 getters
  • actionaction 相似于 muation, 不同在于:action 提交的是 mutation,而不是间接变更状态action 能够蕴含任意异步操作
  • modules:面对简单的应用程序,当治理的状态比拟多时;咱们须要将vuexstore对象宰割成模块(modules)

modules:我的项目特地简单的时候,能够让每一个模块领有本人的statemutationactiongetters,使得构造十分清晰,方便管理

答复范例

思路

  • 给定义
  • 必要性论述
  • 何时应用
  • 拓展:一些集体思考、实践经验等

答复范例

  1. Vuex 是一个专为 Vue.js 利用开发的 状态管理模式 + 库 。它采纳集中式存储,治理利用的所有组件的状态,并以相应的规定保障状态以一种可预测的形式发生变化。
  2. 咱们期待以一种简略的“单向数据流”的形式治理利用,即状态 -> 视图 -> 操作单向循环的形式。但当咱们的利用遇到多个组件共享状态时,比方:多个视图依赖于同一状态或者来自不同视图的行为须要变更同一状态。此时单向数据流的简洁性很容易被毁坏。因而,咱们有必要把组件的共享状态抽取进去,以一个全局单例模式治理。通过定义和隔离状态治理中的各种概念并通过强制规定维持视图和状态间的独立性,咱们的代码将会变得更结构化且易保护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念
  3. Vuex 解决状态治理的同时引入了不少概念:例如statemutationaction等,是否须要引入还须要依据利用的理论状况掂量一下:如果不打算开发大型单页利用,应用 Vuex 反而是繁琐冗余的,一个简略的 store 模式就足够了。然而,如果要构建一个中大型单页利用,Vuex 根本是标配。
  4. 我在应用vuex过程中感触到一些等

可能的诘问

  1. vuex有什么毛病吗?你在开发过程中有遇到什么问题吗?
  2. 刷新浏览器,vuex中的state会从新变为初始状态。解决方案-插件 vuex-persistedstate
  3. actionmutation的区别是什么?为什么要辨别它们?
  • action中解决异步,mutation不能够
  • mutation做原子操作
  • action能够整合多个mutation的汇合
  • mutation 是同步更新数据(外部会进行是否为异步形式更新数据的检测) $watch 严格模式下会报错
  • action 异步操作,能够获取数据后调佣 mutation 提交最终数据
  • 流程程序:“相应视图—>批改State”拆分成两局部,视图触发ActionAction再触发Mutation`。
  • 基于流程程序,二者表演不同的角色:Mutation:专一于批改State,实践上是批改State的惟一路径。Action:业务代码、异步申请
  • 角色不同,二者有不同的限度:Mutation:必须同步执行。Action:能够异步,但不能间接操作State

双向数据绑定的原理

Vue.js 是采纳数据劫持联合发布者-订阅者模式的形式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时公布音讯给订阅者,触发相应的监听回调。次要分为以下几个步骤:

  1. 须要observe的数据对象进行递归遍历,包含子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变动
  2. compile解析模板指令,将模板中的变量替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,更新视图
  3. Watcher订阅者是Observer和Compile之间通信的桥梁,次要做的事件是: ①在本身实例化时往属性订阅器(dep)外面增加本人 ②本身必须有一个update()办法 ③待属性变动dep.notice()告诉时,能调用本身的update()办法,并触发Compile中绑定的回调,则功成身退。
  4. MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听本人的model数据变动,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变动 -> 视图更新;视图交互变动(input) -> 数据model变更的双向绑定成果。

写过自定义指令吗 原理是什么

指令实质上是装璜器,是 vue 对 HTML 元素的扩大,给 HTML 元素减少自定义性能。vue 编译 DOM 时,会找到指令对象,执行指令的相干办法。

自定义指令有五个生命周期(也叫钩子函数),别离是 bind、inserted、update、componentUpdated、unbind

1. bind:只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。2. inserted:被绑定元素插入父节点时调用 (仅保障父节点存在,但不肯定已被插入文档中)。3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变动。通过比拟更新前后的绑定值,能够疏忽不必要的模板更新。4. componentUpdated:被绑定元素所在模板实现一次更新周期时调用。5. unbind:只调用一次,指令与元素解绑时调用。

原理

1.在生成 ast 语法树时,遇到指令会给以后元素增加 directives 属性

2.通过 genDirectives 生成指令代码

3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子

4.当执行指令对应钩子函数时,调用对应指令定义的办法

用过pinia吗?有什么长处?

1. pinia是什么?

  • Vue3中,能够应用传统的Vuex来实现状态治理,也能够应用最新的pinia来实现状态治理,咱们来看看官网如何解释pinia的:PiniaVue 的存储库,它容许您跨组件/页面共享状态。
  • 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家能够间接将pinia比作为Vue3Vuex

2. 为什么要应用pinia?

  • Vue2Vue3都反对,这让咱们同时应用Vue2Vue3的小伙伴都能很快上手。
  • pinia中只有stategetteraction,摈弃了Vuex中的MutationVuexmutation始终都不太受小伙伴们的待见,pinia间接摈弃它了,这无疑缩小了咱们工作量。
  • piniaaction反对同步和异步,Vuex不反对
  • 良好的Typescript反对,毕竟咱们Vue3都举荐应用TS来编写,这个时候应用pinia就十分适合了
  • 无需再创立各个模块嵌套了,Vuex中如果数据过多,咱们通常分模块来进行治理,稍显麻烦,而pinia中每个store都是独立的,相互不影响。
  • 体积十分小,只有1KB左右。
  • pinia反对插件来扩大本身性能。
  • 反对服务端渲染

3. pinna应用

pinna文档(opens new window)

  1. 筹备工作

咱们这里搭建一个最新的Vue3 + TS + Vite我的项目

npm create [email protected] my-vite-app --template vue-ts
  1. pinia根底应用
yarn add pinia
// main.tsimport { createApp } from "vue";import App from "./App.vue";import { createPinia } from "pinia";const pinia = createPinia();const app = createApp(App);app.use(pinia);app.mount("#app");

2.1 创立store

//sbinsrc/store/user.tsimport { defineStore } from 'pinia'// 第一个参数是应用程序中 store 的惟一 idexport const useUsersStore = defineStore('users', {  // 其它配置项})

创立store很简略,调用pinia中的defineStore函数即可,该函数接管两个参数:

  • name:一个字符串,必传项,该store的惟一id
  • options:一个对象,store的配置项,比方配置store内的数据,批改数据的办法等等。

咱们能够定义任意数量的store,因为咱们其实一个store就是一个函数,这也是pinia的益处之一,让咱们的代码扁平化了,这和Vue3的实现思维是一样的

2.2 应用store

<!-- src/App.vue --><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();console.log(store);</script>

2.3 增加state

export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },});

2.4 读取state数据

<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p></template><script setup lang="ts">import { ref } from "vue";import { useUsersStore } from "../src/store/user";const store = useUsersStore();const name = ref<string>(store.name);const age = ref<number>(store.age);const sex = ref<string>(store.sex);</script>

上段代码中咱们间接通过store.age等形式获取到了store存储的值,然而大家有没有发现,这样比拟繁琐,咱们其实能够用解构的形式来获取值,使得代码更简洁一点

import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的

2.5 批改state数据

<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p>  <button @click="changeName">更改姓名</button></template><script setup lang="ts">import child from './child.vue';import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store);const changeName = () => {  store.name = "张三";  console.log(store);};</script>

2.6 重置state

  • 有时候咱们批改了state数据,想要将它还原,这个时候该怎么做呢?就比方用户填写了一部分表单,忽然想重置为最初始的状态。
  • 此时,咱们间接调用store$reset()办法即可,持续应用咱们的例子,增加一个重置按钮
<button @click="reset">重置store</button>// 重置storeconst reset = () => {  store.$reset();};

当咱们点击重置按钮时,store中的数据会变为初始状态,页面也会更新

2.7 批量更改state数据

如果咱们一次性须要批改很多条数据的话,有更加简便的办法,应用store$patch办法,批改app.vue代码,增加一个批量更改数据的办法

<button @click="patchStore">批量批改数据</button>// 批量批改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};
  • 有教训的小伙伴可能发现了,咱们采纳这种批量更改的形式仿佛代价有一点大,如果咱们state中有些字段无需更改,然而依照上段代码的写法,咱们必须要将state中的所有字段例举出了。
  • 为了解决该问题,pinia提供的$patch办法还能够接管一个回调函数,它的用法有点像咱们的数组循环回调函数了。
store.$patch((state) => {  state.items.push({ name: 'shoes', quantity: 1 })  state.hasChanged = true})

2.8 间接替换整个state

pinia提供了办法让咱们间接替换整个state对象,应用store$state办法

store.$state = { counter: 666, name: '张三' }

上段代码会将咱们提前申明的state替换为新的对象,可能这种场景用得比拟少

  1. getters属性
  2. gettersdefineStore参数配置项外面的另一个属性
  3. 能够把getter设想成Vue中的计算属性,它的作用就是返回一个新的后果,既然它和Vue中的计算属性相似,那么它必定也是会被缓存的,就和computed一样

3.1 增加getter

export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 10,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },  },})

上段代码中咱们在配置项参数中增加了getter属性,该属性对象中定义了一个getAddAge办法,该办法会默认接管一个state参数,也就是state对象,而后该办法返回的是一个新的数据

3.2 应用getter

<template>  <p>新年龄:{{ store.getAddAge }}</p>  <button @click="patchStore">批量批改数据</button></template><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();// 批量批改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};</script>

上段代码中咱们间接在标签上应用了store.gettAddAge办法,这样能够保障响应式,其实咱们state中的name等属性也能够以此种形式间接在标签上应用,也能够放弃响应式

3.3 getter中调用其它getter

export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});

3.3 getter传参

export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
<p>新年龄:{{ store.getAddAge(1100) }}</p>
  1. actions属性
  2. 后面咱们提到的stategetters属性都次要是数据层面的,并没有具体的业务逻辑代码,它们两个就和咱们组件代码中的data数据和computed计算属性一样。
  3. 那么,如果咱们有业务代码的话,最好就是卸载actions属性外面,该属性就和咱们组件代码中的methods类似,用来搁置一些解决业务逻辑的办法。
  4. actions属性值同样是一个对象,该对象外面也是存储的各种各样的办法,包含同步办法和异步办法

4.1 增加actions

export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },  actions: {    // 在理论场景中,该办法能够是任何逻辑,比方发送申请、存储token等等。大家把actions办法当作一个一般的办法即可,非凡之处在于该办法外部的this指向的是以后store    saveName(name: string) {      this.name = name;    },  },});

4.2 应用actions

应用actions中的办法也非常简单,比方咱们在App.vue中想要调用该办法

const saveName = () => {  store.saveName("poetries");};

总结

pinia的知识点很少,如果你有Vuex根底,那么学起来更是大海捞针

pinia无非就是以下3个大点:

  • state
  • getters
  • actions

Vue组件如何通信?

Vue组件通信的办法如下:

  • props/$emit+v-on: 通过props将数据自上而下传递,而通过$emit和v-on来向上传递信息。
  • EventBus: 通过EventBus进行信息的公布与订阅
  • vuex: 是全局数据管理库,能够通过vuex治理全局的数据流
  • $attrs/$listeners: Vue2.4中退出的$attrs/$listeners能够进行跨级的组件通信
  • provide/inject:以容许一个先人组件向其所有子孙后代注入一个依赖,不管组件档次有多深,并在起上下游关系成立的工夫里始终失效,这成为了跨组件通信的根底

还有一些用solt插槽或者ref实例进行通信的,应用场景过于无限就不赘述了。

v-model 的原理?

咱们在 vue 我的项目中次要应用 v-model 指令在表单 input、textarea、select 等元素上创立双向数据绑定,咱们晓得 v-model 实质上不过是语法糖,v-model 在外部为不同的输出元素应用不同的属性并抛出不同的事件:

  • text 和 textarea 元素应用 value 属性和 input 事件;
  • checkbox 和 radio 应用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

以 input 表单元素为例:

<input v-model='something'>相当于<input v-bind:value="something" v-on:input="something = $event.target.value">

如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

父组件:<ModelChild v-model="message"></ModelChild>子组件:<div>{{value}}</div>props:{    value: String},methods: {  test1(){     this.$emit('input', '小红')  },},