热气冒泡的 Vue 大杂烩面 试题(tu xie zheng li)__Vue.js
发布于 8 个月前 作者 banyungong 504 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

Vue 实现的原理

把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 👉Object.defineProperty 把这些 property 全部转为 👉getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

挂载数据对象data选项vue遍历此对象所有的属性,通过Object.defineProperty转为为getter/settergetter 做的事情是依赖收集setter 做的事情是派发更新。即对数据进行响应式劫持。(将数据data变成可观察(observable))

Vue 3放弃 Object.defineProperty ,使用更快的原生 Proxy👉Vue 组合式 API

<figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">网络转载,如有侵权请联系删除<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">网络转载,如有侵权请联系删除</figcaption></figure>

new Vue() 发生了什么?

new Vue() --> 调用 this._init() -->合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher等等。

在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。

<figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">网络转载,如有侵权请联系删除<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">网络转载,如有侵权请联系删除</figcaption></figure>

Vue 的挂载过程。$mount 方法实际上会去调用 mountComponent 方法,mountComponent核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数

函数最后判断为根节点的时候设置 vm._isMountedtrue, 表示这个实例已经挂载了,同时执行 mounted 钩子函数

总结:

this_init()主要做了两件事:

  1. 初始化(包括生命周期、事件、render函数、state等)。
  2. $mount组件。

组件渲染过程

初次渲染过程:

  • 解析模板为render函数
  • 触发响应式,监听data属性gettersetter(模板中使用到的变量会触发getter)
  • 执行render函数(触发getter),生成vnode,patch(elem,vnode)渲染到页面上

更新过程:

  • 修改data的数据,触发setter(此前data数据在getter中已被监听)
  • 重新执行render函数,生成newVnode(新的虚拟dom)
  • 使用 patch(vnode,newVnode)更新到页面上

如何追踪变化?

<figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">data<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">data</figcaption></figure>
  1. 解析模板为render函数,通过render函数生成vdom
  2. 执行render函数,触发data中的getter
  3. getter方法收集依赖(通俗点就是,在模板里面触发了哪个变量的getter,就把哪个变量观察起来)
  4. 在依赖中setter修改data中的数据的时候,Notify看下修改的那个数据是不是之前被观察起来的
  5. 如果是之前观察起来的,就重新渲染( re-render),重新生成render函数,生成newVdom形成一个闭环

vue中如何检测数组的变化 ?

通过分析Vue源码中的src/observer/array.js文件,我们可以知道,Vue对pushpopshiftunshiftsplicesortreverse等方法进行了重写。

Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。通过Object.defineProperty重写相关属性。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

# src/core/observer/array.js

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */

methodsToPatch.forEach(function (method{
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args// 从新定义属性
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

# src/core/utils/lang.js 
/**
 * Define a property. 定义属性
 */

export function def (obj: Object, key: string, val: any, enumerable?: boolean{
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writabletrue,
    configurabletrue
  })
}

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(👉Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组.

push();pop();shift();unshift();splice();sort();reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。👉Proxy MDN传送

👉12道vue高频原理面试题,你能答出几道?

Object.defineProperty VS Proxy

1. Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。

由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。

2. Object.defineProperty对新增属性需要手动进行Observe。

由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。

3. Proxy支持13种拦截操作,这是defineProperty所不具有的

👉为什么Vue3.0使用Proxy实现数据监听(defineProperty表示不背这个锅)


Vue的生命周期(四季轮回)

Vue来说它的生命周期就是Vue实例从创建到销毁的过程。具体生命周期图可查看👉官方传送

beforeCreate<创建>created beforeMount<挂载>mounted beforeUpdate<更新>updated beforeDestroy<销毁>destroyed

# src/core/shared/constants.js
# 定义生命周期钩子函数
export const LIFECYCLE_HOOKS = [
  'beforeCreate'// 是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。
  'created'// 在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。
  'beforeMount'// 发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。
  'mounted'// 在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
  'beforeUpdate'// 发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
  'updated'// 发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
  'beforeDestroy'// 发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
  'destroyed'// 发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
  'activated'// 钩子函数是专门为 `keep-alive` 组件定制的钩子,结合 `keep-alive` 组件,可以实现从其他页面跳转到这个页面时会请求数据,当从下级页面返回这个页面时就不会重新请求数据 
  'deactivated',  // 钩子函数是专门为 `keep-alive` 组件定制的钩子 
  'errorCaptured',
  'serverPrefetch'
]

👉Vue生命周期activated之返回上一页不重新请求数据

👉从源码解读Vue生命周期,让面试官对你刮目相看


Vue事件机制

Vue.js为我们提供了四个事件API,分别是👉on](https://cn.vuejs.org/v2/api/#vm-on-event-callback),[once👉off](https://cn.vuejs.org/v2/api/#vm-off-event-callback),[emit

原生事件绑定是通过addEventListener绑定给真实元素的,

组件事件绑定是通过Vue自定义的$on实现的。

<figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">2020_07_31_1OVq2k<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">2020_07_31_1OVq2k</figcaption></figure>

初始化事件,在vm上创建一个_events对象。

export function initEvents (vm: Component{
  vm._events = Object.create(null// /*在vm上创建一个_events对象,用来存放事件。*/
  vm._hasHookEvent = false // /*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
  // init parent attached events /*初始化父组件attach的事件*/
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

vm实例原型上定义,$on$off$off$emit方法。

# src/core/instance/events.js 
# $on方法用来在vm实例上监听一个自定义事件,该事件可用$emit触发。
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

# $once监听一个只能触发一次的事件,在触发以后会自动移除该事件。
 Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on ({
      /*在第一次执行的时候将该事件销毁*/
      vm.$off(event, on)
      /*执行注册的方法*/
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

# $off用来移除自定义事件
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    /*如果不传参数则注销所有事件*/
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    /*如果event是数组则递归注销事件*/
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    /*Github:https://github.com/answershuto*/
    /*本身不存在该事件则直接返回*/
    if (!cbs) {
      return vm
    }
    /*如果只传了event参数则注销该event方法下的所有方法*/
    if (arguments.length === 1) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    /*遍历寻找对应方法并删除*/
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

# $emit用来触发指定的自定义事件。
Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      /*将类数组的对象转换成数组*/
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments1)
      /*遍历执行*/
      for (let i = 0, l = cbs.length; i < l; i++) {
        cbs[i].apply(vm, args)
      }
    }
    return vm
  }

👉Vue事件API


Vue模版编译原理知道吗?

Vue的编译过程就是将template转化为render函数的过程

  1. template通过compile执行parse会被编译成AST,那么AST是什么?
    1. 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。具体可以查看👉抽象语法树
  2. AST 经过generate得到render函数render的返回值是VNode,VNode是Vue的虚拟DOM节点.

具体如下:

compile首先会将模板template进行parse得到一个AST,再通过optimize做一些优化,最后通过generate得到render以及staticRenderFns。

  • parse:会用正则等方式解析template模板中的指令、class、style等数据,形成AST
  • optimize:作用是标记static静态节点,这是Vue在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。
  • generate:将AST转化成render funtion字符串的过程,得到结果是render的字符串以及staticRenderFns字符串

👉聊聊Vue的template编译


计算属性Computed和侦听器Watch

👉计算属性和侦听器 官方传送

对于任何复杂逻辑,你都应当使用计算属性

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的


Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。

Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。

有啥不一样呢?
  • 处理数据场景不同:注意箭头方向
    • 网络转载,如有侵权请联系删除<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">网络转载,如有侵权请联系删除</figcaption>
  • 当依赖的值变化时,在watch中,是可以做一些复杂的操作的,而computed中的依赖,仅仅是一个值依赖于另一个值,是值上的依赖
总结:
  • 如果一个数据需要经过复杂计算就用computed
  • 如果一个数据需要被监听并且对数据做一些操作就用 watch

vue组件中的data为什么是一个函数?

组件是可复用的vue实例,本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

👉data 必须是一个函数


v-if和v-show的区别

当条件不成立时,v-if不会渲染DOM元素

v-show操作的是样式(display),切换当前DOM的显示和隐藏。不管条件成立不成立,都会渲染DOM


vue中v-model的原理

v-model本质上就是语法糖,其实就是既绑定了数据又添加了一个input事件监听

实现靠的还是

  • v-bind:绑定响应式数据
  • 触发oninput 事件并传递数据

👉在组件上使用 v-model

👉v-model 官网

👉vue 中v-model原理及应用

# 父组件
<template>
  <div id="app">
    <Children v-model="msg"></Children>
    <p>{{msg}}</p>
  </div>

</template>

<script>
import Children from "./Children";
  export default {
    components: {
      Children,
    },
    data() {
      return {
        msg"hello"
      }
    },
  }
</script>

# 子组件
<template>
  <div>
    <button @click="change"></button>
  </div>

</template>

<script>
  export default {
    methods: {
      change() {
        this.$emit('input',`world`)
      }
    },
  }
</script>

# 点击后会从 hello -> world


vue组件通信有哪些方式?

vue组件关系和DOM树类似。我们针对不同的使用场景,怎么选择有效的通信方式呢?

<figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">2020_07_31_lhjXu4<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">2020_07_31_lhjXu4</figcaption></figure>
组件通信方式:
  1. props、$emit [适用父子组件场景]

    1. 父组件向子组件传值(子组件通过props接收)
    2. 子组件向父组件传值(通过$emit事件形式
  2. 中央事件总线EventBus [适用隔代、相邻场景]

    1. 原理是:new Vue创建一个EventBus,挂载到Vue原型上。通过$emit$on$off调用相关事件。
  3. vuex大杀器 [一网打尽] 👉官网传送

    1. Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。
  4. $attrs/ $listeners [让父组件之间传递消息给子子组件,即多级组件嵌套需要传递数据]

  5. 简单来说: attrs与listeners 是两个对象, attrs 里存放的是父组件中绑定的非 Props 属性,listeners里存放的是父组件中绑定的非原生事件。

  6. 意思就是父组件传向子组件传的,子组件非prop接受的数据都会放在attrs中,子组件直接用this.attrs获取就可以了。如过从父->孙传,就在子组件中添加v-bind='attrs',就把父组件传来的子组件非props接收的数据全部传到孙组件;我觉得attrs 和 $listeners 属性像两个收纳箱,一个负责收纳属性,一个负责收纳事件,都是以对象的形式来保存数据;

  7. <figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">2020_07_31_JGsARf<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">2020_07_31_JGsARf</figcaption></figure>
  8. provide/inject (主要在开发高阶插件/组件库时使用。不推荐用于普通应用程序代码中。)

  9. $parent / $children &ref [无法在跨级或兄弟间通信。]

  10. $parent / $children:访问父 / 子实例

  11. ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  12. .sync 修饰符 也是一个语法糖而已

    1. <text-document :title.sync="title"></text-document>

      # 当子组件需要更新 title 的值时,它需要显式地触发一个更新事件:
      this.$emit('update:title', newValue)
      # 常用于控制模态框的显示隐藏
总结:

常见使用场景可以分为三类:

父子通信: 父向子传递数据是通过 props,子向父是通过 events( emit);通过父链 / 子链也可以通信(parent / children);ref 也可以访问组件实例;provide / inject API;attrs/listeners 兄弟通信: Bus;Vuex 跨级通信: Bus;Vuex;provide / inject API、attrs/$listeners


vue 中$nextTick作用是什么?

首先Vue中有一个重要的概念:异步更新队列

  1. Vue异步更新DOM的原理:Vue在观察到数据变化时,并不是直接更新DOM,而是开启一个队列,并且缓存同一轮事件循环中的所有数据改变。在缓冲时会除去重复的操作,等到下一轮事件循环时,才开始更新。
  2. 异步更新队列实现的选择 : Vue会根据当前浏览器环境优先使用原生的Promise.thenMutationObserver(现已经换为MessageChannel ,是宏任务),如果都不支持,就会采用setTimeout代替。

最后:$nextTick就是用来告知DOM什么时候更新完,当DOM更新完毕后nextTick方法里面的回调就会执行

为什么要异步更新视图?

现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。

保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。

我们了解到数据的变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick。这就是我们平时在开发的过程中,比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行

export function renderMixin (Vue: Class<Component>{
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function{
    return nextTick(fn, this)
  }
}
# src/core/util/next-tick.js
/**
 * Defer a task to execute it asynchronously.
 */

 /*
    延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
    这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
    目的是延迟到当前调用栈执行完以后执行
*/

export const nextTick = (function ({
  /*存放异步执行的回调*/
  const callbacks = []
  /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
  let pending = false
  /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
  let timerFunc

  /*下一个tick时的回调*/
  function nextTickHandler ({
    /*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
    pending = false
    /*执行所有callback*/
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  /*
    这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
    优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。
    如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
    参考:https://www.zhihu.com/question/55364497
  */

  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    /*使用Promise*/
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // in problematic UIWebViews, Promise.then doesn't completely break, but
      // it can get stuck in a weird state where callbacks are pushed into the
      // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) setTimeout(noop)
    }
  } else if (typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterDatatrue
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    /*使用setTimeout将回调推入任务队列尾部*/
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  /*
    推送到队列中下一个tick时执行
    cb 回调函数
    ctx 上下文
  */

  return function queueNextTick (cb?: Function, ctx?: Object{
    let _resolve
    /*cb存到callbacks中*/
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()


vue双向绑定和 vuex 是否冲突?

当在严格模式中使用Vuex 时,会有。原因是:在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。

解决方案:👉详情看官方文档


vue 的父组件和子组件生命周期钩子执行顺序是怎么样的?

根据生命周期来定阶段过程,主要有以下过程:

  1. 加载渲染过程: 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  2. 子组件更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
  3. 父组件更新过程: 父beforeUpdate->父updated
  4. 销毁过程:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
<figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">2020_08_03_omOZ9j<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">2020_08_03_omOZ9j</figcaption></figure> <figure style="margin: 0; margin-top: 10px; margin-bottom: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center;">2020_08_03_Up9S09<figcaption style="margin-top: 5px; text-align: center; font-size: 14px; margin: 12px auto; color: #999999;">2020_08_03_Up9S09</figcaption></figure>

vue 注册组件有哪些方式?

组件只能有一个根标签

全局注册组件:是直接给全局对象Vue注册了一个组件。在本页内已挂载Vue 实例的DOM目标元素 都可以使用(区别于局部组件只能是挂载哪一个DOM,哪个才能使用)。

# 注册
Vue.component("hello-component",{
    props:["message"],
    template :"<div ><h1>组件定义之全局组件</h1><h4>{{message}}</h4></div>"
});
# 使用
<hello-component message=" global component"></hello-component>

局部注册组件:通过某个 Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件。 js中用反斜线“\”’实现字符串换行

# 注册
var limitComponent = {
    props:["message"],
    template :"<div><h1>{{message}}</h1><input  \
    type='text' v-model='message'></input></div>"

}
new Vue ({
    el : "#app",
    components :{
        "child-component": limitComponent
    }
});
# 使用
<child-component message = "动态局部组件"></child-component>

extend

var com1 = Vue.extend({
   template:'<h3>这是第一种方式</h3>'
});
Vue.component("myCom1",com1);

👉组件注册


在vue-cli中如何批量导入组件?

批量的前提是使用了webpack打包工具,利用require.context后,遍历。

import Vue from "vue"
import upperFirst from "lodash/upperFirst"
import camelCase from "lodash/camelCase"
const requireComponent = require.context(
    './'//组件所在目录的相对路径
    false//是否查询其子目录
    /Base[A-Z]\w+\.(vue|js)$/ //匹配基础组件文件名的正则表达式
)
requireComponent.keys().forEach(fileName=>{
    // 获取文件名
    var names = fileName.split("/").pop().replace(/\.\w+$/,"");//BaseBtn
    // 获取组件配置
    const componentConfig = requireComponent(fileName);
    // 若该组件是通过"export default"导出的,优先使用".default",
    // 否则退回到使用模块的根
    Vue.component(names,componentConfig.default || componentConfig);

👉全局注册通用的基础组件 官网传送


vue中的内置组件keep-alive有啥子用?

keep-alive主要用于保留组件状态避免重新渲染👉keep-alive 官网传送

能够缓存组件,防止重复渲染页面,提高用户体验,同时可以很大程度上减少接口请求,减小服务器压力。


vue中extend、mixins和extends有什么区别?

  1. extend:创建组件的构造函数,Vue.extend是为了创建可复用的组件
  2. mixins:mixins是对组件的扩充,包括methods、components、directive等。
  3. extends:与mixins类似;合并规则和mixins一致。extends允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件

继承钩子函数的时候,是不进行覆盖的,extends的钩子函数先触发,然后再是mixins的钩子函数触发,最后就是组件自身的钩子函数触发。extends优先级比mixins优先级高,先运行优先级高的钩子函数。

👉详细可查看这篇文章

👉vue源码解析推荐ustbhuangyi老师 传送

下一篇学习vue-router

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: zlevai 原文链接:https://juejin.im/post/6856606052889788429

回到顶部