关于vue.js:前端二面vue面试题边面边更

8次阅读

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

Vuex 有哪几种属性?

有五种,别离是 State、Getter、Mutation、Action、Module

  • state => 根本数据(数据源寄存地)
  • getters => 从根本数据派生进去的数据
  • mutations => 提交更改数据的办法,同步
  • actions => 像一个装璜器,包裹 mutations,使之能够异步。
  • modules => 模块化 Vuex

谈谈对 keep-alive 的理解

keep-alive 能够实现组件的缓存,当组件切换时不会对以后组件进行卸载。罕用的 2 个属性 include/exclude,2 个生命周期 activated deactivated

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.js 外部运行机制会有一个大略的意识。

初始化及挂载

new Vue() 之后。Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、props、methods、data、computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式 」以及「 依赖收集」,前面会具体讲到,这里只有有一个印象即可。

初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 然而存在 template 的状况,须要进行「编译」步骤。

编译

compile 编译能够分成 parseoptimizegenerate 三个阶段,最终须要失去 render function。

1. parse

parse 会用正则等形式解析 template 模板中的指令、class、style 等数据,造成 AST。

2. optimize

optimize 的次要作用是标记 static 动态节点,这是 Vue 在编译过程中的一处优化,前面当 update 更新界面时,会有一个 patch 的过程,diff 算法会间接跳过动态节点,从而缩小了比拟的过程,优化了 patch 的性能。

3. generate

generate 是将 AST 转化成 render function字符串的过程,失去后果是 render 的字符串以及 staticRenderFns 字符串。

  • 在经验过 parseoptimizegenerate 这三个阶段当前,组件中就会存在渲染 VNode 所需的 render function 了。

响应式

接下来也就是 Vue.js 响应式外围局部。

这里的 gettersetter 曾经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。

  • render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集 」,「 依赖收集」的目标是将观察者 Watcher 对象寄存到以后闭包中的订阅者 Depsubs 中。造成如下所示的这样一个关系。

在批改对象的值的时候,会触发对应的 settersetter 告诉之前「依赖收集」失去的 Dep 中的每一个 Watcher,通知它们本人的值扭转了,须要从新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这两头还有一个 patch 的过程以及应用队列来异步更新的策略,这个咱们前面再讲。

Virtual DOM

咱们晓得,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为根底的树,用对象属性来形容节点,实际上它只是一层对实在 DOM 的形象。最终能够通过一系列操作使这棵树映射到实在环境上。因为 Virtual DOM 是以 JavaScript 对象为根底而不依赖实在平台环境,所以使它具备了跨平台的能力,比如说浏览器平台、Weex、Node 等。

比如说上面这样一个例子:

{
    tag: 'div',                 /* 阐明这是一个 div 标签 */
    children: [                 /* 寄存该标签的子节点 */
        {
            tag: 'a',           /* 阐明这是一个 a 标签 */
            text: 'click me'    /* 标签的内容 */
        }
    ]
}

渲染后能够失去

<div>
    <a>click me</a>
</div>

这只是一个简略的例子,实际上的节点有更多的属性来标记节点,比方 isStatic(代表是否为动态节点)、isComment(代表是否为正文节点)等。

更新视图

  • 后面咱们说到,在批改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来批改对应的视图,那么最终是如何更新视图的呢?
  • 当数据变动后,执行 render function 就能够失去一个新的 VNode 节点,咱们如果想要失去新的视图,最简略粗犷的办法就是间接解析这个新的 VNode 节点,而后用 innerHTML 间接全副渲染到实在 DOM 中。然而其实咱们只对其中的一小块内容进行了批改,这样做仿佛有些「节约」。
  • 那么咱们为什么不能只批改那些「扭转了的中央」呢?这个时候就要介绍咱们的「patch」了。咱们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比拟,通过 diff 算法得出它们的「差别 」。最初咱们只须要将这些「 差别」的对应 DOM 进行批改即可。

再看全局

回过头再来看看这张图,是不是大脑中曾经有一个大略的脉络了呢?

Vue 中如何进行依赖收集?

  • 每个属性都有本人的 dep 属性,寄存他所依赖的 watcher,当属性变动之后会告诉本人对应的watcher 去更新
  • 默认会在初始化时调用 render 函数,此时会触发属性依赖收集 dep.depend
  • 当属性产生批改时会触发 watcher 更新dep.notify()

依赖收集简版

let obj = {name: 'poetry', age: 20};

class Dep {constructor() {this.subs = [] // subs [watcher]
    }
    depend() {this.subs.push(Dep.target)
    }
    notify() {this.subs.forEach(watcher => watcher.update())
    }
}
Dep.target = null;
observer(obj); // 响应式属性劫持

// 依赖收集  所有属性都会减少一个 dep 属性,// 当渲染的时候取值了,这个 dep 属性 就会将渲染的 watcher 收集起来
// 数据更新 会让 watcher 从新执行

// 观察者模式

// 渲染组件时 会创立 watcher
class Watcher {constructor(render) {this.get();
    }
    get() {
      Dep.target = this;
      render(); // 执行 render
      Dep.target = null;
    }
    update() {this.get();
    }
}
const render = () => {console.log(obj.name); // obj.name => get 办法
}

// 组件是 watcher、计算属性是 watcher
new Watcher(render);

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) {
    // 创立一个 dep
    let dep = new Dep();

    // 递归察看子属性
    observer(value);

    Object.defineProperty(obj, key, {get() { // 收集对应的 key 在哪个办法(组件)中被应用
            if (Dep.target) { // watcher
                dep.depend(); // 这里会建设 dep 和 watcher 的关系}
            return value;
        },
        set(newValue) {if (newValue !== value) {observer(newValue);
                value = newValue; // 让 key 对应的办法(组件从新渲染)从新执行
                dep.notify()}
        }
    })
}

// 模仿数据获取,触发 getter
obj.name = 'poetries'

// 一个属性一个 dep,一个属性能够对应多个 watcher(一个属性能够在任何组件中应用、在多个组件中应用)// 一个 dep 对应多个 watcher 
// 一个 watcher 对应多个 dep(一个视图对应多个属性)// dep 和 watcher 是多对多的关系

Vue 性能优化

编码优化

  • 事件代理
  • keep-alive
  • 拆分组件
  • key 保障唯一性
  • 路由懒加载、异步组件
  • 防抖节流

Vue 加载性能优化

  • 第三方模块按需导入( babel-plugin-component
  • 图片懒加载

用户体验

  • app-skeleton 骨架屏
  • shellap p 壳
  • pwa

SEO 优化

  • 预渲染

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

Vue 中给 data 中的对象属性增加一个新的属性时会产生什么?如何解决?

<template> 
   <div>
      <ul>
         <li v-for="value in obj" :key="value"> {{value}} </li>       </ul>       <button @click="addObjB"> 增加 obj.b</button>    </div>
</template>

<script>
    export default {data () {return {               obj: {                   a: 'obj.a'}           }        },       methods: {addObjB () {this.obj.b = 'obj.b'               console.log(this.obj)           }       }   }
</script>

点击 button 会发现,obj.b 曾经胜利增加,然而视图并未刷新。这是因为在 Vue 实例创立时,obj.b 并未申明,因而就没有被 Vue 转换为响应式的属性,天然就不会触发视图的更新,这时就须要应用 Vue 的全局 api $set():

addObjB () (this.$set(this.obj, 'b', 'obj.b')
   console.log(this.obj)
}

$set()办法相当于手动的去把 obj.b 解决成一个响应式的属性,此时视图也会跟着扭转了。

对 SSR 的了解

SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端实现,而后再把 html 间接返回给客户端

SSR 的劣势:

  • 更好的 SEO
  • 首屏加载速度更快

SSR 的毛病:

  • 开发条件会受到限制,服务器端渲染只反对 beforeCreate 和 created 两个钩子;
  • 当须要一些内部扩大库时须要非凡解决,服务端渲染应用程序也须要处于 Node.js 的运行环境;
  • 更多的服务端负载。

Vue data 中某一个属性的值产生扭转后,视图会立刻同步执行从新渲染吗?

不会立刻同步执行从新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立刻变动,而是按肯定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只有侦听到数据变动,Vue 将开启一个队列,并缓冲在同一事件循环中产生的所有数据变更。

如果同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除反复数据对于防止不必要的计算和 DOM 操作是十分重要的。而后,在下一个的事件循环 tick 中,Vue 刷新队列并执行理论(已去重的)工作。

extend 有什么作用

这个 API 很少用到,作用是扩大组件生成一个结构器,通常会与 $mount 一起应用。

// 创立组件结构器
let Component = Vue.extend({template: "<div>test</div>"});
// 挂载到 #app 上 new Component().$mount('#app')
// 除了下面的形式,还能够用来扩大已有的组件
let SuperComponent = Vue.extend(Component);
new SuperComponent({created() {console.log(1);
  },
});
new SuperComponent().$mount("#app");

应用 Object.defineProperty() 来进行数据劫持有什么毛病?

在对一些属性进行操作时,应用这种办法无奈拦挡,比方通过下标形式批改数组数据或者给对象新增属性,这都不能触发组件的从新渲染,因为 Object.defineProperty 不能拦挡到这些操作。更准确的来说,对于数组而言,大部分操作都是拦挡不到的,只是 Vue 外部通过重写函数的形式解决了这个问题。

在 Vue3.0 中曾经不应用这种形式了,而是通过应用 Proxy 对对象进行代理,从而实现数据劫持。应用 Proxy 的益处是它能够完满的监听到任何形式的数据扭转,惟一的毛病是兼容性的问题,因为 Proxy 是 ES6 的语法。

你是怎么解决 vue 我的项目中的谬误的?

剖析

  • 这是一个综合利用题目,在我的项目中咱们经常须要将 App 的异样上报,此时错误处理就很重要了。
  • 这里要辨别谬误的类型,针对性做收集。
  • 而后是将收集的的错误信息上报服务器。

思路

  • 首先辨别谬误类型
  • 依据谬误不同类型做相应收集
  • 收集的谬误是如何上报服务器的

答复范例

  1. 利用中的谬误类型分为 ”接口异样 “ 和“ 代码逻辑异样
  2. 咱们须要依据不同谬误类型做相应解决:接口异样是咱们申请后端接口过程中产生的异样,可能是申请失败,也可能是申请取得了服务器响应,然而返回的是谬误状态。以 Axios 为例,这类异样咱们能够通过封装 Axios,在拦截器中对立解决整个利用中申请的谬误。 代码逻辑异样 是咱们编写的前端代码中存在逻辑上的谬误造成的异样,vue利用中最常见的形式是应用全局谬误处理函数 app.config.errorHandler 收集谬误
  3. 收集到谬误之后,须要对立解决这些异样:剖析谬误,获取须要错误信息和数据。这里应该无效辨别谬误类型,如果是申请谬误,须要上报接口信息,参数,状态码等;对于前端逻辑异样,获取谬误名称和详情即可。另外还能够收集利用名称、环境、版本、用户信息,所在页面等。这些信息能够通过 vuex 存储的全局状态和路由信息获取

实际

axios拦截器中解决捕捉异样:

// 响应拦截器
instance.interceptors.response.use((response) => {return response.data;},
  (error) => {
    // 存在 response 阐明服务器有响应
    if (error.response) {
      let response = error.response;
      if (response.status >= 400) {handleError(response);
      }
    } else {handleError(null);
    }
    return Promise.reject(error);
  },
);

vue中全局捕捉异样:

import {createApp} from 'vue'
​
const app = createApp(...)
​
app.config.errorHandler = (err, instance, info) => {// report error to tracking services}

解决接口申请谬误:

function handleError(error, type) {if(type == 1) {
    // 接口谬误,从 config 字段中获取申请信息
    let {url, method, params, data} = error.config
    let err_data = {
       url, method,
       params: {query: params, body: data},
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}

解决前端逻辑谬误:

function handleError(error, type) {if(type == 2) {
    let errData = null
    // 逻辑谬误
    if(error instanceof Error) {let { name, message} = error
      errData = {
        type: name,
        error: message
      }
    } else {
      errData = {
        type: 'other',
        error: JSON.strigify(error)
      }
    }
  }
}

vue 中应用了哪些设计模式

  • 工厂模式 传入参数即可创立实例:虚构 DOM 依据参数的不同返回根底标签的 Vnode 和组件 Vnode
  • 单例模式 整个程序有且仅有一个实例:vuexvue-router 的插件注册办法 install 判断如果零碎存在实例就间接返回掉
  • 公布 - 订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装璜模式: (@装璜器的用法)
  • 策略模式 策略模式指对象有某个行为, 然而在不同的场景中, 该行为有不同的实现计划 - 比方选项的合并策略

vue-cli 工程罕用的 npm 命令有哪些

  • 下载 node_modules 资源包的命令:
npm install
  • 启动 vue-cli 开发环境的 npm 命令:
npm run dev
  • vue-cli 生成 生产环境部署资源 的 npm命令:
npm run build
  • 用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:
npm run build --report

在浏览器上自动弹出一个 展现 vue-cli 工程打包后 app.jsmanifest.jsvendor.js 文件外面所蕴含代码的页面。能够具此优化 vue-cli 生产环境部署的动态资源,晋升 页面 的加载速度

动静给 vue 的 data 增加一个新的属性时会产生什么?怎么解决?

Vue 不容许在曾经创立的实例上动静增加新的响应式属性

若想实现数据与视图同步更新,可采取上面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set()

Vue.set(target, propertyName/index, value)

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过 Vue.set 向响应式对象中增加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新

对于 Vue.set 源码(省略了很多与本节不相干的代码)

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

function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用 defineReactive 办法,实现新增属性的响应式

对于 defineReactive 办法,外部还是通过 Object.defineProperty 实现属性拦挡

大抵代码如下:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {get() {console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {if (newVal !== val) {console.log(`set ${key}:${newVal}`);
                val = newVal
            }
        }
    })
}

Object.assign()

间接应用 Object.assign() 增加到对象的新属性不会触发更新

应创立一个新的对象,合并原对象和混入对象的属性

this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

$forceUpdate

如果你发现你本人须要在 Vue中做一次强制更新,99.9% 的状况,是你在某个中央做错了事

$forceUpdate迫使Vue 实例从新渲染

PS:仅仅影响实例自身和插入插槽内容的子组件,而不是所有子组件。

小结

  • 如果为对象增加大量的新属性,能够间接采纳Vue.set()
  • 如果须要为新对象增加大量的新属性,则通过 Object.assign() 创立新对象
  • 如果你切实不晓得怎么操作时,可采取 $forceUpdate() 进行强制刷新 (不倡议)

PS:vue3是用过 proxy 实现数据响应式的,间接动静增加新属性仍能够实现数据响应式

Vue 我的项目本地开发实现后部署到服务器后报 404 是什么起因呢

如何部署

前后端拆散开发模式下,前后端是独立布署的,前端只须要将最初的构建物上传至指标服务器的 web 容器指定的动态目录下即可

咱们晓得 vue 我的项目在构建后,是生成一系列的动态文件

惯例布署咱们只须要将这个目录上传至指标服务器即可

web 容器跑起来,以 nginx 为例

server {
  listen  80;
  server_name  www.xxx.com;

  location / {index  /data/dist/index.html;}
}

配置实现记得重启nginx

// 查看配置是否正确
nginx -t 

// 平滑重启
nginx -s reload

操作完后就能够在浏览器输出域名进行拜访了

当然下面只是提到最简略也是最间接的一种布署形式

什么自动化,镜像,容器,流水线布署,实质也是将这套逻辑形象,隔离,用程序来代替重复性的劳动,本文不开展

404 问题

这是一个经典的问题,置信很多同学都有遇到过,那么你晓得其真正的起因吗?

咱们先还原一下场景:

  • vue我的项目在本地时运行失常,但部署到服务器中,刷新页面,呈现了 404 谬误

先定位一下,HTTP 404 谬误意味着链接指向的资源不存在

问题在于为什么不存在?且为什么只有 history 模式下会呈现这个问题?

为什么 history 模式下有问题

Vue是属于单页利用(single-page application)

SPA 是一种网络应用程序或网站的模型,所有用户交互是通过动静重写以后页面,后面咱们也看到了,不论咱们利用有多少页面,构建物都只会产出一个index.html

当初,咱们回头来看一下咱们的 nginx 配置

server {
  listen  80;
  server_name  www.xxx.com;

  location / {index  /data/dist/index.html;}
}

能够依据 nginx 配置得出,当咱们在地址栏输出 www.xxx.com 时,这时会关上咱们 dist 目录下的 index.html 文件,而后咱们在跳转路由进入到 www.xxx.com/login

要害在这里,当咱们在 website.com/login 页执行刷新操作,nginx location 是没有相干配置的,所以就会呈现 404 的状况

为什么 hash 模式下没有问题

router hash 模式咱们都晓得是用符号 #示意的,如 website.com/#/login, hash 的值为 #/login

它的特点在于:hash 尽管呈现在 URL 中,但不会被包含在 HTTP 申请中,对服务端齐全没有影响,因而扭转 hash 不会从新加载页面

hash 模式下,仅 hash 符号之前的内容会被蕴含在申请中,如 website.com/#/login 只有 website.com 会被蕴含在申请中,因而对于服务端来说,即便没有配置 location,也不会返回404 谬误

解决方案

看到这里我置信大部分同学都能想到怎么解决问题了,

产生问题的实质是因为咱们的路由是通过 JS 来执行视图切换的,

当咱们进入到子路由时刷新页面,web容器没有绝对应的页面此时会呈现404

所以咱们只须要配置将任意页面都重定向到 index.html,把路由交由前端解决

nginx 配置文件 .conf 批改,增加try_files $uri $uri/ /index.html;

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
    try_files $uri $uri/ /index.html;
  }
}

批改完配置文件后记得配置的更新

nginx -s reload

这么做当前,你的服务器就不再返回 404 谬误页面,因为对于所有门路都会返回 index.html 文件

为了防止这种状况,你应该在 Vue 利用外面笼罩所有的路由状况,而后在给出一个 404 页面

const router = new VueRouter({
  mode: 'history',
  routes: [{ path: '*', component: NotFoundComponent}
  ]
})

MVVM、MVC、MVP 的区别

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,次要通过拆散关注点的形式来组织代码构造,优化开发效率。

在开发单页面利用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简略我的项目时,可能看不出什么问题,如果我的项目变得复杂,那么整个文件就会变得简短、凌乱,这样对我的项目开发和前期的我的项目保护是十分不利的。

(1)MVC

MVC 通过拆散 Model、View 和 Controller 的形式来组织代码构造。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 利用了观察者模式,当 Model 层产生扭转的时候它会告诉无关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它次要负责用户与利用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来实现对 Model 的批改,而后 Model 层再去告诉 View 层更新。

(2)MVVM

MVVM 分为 Model、View、ViewModel:

  • Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
  • View 代表 UI 视图,负责数据的展现;
  • ViewModel 负责监听 Model 中数据的扭转并且管制视图的更新,解决用户交互操作;

Model 和 View 并无间接关联,而是通过 ViewModel 来进行分割的,Model 和 ViewModel 之间有着双向数据绑定的分割。因而当 Model 中的数据扭转时会触发 View 层的刷新,View 中因为用户交互操作而扭转的数据也会在 Model 中同步。

这种模式实现了 Model 和 View 的数据主动同步,因而开发者只须要专一于数据的保护操作即可,而不须要本人操作 DOM。

(3)MVP

MVP 模式与 MVC 惟一不同的在于 Presenter 和 Controller。在 MVC 模式中应用观察者模式,来实现当 Model 层数据发生变化的时候,告诉 View 层的更新。这样 View 层和 Model 层耦合在一起,当我的项目逻辑变得复杂的时候,可能会造成代码的凌乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过应用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的 Controller 只晓得 Model 的接口,因而它没有方法管制 View 层的更新,MVP 模式中,View 层的接口裸露给了 Presenter 因而能够在 Presenter 中将 Model 的变动和 View 的变动绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还蕴含了其余的响应逻辑。

说下 $attrs 和 $listeners 的应用场景

API 考查,但 $attrs$listeners是比拟少用的边界常识,而且 vue3 有变动,$listeners曾经移除,还是有细节可说的

体验

一个蕴含组件透传属性的对象

<template>
    <child-component v-bind="$attrs">
        将非属性个性透传给外部的子组件
    </child-component>
</template>

答复范例

  • 咱们可能会有一些属性和事件没有在 props 中定义,这类称为非属性个性,联合 v-bind 指令能够间接透传给外部的子组件。
  • 这类“属性透传”经常用于包装高阶组件时往外部传递属性,罕用于爷孙组件之间传参。比方我在扩大 A 组件时创立了组件 B 组件,而后在 C 组件中应用 B,此时传递给 C 的属性中只有 props 外面申明的属性是给 B 应用的,其余的都是 A 须要的,此时就能够利用 v-bind="$attrs" 透传下去。
  • 最常见用法是联合 v-bind 做开展;$attrs自身不是响应式的,除非拜访的属性自身是响应式对象。
  • vue2中应用 listeners 获取事件,vue3中已移除,均合并到 attrs 中, 应用起来更简略了

原理

查看透传属性 foo 和一般属性 bar,发现vnode 构造完全相同,这阐明 vue3 中将分辨两者工作由框架实现而非用户指定:

<template>
  <h1>{{msg}}</h1>
  <comp foo="foo" bar="bar" />
</template>
<template>
  <div>
    {{$attrs.foo}} {{bar}}
  </div>
</template>

<script setup>
defineProps({bar: String})
</script>
_createVNode(Comp, {
    foo: "foo",
    bar: "bar"
})

应用 vue 渲染大量数据时应该怎么优化?说下你的思路!

剖析

企业级我的项目中渲染大量数据的状况比拟常见,因而这是一道十分好的综合实际题目。

答复

  1. 在大型企业级我的项目中常常须要渲染大量数据,此时很容易呈现卡顿的状况。比方大数据量的表格、树
  2. 解决时要依据状况做不同解决:
  3. 能够采取分页的形式获取,防止渲染大量数据
  • vue-virtual-scroller (opens new window)等虚构滚动计划,只渲染视口范畴内的数据
  • 如果不须要更新,能够应用 v -once 形式只渲染一次
  • 通过 v -memo (opens new window)能够缓存后果,联合 v-for 应用,防止数据变动时不必要的 VNode 创立
  • 能够采纳懒加载形式,在用户须要的时候再加载数据,比方 tree 组件子树的懒加载
  • 还是要看具体需要,首先从设计上防止大数据获取和渲染;切实须要这样做能够采纳虚表的形式优化渲染;最初优化更新,如果不须要更新能够 v-once 解决,须要更新能够 v-memo 进一步优化大数据更新性能。其余能够采纳的是交互方式优化,无线滚动、懒加载等计划

说下你的 vue 我的项目的目录构造,如果是大型项目你该怎么划分构造和划分组件呢

一、为什么要划分

应用 vue 构建我的项目,我的项目构造清晰会进步开发效率,相熟我的项目的各种配置同样会让开发效率更高

在划分我的项目构造的时候,须要遵循一些根本的准则:

  • 文件夹和文件夹外部文件的语义一致性
  • 繁多入口 / 进口
  • 就近准则,紧耦合的文件应该放到一起,且应以相对路径援用
  • 公共的文件应该以绝对路径的形式从根目录援用
  • /src 外的文件不应该被引入

文件夹和文件夹外部文件的语义一致性

咱们的目录构造都会有一个文件夹是依照路由模块来划分的,如 pages 文件夹,这个文件夹外面应该蕴含咱们我的项目所有的路由模块,并且仅应该蕴含路由模块,而不应该有别的其余的非路由模块的文件夹

这样做的益处在于一眼就从 pages文件夹看出这个我的项目的路由有哪些

繁多入口 / 进口

举个例子,在 pages 文件夹外面存在一个 seller 文件夹,这时候seller 文件夹应该作为一个独立的模块由内部引入,并且 seller/index.js 应该作为内部引入 seller 模块的惟一入口

// 谬误用法
import sellerReducer from 'src/pages/seller/reducer'

// 正确用法
import {reducer as sellerReducer} from 'src/pages/seller'

这样做的益处在于,无论你的模块文件夹外部有多乱,内部援用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需要,你就会发现这种形式的长处

就近准则,紧耦合的文件应该放到一起,且应以相对路径援用

应用相对路径能够保障模块外部的独立性

// 正确用法
import styles from './index.module.scss'
// 谬误用法
import styles from 'src/pages/seller/index.module.scss'

举个例子

假如咱们当初的 seller 目录是在 src/pages/seller,如果咱们后续产生了路由变更,须要加一个层级,变成 src/pages/user/seller

如果咱们采纳第一种相对路径的形式,那就能够间接将整个文件夹拖过来就好,seller 文件夹外部不须要做任何变更。

然而如果咱们采纳第二种绝对路径的形式,挪动文件夹的同时,还须要对每个 import 的门路做批改

公共的文件应该以绝对路径的形式从根目录援用

公共指的是多个路由模块共用,如一些公共的组件,咱们能够放在 src/components

在应用到的页面中,采纳绝对路径的模式援用

// 谬误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'

同样的,如果咱们须要对文件夹构造进行调整。将 /src/components/input 变成 /src/components/new/input,如果应用绝对路径,只须要全局搜寻替换

再加上绝对路径有全局的语义,相对路径有独立模块的语义

src 外的文件不应该被引入

vue-cli脚手架曾经帮咱们做了相干的束缚了,失常咱们的前端我的项目都会有个 src 文件夹,外面放着所有的我的项目须要的资源,js,css, png, svg 等等。src 外会放一些我的项目配置,依赖,环境等文件

这样的益处是不便划分我的项目代码文件和配置文件

二、目录构造

单页面目录构造

project
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
|-- src
    |-- components
        |-- input
            |-- index.js
            |-- index.module.scss
    |-- pages
        |-- seller
            |-- components
                |-- input
                    |-- index.js
                    |-- index.module.scss
            |-- reducer.js
            |-- saga.js
            |-- index.js
            |-- index.module.scss
        |-- buyer
            |-- index.js
        |-- index.js

多页面目录构造

my-vue-test:.
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
└─src
    ├─apis // 接口文件依据页面或实例模块化
    │      index.js
    │      login.js
    │
    ├─components // 全局公共组件
    │  └─header
    │          index.less
    │          index.vue
    │
    ├─config // 配置(环境变量配置不同 passid 等)│      env.js
    │      index.js
    │
    ├─contant // 常量
    │      index.js
    │
    ├─images // 图片
    │      logo.png
    │
    ├─pages // 多页面 vue 我的项目,不同的实例
    │  ├─index // 主实例
    │  │  │  index.js
    │  │  │  index.vue
    │  │  │  main.js
    │  │  │  router.js
    │  │  │  store.js
    │  │  │
    │  │  ├─components // 业务组件
    │  │  └─pages // 此实例中的各个路由
    │  │      ├─amenu
    │  │      │      index.vue
    │  │      │
    │  │      └─bmenu
    │  │              index.vue
    │  │
    │  └─login // 另一个实例
    │          index.js
    │          index.vue
    │          main.js
    │
    ├─scripts // 蕴含各种罕用配置,工具函数
    │  │  map.js
    │  │
    │  └─utils
    │          helper.js
    │
    ├─store //vuex 仓库
    │  │  index.js
    │  │
    │  ├─index
    │  │      actions.js
    │  │      getters.js
    │  │      index.js
    │  │      mutation-types.js
    │  │      mutations.js
    │  │      state.js
    │  │
    │  └─user
    │          actions.js
    │          getters.js
    │          index.js
    │          mutation-types.js
    │          mutations.js
    │          state.js
    │
    └─styles // 款式对立配置
        │  components.less
        │
        ├─animation
        │      index.less
        │      slide.less
        │
        ├─base
        │      index.less
        │      style.less
        │      var.less
        │      widget.less
        │
        └─common
                index.less
                reset.less
                style.less
                transition.less

小结

我的项目的目录构造很重要,因为目录构造能体现很多货色,怎么布局目录构造可能每个人有本人的了解,然而依照肯定的标准去进行目录的设计,能让我的项目整个架构看起来更为简洁,更加易用

正文完
 0