关于vue.js:腾讯前端一面常考vue面试题汇总

8次阅读

共计 37793 个字符,预计需要花费 95 分钟才能阅读完成。

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 中如何扩大一个组件

此题属于实际题,考查大家对 vue 罕用 api 应用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同

答题思路:

  • 依照逻辑扩大和内容扩大来列举

    • 逻辑扩大有:mixinsextendscomposition api
    • 内容扩大有slots
  • 别离说出他们应用办法、场景差别和问题。
  • 作为扩大,还能够说说 vue3 中新引入的 composition api 带来的变动

答复范例:

  1. 常见的组件扩大办法有:mixinsslotsextends
  2. 混入 mixins 是散发 Vue 组件中可复用性能的非常灵活的形式。混入对象能够蕴含任意组件选项。当组件应用混入对象时,所有混入对象的选项将被混入该组件自身的选项
// 复用代码:它是一个配置对象,选项和组件外面一样
const mymixin = {
   methods: {dosomething(){}}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)

// 部分混入:做数组项设置到 mixins 选项,仅作用于以后组件
const Comp = {mixins: [mymixin]
}
  1. 插槽次要用于 vue 组件中的内容散发,也能够用于组件扩大

子组件 Child

<div>
  <slot> 这个内容会被父组件传递的内容替换 </slot>
</div>

父组件 Parent

<div>
   <Child> 来自父组件内容 </Child>
</div>

如果要准确散发到不同地位能够应用 具名插槽,如果要应用子组件中的数据能够应用作用域插槽

  1. 组件选项中还有一个不太罕用的选项extends,也能够起到扩大组件的目标
// 扩大对象
const myextends = {
   methods: {dosomething(){}}
}
// 组件扩大:做数组项设置到 extends 选项,仅作用于以后组件
// 跟混入的不同是它只能扩大单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {extends: myextends}
  1. 混入的数据和办法不能明确判断起源且可能和以后组件内变量产生命名抵触,vue3中引入的 composition api,能够很好解决这些问题,利用独立进去的响应式模块能够很不便的编写独立逻辑并提供响应式的数据,而后在setup 选项中组合应用,加强代码的可读性和维护性。例如
// 复用逻辑 1
function useXX() {}
// 复用逻辑 2
function useYY() {}
// 逻辑组合
const Comp = {setup() {const {xx} = useXX()
      const {yy} = useYY()
      return {xx, yy}
   }
}

Vue-router 跳转和 location.href 有什么区别

  • 应用 location.href= /url 来跳转,简略不便,然而刷新了页面;
  • 应用 history.pushState(/url),无刷新页面,动态跳转;
  • 引进 router,而后应用 router.push(/url) 来跳转,应用了 diff 算法,实现了按需加载,缩小了 dom 的耗费。其实应用 router 跳转和应用 history.pushState() 没什么差异的,因为 vue-router 就是用了 history.pushState(),尤其是在 history 模式下。

watch 原理

watch 实质上是为每个监听属性 setter 创立了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创立一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象 setter,因而引入 deep 可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。
  • immediate:在初始化时间接调用回调函数,能够通过在 created 阶段手动调用回调函数实现雷同的成果

Vue computed 实现

  • 建设与其余属性(如:dataStore)的分割;
  • 属性扭转后,告诉计算属性从新计算

实现时,次要如下

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

说说 vue 内置指令

参考 前端进阶面试题具体解答

Vue 中常见性能优化

编码优化

  1. 应用 v-show 复用DOM:防止反复创立组件
<template>
  <div class="cell">
    <!-- 这种状况用 v -show 复用 DOM,比 v -if 成果好 -->
    <div v-show="value" class="on">
      <Heavy :n="10000"/>
    </div>
    <section v-show="!value" class="off">
      <Heavy :n="10000"/>
    </section>
  </div>
</template>
  1. 正当应用路由懒加载、异步组件,无效拆分 App 尺寸,拜访时才异步加载
const router = createRouter({
  routes: [// 借助 webpack 的 import()实现异步组件
    {path: '/foo', component: () => import('./Foo.vue') }
  ]
})
  1. keep-alive缓存页面:防止反复创立组件实例,且能保留缓存组件状态
<router-view v-slot="{Component}">
    <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. v-oncev-memo:不再变动的数据应用v-once
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

按条件跳过更新时应用v-momo:上面这个列表只会更新选中状态变动项

<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{item.id}} - selected: {{item.id === selected}}</p>
  <p>...more child nodes</p>
</div>
  1. 长列表性能优化:如果是大数据长列表,可采纳虚构滚动,只渲染少部分区域的内容
<recycle-scroller
  class="items"
  :items="items"
  :item-size="24"
>
  <template v-slot="{item}">
    <FetchItemView
      :item="item"
      @vote="voteItem(item)"
    />
  </template>
</recycle-scroller>
  1. 避免外部透露,组件销毁后把全局变量和事件销毁:Vue 组件销毁时,会主动解绑它的全副指令及事件监听器,然而仅限于组件自身的事件
export default {created() {this.timer = setInterval(this.refresh, 2000)
  },
  beforeUnmount() {clearInterval(this.timer)
  }
}
  1. 图片懒加载

对于图片过多的页面,为了减速页面加载速度,所以很多时候咱们须要将页面内未呈现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载

<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png">
  1. 滚动到可视区域动静加载

https://tangbc.github.io/vue-virtual-scroll-list(opens new window)

  1. 第三方插件按需引入:(babel-plugin-component

element-plus 这样的第三方组件库能够按需引入防止体积太大

import {createApp} from 'vue';
import {Button, Select} from 'element-plus';
​
const app = createApp()
app.use(Button)
app.use(Select)
  1. 服务端渲染:SSR

如果 SPA 利用有首屏渲染慢的问题,能够思考SSR

以及上面的其余办法

  • 不要将所有的数据都放在 data 中,data中的数据都会减少 gettersetter,会收集对应的watcher
  • v-for 遍历为 item 增加 key
  • v-for 遍历防止同时应用 v-if
  • 辨别 computedwatch 的应用
  • 拆分组件(进步复用性、减少代码的可维护性, 缩小不必要的渲染)
  • 防抖、节流

用户体验

  • app-skeleton 骨架屏
  • pwa serviceworker

SEO 优化

  • 预渲染插件 prerender-spa-plugin
  • 服务端渲染 ssr

打包优化

  • Webpack 对图片进行压缩
  • 应用 cdn 的形式加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • 优化 SourceMap
  • 构建后果输入剖析,利用 webpack-bundle-analyzer 可视化剖析工具

根底的 Web 技术的优化

  • 服务端 gzip 压缩
  • 浏览器缓存
  • CDN 的应用
  • 应用 Chrome Performance 查找性能瓶颈

构建的 vue-cli 工程都到了哪些技术,它们的作用别离是什么

  • vue.jsvue-cli工程的外围,次要特点是 双向数据绑定 和 组件零碎。
  • vue-routervue官网举荐应用的路由框架。
  • vuex:专为 Vue.js 利用我的项目开发的状态管理器,次要用于保护 vue 组件间共用的一些 变量 和 办法。
  • axios(或者 fetchajax):用于发动 GET、或 POSThttp申请,基于 Promise 设计。
  • vuex等:一个专为 vue 设计的挪动端 UI 组件库。
  • 创立一个 emit.js 文件,用于 vue 事件机制的治理。
  • webpack:模块加载和 vue-cli 工程打包器。

vue 初始化页面闪动问题

应用 vue 开发时,在 vue 初始化之前,因为 div 是不归 vue 管的,所以咱们写的代码在还没有解析的状况下会容易呈现花屏景象,看到相似于 {{message}} 的字样,尽管个别状况下这个工夫很短暂,然而还是有必要让解决这个问题的。

首先:在 css 里加上以下代码:

[v-cloak] {display: none;}

如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display:'block'}"

Vue 组件之间通信形式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信 父子组件通信 隔代组件通信 兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信

组件传参的各种形式

组件通信罕用形式有以下几种

  • props / $emit 实用 父子组件通信

    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3 废除) 实用 父子组件通信

    • ref:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例
    • $parent / $children:拜访拜访父组件的属性或办法 / 拜访子组件的属性或办法
  • EventBus($emit / $on) 实用于 父子、隔代、兄弟组件通信

    • 这种办法通过一个空的 Vue 实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件
  • $attrs / $listeners(vue3 废除) 实用于 隔代组件通信

    • $attrs:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 (classstyle 除外 )。当一个组件没有申明任何 prop时,这里会蕴含所有父作用域的绑定 (classstyle 除外 ),并且能够通过 v-bind="$attrs" 传入外部组件。通常配合 inheritAttrs 选项一起应用
    • $listeners:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过 v-on="$listeners" 传入外部组件
  • provide / inject 实用于 隔代组件通信

    • 先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系
  • $root 实用于 隔代组件通信 拜访根组件中的属性或办法,是根组件,不是父组件。$root 只对根组件有用
  • Vuex 实用于 父子、隔代、兄弟组件通信

    • Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。每一个 Vuex 利用的外围就是 store(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
    • 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。

依据组件之间关系探讨组件通信最为清晰无效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

上面演示组件之间通信三种状况: 父传子、子传父、兄弟组件之间的通信

1. 父子组件通信

应用 props,父组件能够应用props 向子组件传递数据。

父组件 vue 模板father.vue:

<template>
  <child :msg="message"></child>
</template>

<script>
import child from './child.vue';
export default {
  components: {child},
  data () {
    return {message: 'father message';}
  }
}
</script>

子组件 vue 模板child.vue:

<template>
    <div>{{msg}}</div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}
</script>

回调函数(callBack)

父传子:将父组件里定义的 method 作为 props 传入子组件

// 父组件 Parent.vue:<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子组件 Child.vue:<button @click="changeMsgFn">
props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件办法,子组件通过 $emit 触发事件,回调给父组件

父组件 vue 模板father.vue:

<template>
    <child @msgFunc="func"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {child},
    methods: {func (msg) {console.log(msg);
        }
    }
}
</script>

子组件 vue 模板child.vue:

<template>
    <button @click="handleClick"> 点我 </button>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    },
    methods () {handleClick () {
          //........
          this.$emit('msgFunc');
        }
    }
}
</script>

2. provide / inject 跨级拜访先人组件的数据

父组件通过应用 provide(){return{}} 提供须要传递的数据

export default {data() {
    return {
      title: '我是父组件',
      name: 'poetry'
    }
  },
  methods: {say() {alert(1)
    }
  },
  // provide 属性 可能为前面的后辈组件 / 嵌套的组件提供所须要的变量和办法
  provide() {
    return {
      message: '我是先人组件提供的数据',
      name: this.name, // 传递属性
      say: this.say
    }
  }
}

子组件通过应用 inject:[“参数 1”,”参数 2”,…] 接管父组件传递的参数

<template>
  <p> 曾孙组件 </p>
  <p>{{message}}</p>
</template>
<script>
export default {
  // inject 注入 / 接管先人组件传递的所须要的数据即可 
  // 接管到的数据 变量 跟 data 外面的变量一样 能够间接绑定到页面 {{}}
  inject: ["message","say"],
  mounted() {this.say();
  },
};
</script>

3. $parent + $children 获取父组件实例和子组件实例的汇合

  • this.$parent 能够间接拜访该组件的父实例或组件
  • 父组件也能够通过 this.$children 拜访它所有的子组件;须要留神 $children 并不保障程序,也不是响应式的
<!-- parent.vue -->
<template>
<div>
  <child1></child1>   
  <child2></child2> 
  <button @click="clickChild">$children 形式获取子组件值 </button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){
    return {total: 108}
  },
  components: {
    child1,
    child2  
  },
  methods: {funa(e){console.log("index",e)
    },
    clickChild(){console.log(this.$children[0].msg);
      console.log(this.$children[1].msg);
    }
  }
}
</script>
<!-- child1.vue -->
<template>
  <div>
    <button @click="parentClick"> 点击拜访父组件 </button>
  </div>
</template>
<script>
export default {data(){
    return {msg:"child1"}
  },
  methods: {
    // 拜访父组件数据
    parentClick(){this.$parent.funa("xx")
      console.log(this.$parent.total);
    }
  }
}
</script>
<!-- child2.vue -->
<template>
  <div>
    child2
  </div>
</template>
<script>
export default {data(){
    return {msg: 'child2'}
  }
}
</script>

4. $attrs + $listeners 多级组件通信

$attrs 蕴含了从父组件传过来的所有 props 属性

// 父组件 Parent.vue:<Child :name="name" :age="age"/>

// 子组件 Child.vue:<GrandChild v-bind="$attrs" />

// 孙子组件 GrandChild
<p> 姓名:{{$attrs.name}}</p>
<p> 年龄:{{$attrs.age}}</p>

$listeners蕴含了父组件监听的所有事件

// 父组件 Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>

// 子组件 Child.vue:<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

// 父组件 Parent.vue:<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);
    this.$refs.childComp.changeAge()}

// 子组件 Child.vue:data(){
    return{age:20}
},
methods(){changeAge(){this.age=15}
}

6. 非父子, 兄弟组件之间通信

vue2中废除了 broadcast 播送和散发事件的办法。父子组件中能够用 props$emit()。如何实现非父子组件间的通信,能够通过实例一个 vue 实例 Bus 作为媒介,要互相通信的兄弟组件之中,都引入 Bus,而后通过别离调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js 能够是这样:

// Bus.js

// 创立一个地方工夫总线类  
class Bus {constructor() {this.callbacks = {};   // 寄存事件的名字  
  }  
  $on(name, fn) {this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {if (this.callbacks[name]) {this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  

// main.js  
Vue.prototype.$bus = new Bus() // 将 $bus 挂载到 vue 实例的原型上  
// 另一种形式  
Vue.prototype.$bus = new Vue() // Vue 曾经实现了 Bus 的性能  
<template>
    <button @click="toBus"> 子组件传给兄弟组件 </button>
</template>

<script>
export default{
    methods: {toBus () {this.$bus.$emit('foo', '来自兄弟组件')
    }
  }
}
</script>

另一个组件也在钩子函数中监听 on 事件

export default {data() {
    return {message: ''}
  },
  mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})
  }
}

7. $root 拜访根组件中的属性或办法

  • 作用:拜访根组件中的属性或办法
  • 留神:是根组件,不是父组件。$root只对根组件有用
var vm = new Vue({
  el: "#app",
  data() {
    return {rootInfo:"我是根元素的属性"}
  },
  methods: {alerts() {alert(111)
    }
  },
  components: {
    com1: {data() {
        return {info: "组件 1"}
      },
      template: "<p>{{info}} <com2></com2></p>",
      components: {
        com2: {
          template: "<p> 我是组件 1 的子组件 </p>",
          created() {this.$root.alerts()// 根组件办法
            console.log(this.$root.rootInfo)// 我是根元素的属性
          }
        }
      }
    }
  }
});

8. vuex

  • 实用场景: 简单关系的组件数据传递
  • Vuex 作用相当于一个用来存储共享变量的容器
  • state用来寄存共享变量的中央
  • getter,能够减少一个 getter 派生状态,(相当于 store 中的计算属性),用来取得共享变量的值
  • mutations用来寄存批改 state 的办法。
  • actions也是用来寄存批改 state 的办法,不过 action 是在 mutations 的根底上进行。罕用来做一些异步操作

小结

  • 父子关系的组件数据传递抉择 props$emit进行传递,也可抉择ref
  • 兄弟关系的组件数据传递可抉择 $bus,其次能够抉择$parent 进行传递
  • 先人与后辈组件数据传递可抉择 attrslisteners或者 ProvideInject
  • 简单关系的组件数据传递能够通过 vuex 寄存共享的变量

个别在哪个生命周期申请异步数据

咱们能够在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 曾经创立,能够将服务端端返回的数据进行赋值。

举荐在 created 钩子函数中调用异步申请,因为在 created 钩子函数中调用异步申请有以下长处:

  • 能更快获取到服务端数据,缩小页面加载工夫,用户体验更好;
  • SSR 不反对 beforeMount、mounted 钩子函数,放在 created 中有助于一致性。

既然 Vue 通过数据劫持能够精准探测数据变动,为什么还须要虚构 DOM 进行 diff 检测差别

  • 响应式数据变动,Vue的确能够在数据变动时,响应式零碎能够立即得悉。然而如果给每个属性都增加 watcher 用于更新的话,会产生大量的 watcher 从而升高性能
  • 而且粒度过细也得导致更新不精确的问题,所以 vue 采纳了组件级的 watcher 配合 diff 来检测差别

v-model 实现原理

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

  • texttextarea 元素应用 value 属性和 input 事件
  • checkboxradio 应用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

所以咱们能够 v -model 进行如下改写:

<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />

当在 input 元素中应用 v-model 实现双数据绑定,其实就是在输出的时候触发元素的 input 事件,通过这个语法糖,实现了数据的双向绑定

  • 这个语法糖必须是固定的,也就是说属性必须为value,办法名必须为:input
  • 晓得了 v-model 的原理,咱们能够在自定义组件上实现v-model
//Parent
<template>
  {{num}}
  <Child v-model="num">
</template>
export default {data(){
    return {num: 0}
  }
}

//Child
<template>
  <div @click="add">Add</div>
</template>
export default {props: ['value'], // 属性必须为 value
  methods:{add(){
      // 办法名为 input
      this.$emit('input', this.value + 1)
    }
  }
}

原理

会将组件的 v-model 默认转化成value+input

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); 

// 察看输入的渲染函数:// with(this) { 
//     return _c('el-checkbox', { 
//         model: {//             value: (check), 
//             callback: function ($$v) {check = $$v}, 
//             expression: "check" 
//         } 
//     }) 
// }
// 源码地位 core/vdom/create-component.js line:155

function transformModel (options, data: any) {const prop = (options.model && options.model.prop) || 'value' 
    const event = (options.model && options.model.event) || 'input' 
    ;(data.attrs || (data.attrs = {}))[prop] = data.model.value 
    const on = data.on || (data.on = {}) 
    const existing = on[event] 
    const callback = data.model.callback 
    if (isDef(existing)) {if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing) 
        } 
    } else {on[event] = callback 
    } 
}

原生的 v-model,会依据标签的不同生成不同的事件和属性

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');

// with(this) { 
//     return _c('input', {//         directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], 
//         domProps: {"value": (value) },
//         on: {"input": function ($event) {//             if ($event.target.composing) return;
//             value = $event.target.value
//         }
//         }
//     })
// }

编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js

if (el.component) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime 
    return false 
} else if (tag === 'select') {genSelect(el, value, modifiers) 
} else if (tag === 'input' && type === 'checkbox') {genCheckboxModel(el, value, modifiers) 
} else if (tag === 'input' && type === 'radio') {genRadioModel(el, value, modifiers) 
} else if (tag === 'input' || tag === 'textarea') {genDefaultModel(el, value, modifiers) 
} else if (!config.isReservedTag(tag)) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime 
    return false 
}

运行时:会对元素解决一些对于输入法的问题 platforms/web/runtime/directives/model.js

inserted (el, binding, vnode, oldVnode) {if (vnode.tag === 'select') { // #6903 
    if (oldVnode.elm && !oldVnode.elm._vOptions) {mergeVNodeHook(vnode, 'postpatch', () => {directive.componentUpdated(el, binding, vnode) 
        }) 
    } else {setSelected(el, binding, vnode.context) 
    }
    el._vOptions = [].map.call(el.options, getValue) 
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { 
        el._vModifiers = binding.modifiers 
        if (!binding.modifiers.lazy) {el.addEventListener('compositionstart', onCompositionStart) 
            el.addEventListener('compositionend', onCompositionEnd) 
            // Safari < 10.2 & UIWebView doesn't fire compositionend when 
            // switching focus before confirming composition choice 
            // this also fixes the issue where some browsers e.g. iOS Chrome
            // fires "change" instead of "input" on autocomplete. 
            el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ 
            if (isIE9) {el.vmodel = true}
        }
    }
}

Vue 的性能优化有哪些

(1)编码阶段

  • 尽量减少 data 中的数据,data 中的数据都会减少 getter 和 setter,会收集对应的 watcher
  • v-if 和 v -for 不能连用
  • 如果须要应用 v -for 给每项元素绑定事件时应用事件代理
  • SPA 页面采纳 keep-alive 缓存组件
  • 在更多的状况下,应用 v -if 代替 v -show
  • key 保障惟一
  • 应用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动静加载
  • 图片懒加载

(2)SEO 优化

  • 预渲染
  • 服务端渲染 SSR

(3)打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 应用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化

(4)用户体验

  • 骨架屏
  • PWA
  • 还能够应用缓存 (客户端缓存、服务端缓存) 优化、服务端开启 gzip 压缩等。

Vue 生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创立组件实例的过程中会调用对应的钩子办法
  • 外部会对钩子函数进行解决,将钩子函数保护成数组的模式

Vue 的生命周期钩子外围实现是利用公布订阅模式先把用户传入的的生命周期钩子订阅好(外部采纳数组的形式存储)而后在创立组件实例的过程中会一次执行对应的钩子办法(公布)

<script>
    // Vue.options 中会寄存所有全局属性

    // 会用本身的 + Vue.options 中的属性进行合并
    // Vue.mixin({//     beforeCreate() {//         console.log('before 0')
    //     },
    // })
    debugger;
    const vm = new Vue({
        el: '#app',
        beforeCreate: [function() {console.log('before 1')
            },
            function() {console.log('before 2')
            }
        ]
    });
    console.log(vm);
</script>

相干代码如下

export function callHook(vm, hook) {
  // 顺次执行生命周期对应的办法
  const handlers = vm.$options[hook];
  if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); // 生命周期外面的 this 指向以后实例
    }
  }
}

// 调用的时候
Vue.prototype._init = function (options) {
  const vm = this;
  vm.$options = mergeOptions(vm.constructor.options, options);
  callHook(vm, "beforeCreate"); // 初始化数据之前
  // 初始化状态
  initState(vm);
  callHook(vm, "created"); // 初始化数据之后
  if (vm.$options.el) {vm.$mount(vm.$options.el);
  }
};

// 销毁实例实现
Vue.prototype.$destory = function() {
     // 触发钩子
    callHook(vm, 'beforeDestory')
    // 本身及子节点
    remove() 
    // 删除依赖
    watcher.teardown() 
    // 删除监听
    vm.$off() 
    // 触发钩子
    callHook(vm, 'destoryed')
}

原理流程图

Vue2.x 响应式数据原理

整体思路是数据劫持 + 观察者模式

对象外部通过 defineReactive 办法,应用 Object.defineProperty 来劫持各个属性的 settergetter(只会劫持曾经存在的属性),数组则是通过 重写数组 7 个办法 来实现。当页面应用对应属性时,每个属性都领有本人的 dep 属性,寄存他所依赖的 watcher(依赖收集),当属性变动后会告诉本人对应的 watcher 去更新(派发更新)

Object.defineProperty 根本应用

function observer(value) { // proxy reflect
    if (typeof value === 'object' && typeof value !== null)
    for (let key in value) {defineReactive(value, key, value[key]);
    }
}

function defineReactive(obj, key, value) {observer(value);
    Object.defineProperty(obj, key, {get() { // 收集对应的 key 在哪个办法(组件)中被应用
            return value;
        },
        set(newValue) {if (newValue !== value) {observer(newValue);
                value = newValue; // 让 key 对应的办法(组件从新渲染)从新执行
            }
        }
    })
}
let obj1 = {school: { name: 'poetry', age: 20} };
observer(obj1);
console.log(obj1)

源码剖析

class Observer {
  // 观测值
  constructor(value) {this.walk(value);
  }
  walk(data) {
    // 对象上的所有属性顺次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty 数据劫持外围 兼容性在 ie9 以及以上
function defineReactive(data, key, value) {observe(value); // 递归要害
  // -- 如果 value 还是一个对象会持续走一遍 odefineReactive 层层遍历始终到 value 不是对象才进行
  //   思考?如果 Vue 数据嵌套层级过深 >> 性能会受影响
  Object.defineProperty(data, key, {get() {console.log("获取值");

      // 须要做依赖收集过程 这里代码没写进去
      return value;
    },
    set(newValue) {if (newValue === value) return;
      console.log("设置值");
      // 须要做派发更新过程 这里代码没写进去
      value = newValue;
    },
  });
}
export function observe(value) {
  // 如果传过来的是对象或者数组 进行属性劫持
  if (Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {return new Observer(value);
  }
}

说一说你对 vue 响应式了解答复范例

  • 所谓数据响应式就是 可能使数据变动能够被检测并对这种变动做出响应的机制
  • MVVM框架中要解决的一个外围问题是连贯数据层和视图层,通过 数据驱动 利用,数据变动,视图更新,要做到这点的就须要对数据做响应式解决,这样一旦数据发生变化就能够立刻做出更新解决
  • vue 为例阐明,通过数据响应式加上虚构 DOMpatch算法,开发人员只须要操作数据,关怀业务,齐全不必接触繁琐的 DOM 操作,从而大大晋升开发效率,升高开发难度
  • vue2中的数据响应式会依据数据类型来做不同解决,如果是 对象则采纳 Object.defineProperty() 的形式定义数据拦挡,当数据被拜访或发生变化时,咱们感知并作出响应;如果是数组则通过笼罩数组对象原型的 7 个变更办法 ,使这些办法能够额定的做更新告诉,从而作出响应。这种机制很好的解决了数据响应化的问题,但在理论应用中也存在一些毛病:比方初始化时的递归遍历会造成性能损失;新增或删除属性时须要用户应用Vue.set/delete 这样非凡的 api 能力失效;对于 es6 中新产生的 MapSet 这些数据结构不反对等问题
  • 为了解决这些问题,vue3从新编写了这一部分的实现:利用 ES6Proxy代理要响应化的数据,它有很多益处,编程体验是统一的,不须要应用非凡 api,初始化性能和内存耗费都失去了大幅改善;另外因为响应化的实现代码抽取为独立的reactivity 包,使得咱们能够更灵便的应用它,第三方的扩大开发起来更加灵便了

vue-router 动静路由是什么

咱们常常须要把某种模式匹配到的所有路由,全都映射到同个组件。例如,咱们有一个 User 组件,对于所有 ID 各不相同的用户,都要应用这个组件来渲染。那么,咱们能够在 vue-router 的路由门路中应用“动静门路参数”(dynamic segment) 来达到这个成果

const User = {template: "<div>User</div>",};

const router = new VueRouter({
  routes: [
    // 动静门路参数 以冒号结尾
    {path: "/user/:id", component: User},
  ],
});

问题: vue-router 组件复用导致路由参数生效怎么办?

解决办法:

  1. 通过 watch 监听路由参数再发申请
watch: { // 通过 watch 来监听路由变动
 "$route": function(){this.getData(this.$route.params.xxx);
 }
}
  1. :key 来阻止“复用”
<router-view :key="$route.fullPath" />

答复范例

  1. 很多时候,咱们须要将给定匹配模式的路由映射到同一个组件,这种状况就须要定义动静路由
  2. 例如,咱们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router中,咱们能够在门路中应用一个动静字段来实现,例如:{path: '/users/:id', component: User},其中 :id 就是门路参数
  3. 门路参数 用冒号 : 示意。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的模式裸露进去。
  4. 参数还能够有多个,例如 /users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其余有用的信息,如 $route.query$route.hash

组件通信

组件通信的形式如下:

(1)props / $emit

父组件通过 props 向子组件传递数据,子组件通过 $emit 和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间造成了一个单向上行绑定。子组件的数据会随着父组件不断更新。
  • props 能够显示定义一个或一个以上的数据,对于接管的数据,能够是各种数据类型,同样也能够传递一个函数。
  • props属性名规定:若在 props 中应用驼峰模式,模板中须要应用短横线的模式
// 父组件
<template>
  <div id="father">
    <son :msg="msgData" :fn="myFunction"></son>
  </div>
</template>

<script>
import son from "./son.vue";
export default {
  name: father,
  data() {msgData: "父组件数据";},
  methods: {myFunction() {console.log("vue");
    },
  },
  components: {son},
};
</script>
// 子组件
<template>
  <div id="son">
    <p>{{msg}}</p>
    <button @click="fn"> 按钮 </button>
  </div>
</template>
<script>
export default {name: "son", props: ["msg", "fn"] };
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过 v-on 监听并接管参数。
// 父组件
<template>
  <div class="section">
    <com-article
      :articles="articleList"
      @onEmitIndex="onEmitIndex"
    ></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from "./test/article.vue";
export default {
  name: "comArticle",
  components: {comArticle},
  data() {return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
  },
  methods: {onEmitIndex(idx) {this.currentIndex = idx;},
  },
};
</script>
// 子组件
<template>
  <div>
    <div
      v-for="(item, index) in articles"
      :key="index"
      @click="emitIndex(index)"
    >
      {{item}}
    </div>
  </div>
</template>

<script>
export default {props: ["articles"],
  methods: {emitIndex(index) {this.$emit("onEmitIndex", index); // 触发父组件的办法,并传递参数 index
    },
  },
};
</script>

(2)eventBus 事件总线($emit / $on

eventBus事件总线实用于 父子组件 非父子组件 等之间的通信,应用步骤如下:(1)创立事件核心治理组件之间的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 假如有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default {components: { firstCom, secondCom} };
</script>

firstCom 组件中发送事件:

<template>
  <div>
    <button @click="add"> 加法 </button>
  </div>
</template>

<script>
import {EventBus} from "./event-bus.js"; // 引入事件核心

export default {data() {return { num: 0};
  },
  methods: {add() {EventBus.$emit("addition", { num: this.num++});
    },
  },
};
</script>

(3)接管事件 secondCom 组件中发送事件:

<template>
  <div> 求和: {{count}}</div>
</template>

<script>
import {EventBus} from "./event-bus.js";
export default {data() {return { count: 0};
  },
  mounted() {EventBus.$on("addition", (param) => {this.count = this.count + param.num;});
  },
};
</script>

在上述代码中,这就相当于将 num 值存贮在了事件总线中,在其余组件中能够间接拜访。事件总线就相当于一个桥梁,不必组件通过它来通信。

尽管看起来比较简单,然而这种办法也有不变之处,如果我的项目过大,应用这种形式进行通信,前期保护起来会很艰难。

(3)依赖注入(provide / inject)

这种形式就是 Vue 中的 依赖注入 ,该办法用于 父子组件之间的通信。当然这里所说的父子不肯定是真正的父子,也能够是祖孙组件,在层数很深的状况下,能够应用这种办法来进行传值。就不必一层一层的传递了。

provide / inject是 Vue 提供的两个钩子,和 datamethods 是同级的。并且 provide 的书写模式和 data 一样。

  • provide 钩子用来发送数据或办法
  • inject钩子用来接收数据或办法

在父组件中:

provide() { 
    return {num: this.num};
}

在子组件中:

inject: ['num']

还能够这样写,这样写就能够拜访父组件中的所有属性:

provide() {
 return {app: this};
}
data() {
 return {num: 1};
}

inject: ['app']
console.log(this.app.num)

留神: 依赖注入所提供的属性是 非响应式 的。

(3)ref / $refs

这种形式也是实现 父子组件 之间的通信。

ref:这个属性用在子组件上,它的援用就指向了子组件的实例。能够通过实例来拜访组件的数据和办法。

在子组件中:

export default {data () {
    return {name: 'JavaScript'}
  },
  methods: {sayHello () {console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
  mounted() {console.log(this.$refs.child.name); // JavaScript
    this.$refs.child.sayHello(); // hello},
};
</script>

(4)$parent / $children

  • 应用 $parent 能够让组件拜访父组件的实例(拜访的是上一级父组件的属性和办法)
  • 应用 $children 能够让组件拜访子组件的实例,然而,$children并不能保障程序,并且拜访的数据也不是响应式的。

在子组件中:

<template>
  <div>
    <span>{{message}}</span>
    <p> 获取父组件的值为: {{parentVal}}</p>
  </div>
</template>

<script>
export default {data() {return { message: "Vue"};
  },
  computed: {parentVal() {return this.$parent.msg;},
  },
};
</script>

在父组件中:

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change"> 点击扭转子组件值 </button>
  </div>
</template>

<script>
import child from "./child.vue";
export default {components: { child},
  data() {return { msg: "Welcome"};
  },
  methods: {change() {
      // 获取到子组件
      this.$children[0].message = "JavaScript";
    },
  },
};
</script>

在下面的代码中,子组件获取到了父组件的 parentVal 值,父组件扭转了子组件中 message 的值。须要留神:

  • 通过 $parent 拜访到的是上一级父组件的实例,能够应用 $root 来拜访根组件的实例
  • 在组件中应用 $children 拿到的是所有的子组件的实例,它是一个数组,并且是无序的
  • 在根组件 #app 上拿 $parent 失去的是 new Vue() 的实例,在这实例上再拿 $parent 失去的是 undefined,而在最底层的子组件拿$children 是个空数组
  • $children 的值是 数组 ,而$parent 是个 对象

(5)$attrs / $listeners

思考一种场景,如果 A 是 B 组件的父组件,B 是 C 组件的父组件。如果想要组件 A 给组件 C 传递数据,这种隔代的数据,该应用哪种形式呢?

如果是用 props/$emit 来一级一级的传递,的确能够实现,然而比较复杂;如果应用事件总线,在多人开发或者我的项目较大的时候,保护起来很麻烦;如果应用 Vuex,确实也能够,然而如果仅仅是传递数据,那可能就有点节约了。

针对上述情况,Vue 引入了$attrs / $listeners,实现组件之间的跨代通信。

先来看一下 inheritAttrs,它的默认值 true,继承所有的父组件属性除props 之外的所有属性;inheritAttrs:false 只继承 class 属性。

  • $attrs:继承所有的父组件属性(除了 prop 传递的属性、class 和 style),个别用在子组件的子元素上
  • $listeners:该属性是一个对象,外面蕴含了作用在这个组件上的所有监听器,能够配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A 组件(APP.vue):

<template>
  <div id="app">
    // 此处监听了两个事件,能够在 B 组件或者 C 组件中间接触发
    <child1
      :p-child1="child1"
      :p-child2="child2"
      @test1="onTest1"
      @test2="onTest2"
    ></child1>
  </div>
</template>
<script>
import Child1 from "./Child1.vue";
export default {components: { Child1},
  methods: {onTest1() {console.log("test1 running");
    },
    onTest2() {console.log("test2 running");
    },
  },
};
</script>

B 组件(Child1.vue):

<template>
  <div class="child-1">
    <p>props: {{pChild1}}</p>
    <p>$attrs: {{$attrs}}</p>
    <child2 v-bind="$attrs" v-on="$listeners"></child2>
  </div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {props: ["pChild1"],
  components: {Child2},
  inheritAttrs: false,
  mounted() {this.$emit("test1"); // 触发 APP.vue 中的 test1 办法
  },
};
</script>

C 组件 (Child2.vue):

<template>
  <div class="child-2">
    <p>props: {{pChild2}}</p>
    <p>$attrs: {{$attrs}}</p>
  </div>
</template>
<script>
export default {props: ["pChild2"],
  inheritAttrs: false,
  mounted() {this.$emit("test2"); // 触发 APP.vue 中的 test2 办法
  },
};
</script>

在上述代码中:

  • C 组件中能间接触发 test 的起因在于 B 组件调用 C 组件时 应用 v-on 绑定了$listeners 属性
  • 在 B 组件中通过 v -bind 绑定 $attrs 属性,C 组件能够间接获取到 A 组件中传递下来的 props(除了 B 组件中 props 申明的)

(6)总结

(1)父子组件间通信

  • 子组件通过 props 属性来承受父组件的数据,而后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来取得子组件,子组件通过 $parent 取得父组件,这样也能够实现通信。
  • 应用 provide/inject,在父组件中通过 provide 提供变量,在子组件中通过 inject 来将变量注入到组件中。不管子组件有多深,只有调用了 inject 那么就能够注入 provide 中的数据。

(2)兄弟组件间通信

  • 应用 eventBus 的办法,它的实质是通过创立一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现音讯的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也能够进行通信。

(3)任意组件之间

  • 应用 eventBus,其实就是创立一个事件核心,相当于中转站,能够用它来传递事件和接管事件。

如果业务逻辑简单,很多组件之间须要同时解决一些公共的数据,这个时候采纳下面这一些办法可能不利于我的项目的保护。这个时候能够应用 vuex,vuex 的思维就是将这一些公共的数据抽离进去,将它作为一个全局的变量来治理,而后其余组件就能够对这个公共数据进行读写操作,这样达到理解耦的目标。

Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 代替 Object.defineProperty。因为 Proxy 能够间接监听对象和数组的变动,并且有多达 13 种拦挡办法。

相干代码如下

import {mutableHandlers} from "./baseHandlers"; // 代理相干逻辑
import {isObject} from "./util"; // 工具办法

export function reactive(target) {
  // 依据不同参数创立不同响应式对象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {return function get(target, key, receiver) {
    // 对获取的值进行喷射
    const res = Reflect.get(target, key, receiver);
    console.log("属性获取", key);
    if (isObject(res)) {
      // 如果获取的值是对象类型,则返回以后对象的代理对象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {console.log("属性新增", key, value);
    } else if (hasChanged(value, oldValue)) {console.log("属性值被批改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 当获取属性时调用此办法
  set, // 当批改属性时调用此办法
};

Vue 的事件绑定原理

原生事件绑定是通过 addEventListener 绑定给实在元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上应用原生事件,须要加.native 修饰符,这样就相当于在父组件中把子组件当做一般 html 标签,而后加上原生事件。

$on$emit 是基于公布订阅模式的,保护一个事件核心,on 的时候将事件按名称存在事件中心里,称之为订阅者,而后 emit 将对应的事件进行公布,去执行事件中心里的对应的监听器

EventEmitter(公布订阅模式 – 简略版)

// 手写公布订阅模式 EventEmitter
class EventEmitter {constructor() {this.events = {};
  }
  // 实现订阅
  on(type, callBack) {if (!this.events) this.events = Object.create(null);

    if (!this.events[type]) {this.events[type] = [callBack];
    } else {this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {if (!this.events[type]) return;
    this.events[type] = this.events[type].filter(item => {return item !== callBack;});
  }
  // 只执行一次订阅事件
  once(type, callBack) {function fn() {callBack();
      this.off(type, fn);
    }
    this.on(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
  }
}


// 应用如下
const event = new EventEmitter();

const handle = (...rest) => {console.log(rest);
};

event.on("click", handle);

event.emit("click", 1, 2, 3, 4);

event.off("click", handle);

event.emit("click", 1, 2);

event.once("dbClick", () => {console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");

源码剖析

  1. 原生 dom 的绑定
  2. Vue 在创立真是 dom 时会调用 createElm , 默认会调用 invokeCreateHooks
  3. 会遍历以后平台下绝对的属性解决代码, 其中就有 updateDOMListeners 办法, 外部会传入 add 办法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {return}
    const on = vnode.data.on || {} 
    const oldOn = oldVnode.data.on || {} 
    target = vnode.elm normalizeEvents(on) 
    updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) 
    target = undefined 
}
function add (name: string, handler: Function, capture: boolean, passive: boolean) {
    target.addEventListener( // 给以后的 dom 增加事件 
        name, 
        handler, 
        supportsPassive ? {capture, passive} : capture 
    ) 
}

vue 中绑定事件是间接绑定给实在 dom 元素的

  1. 组件中绑定事件
export function updateComponentListeners (vm: Component, listeners: Object, oldListeners: ?Object) {target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
    target = undefined 
}
function add (event, fn) {target.$on(event, fn) 
}

组件绑定事件是通过 vue 中自定义的 $on 办法来实现的

正文完
 0