乐趣区

关于vue.js:前端常见vue面试题合集

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) // 0
​
count.value++
console.log(count.value) // 1

const obj = reactive({count: 0})
obj.count++
  • ref接管外部值(inner value)返回响应式 Ref 对象,reactive返回响应式代理对象
  • 从定义上看 ref 通常用于解决单值的响应式,reactive用于解决对象类型的数据响应式
  • 两者均是用于结构响应式数据,然而 ref 次要解决原始值的响应式问题
  • ref返回的响应式数据在 JS 中应用须要加上 .value 能力拜访其值,在视图中应用会主动脱 ref,不须要.valueref 能够接管对象或数组等非原始值,但外部仍然是 reactive 实现响应式;reactive外部如果接管 Re f 对象会主动脱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);     // 定义 _init
stateMixin(Vue);    // 定义 $set $get $delete $watch 等
eventsMixin(Vue);   // 定义事件  $on  $once $off $emit
lifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroy
renderMixin(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 method
Vue.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 时变成了全局的,能同时触发多个子模块中同名 mutation
store.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-L115
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
    })
}

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.ts
import {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.ts
import {defineStore} from 'pinia'

// 第一个参数是应用程序中 store 的惟一 id
export const useUsersStore = defineStore('users', {// 其它配置项})

创立 store 很简略,调用 p inia中的 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>
// 重置 store
const 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. 后面咱们提到的 stategetter s 属性都次要是数据层面的,并没有具体的业务逻辑代码,它们两个就和咱们组件代码中的 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', '小红')
  },
},
退出移动版