Vue相关问题

1.Vue的双向绑定原理

vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,通过Object.defineProperty()[对象,对象上的属性,描述符]来劫持对象属性的setter和getter操作实现数据驱动视图。具体参考MVVM

  • Vue如何实现监听数组的pushpopsplice方法?

Object.defineProperty对数组进行响应式化是有缺陷的,虽然我们可以监听到索引的改变,但是defineProperty不能检测到数组长度的变化,准确的说是通过改变length而增加的长度不能监测到。

虽然我们无法使用Object.defineProperty将数组进行响应式的处理,也就是getter-setter,但是还有其他的功能可以供我们使用。就是数据描述符,数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

因此我们只要把原型上的方法,进行value的重新赋值。

如下代码,在重新赋值的过程中,我们可以获取到方法名和所有参数。

function def (obj, key) {
  Object.defineProperty(obj, key, {
    writable: true,
    enumerable: true,
    configurable: true,
    value: function(...args) {
      console.log('key', key);
      console.log('args', args); 
    }
  });
}
// 重写的数组方法
let obj = {
  push() {}
}
// 数组方法的绑定
def(obj, 'push');
obj.push([1, 2], 7, 'hello!');
// 控制台输出 key push
// 控制台输出 args [Array(2), 7, "hello!"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Vue监听Array

第一步:先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。

第二步:对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。

第三步:把需要被拦截的 Array 类型的数据原型指向改造后原型。

const arrayProto = Array.prototype // 获取Array的原型
 
function def (obj, key) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    value: function(...args) {
      console.log(key); // 控制台输出 push
      console.log(args); // 控制台输出 [Array(2), 7, "hello!"]
       
      // 获取原生的方法
      let original = arrayProto[key];
      // 将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变
      const result = original.apply(this, args);
 
      // do something 比如通知Vue视图进行更新
      console.log('我的数据被改变了,视图该更新啦');
      this.text = 'hello Vue';
      return result;
    }
  });
}
// 新的原型
let obj = {
  push() {}
}
// 重写赋值
def(obj, 'push');
let arr = [0];
// 原型的指向重写
arr.__proto__ = obj;
// 执行push
arr.push([1, 2], 7, 'hello!');
console.log(arr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

2.Vue组件通信

对于多组件之间的状态共享,我们需要通过state规定统一的数据仓库,
只能通过Action和Mutation实现数据的更改,然后实现多组件的数据同时改变
总结:props父传子,$emit自定义事件子传父:都是(vue命令)子组件内容 = 父组件内容
1.父组件向子组件传值
父组件中的 v-bind:'子组件props中的属性名称(若子组件props中的名称为myName,
由于HTML特性不区分大小写,所以这里采用my-name)'='父组件中的属性名称'
注意如果是静态字符串则可以不用v-bind,否则必须使用
2.子组件向父组件传值(自定义事件,回调函数)
----自定义事件-----
子组件通过事件'$emit('modify', this.modifylist);'传递消息给父组件,
再用v-on:'子组件事件名称'='父组件事件名称'
简化写法即为`v-model`(主要用于表单的双向绑定),实际上是
`<input type="text" v-model="name">`
相当于:`<input type="text" :value="name" @input="name = $event.target.value">`
----回调函数----
通过'v-bind:callback="callback"',用props将回调函数传给子组件,
然后在子组件中用v-on:click="callback"绑定该方法
3.兄弟组件传值(可以用一个父组件做中继,也可以用事件总线,介绍一下事件总线)
新建一个bus.js(事件总线EventBus)
import Vue from 'Vue'
const eventbus =new Vue()
export default eventbus;
并在兄弟组件中引入,然后:
组件1用eventBus.$emit('1to2', 'emmit')
组件2则用eventBus.$on('1to2', (message) => {this.fromBrother = message}) 保证事件名称相同
4.更深层次的注入provide/inject
在组件中使用provide:
provide: function () {
  return {
    getMap: this.getMap
  }
}
其所有的子组件及后代可以通过inject来进行获取getMap方法:
inject: ['getMap']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

Tips:Vue父子组件传值传引用类型的时候,修改父组件对象中的某个值,利用props则发现子组件使用相应属性时没有发生修改,具体操作方式有:

  • 通过深拷贝拷贝传入子组件的对象,则能够实现监听
  • 通过子组件中使用watch对修改的对象的某个属性进行监听

3.Vue的生命周期

img

vue源码中最终执行生命周期函数都是调用callHook方法,callHook函数的逻辑很简单,根据传入的生命周期类型 hook,去拿到 vm.$options[hook]对应的回调函数数组,然后遍历执行,执行的时候把 vm作为函数执行的上下文。

  1. new Vue(options):创建一个vm实例;

  2. mergeOptions(resolveConstructorOptions(vm.constructor), options, vm):合并Vue构造函数里options和传入的options或合并父子的options。比如:在mergeOptions函数中会调用mergeHook方法合并生命周期的钩子函数,mergeHook方法原理是只有父时返回父,只有子时返回数组类型的子。父、子都存在时,将子添加在父的后面返回组合而成的数组。这也是父子均有钩子函数的时候,先执行父的后执行子的的原因;

  3. initLifecycle(vm)、initEvents(vm)、initRender(vm):在创建的vm实例上初始化生命周期、事件、渲染相关的属性;

  4. callHook(vm, 'beforeCreate'):调用beforeCreate生命周期钩子函数;

  5. initInjections(vm)、initState(vm)、initProvide(vm):初始化数据:inject、state、provide。initState 的作用是初始化 props、data、methods、watch、computed 等属性;

  6. callHook(vm, 'created'):调用created生命周期钩子函数;

  7. vm.$mount(vm.$options.el)$mount方法在多个文件中都有定义,如"src/platform/web/entry-runtime-with-compiler.js"、"src/platform/web/runtime/index.js"、"src/platform/weex/runtime/index.js"。因为$mount方法的实现是和平台、构建方式相关的。以"entry-runtime-with-compiler.js"为例,关键步骤是查看vm.$options中是否有render方法,如果没有则会根据el和template属性确定最终的template字符串,再调用compileToFunctions方法将template字符串转为render方法,最后,调用原先原型上的$mount方法,即开始执行"lifecycle.js"中mountComponent方法;

  8. callHook(vm, 'beforeMount'):调用beforeMount生命周期钩子函数;

  9. vm._render() => vm._update() => vm.__patch__():先执行vm._render方法,即调用createElement生成虚拟DOM,即VNode ,每个VNode有children ,children 每个元素也是⼀个 VNode,这样就形成了⼀个 VNode Tree;再调用vm._update方法进行首次渲染,vm._update方法核心是调用vm.patch方法,这个方法跟vm.$mount一样跟平台相关;vm.patch方法则是根据生成的VNode Tree递归createElm方法创建真实Dom Tree挂载到Dom上;

  10. callHook(vm, 'mount'):调用mount生命周期钩子函数:VNode patch 到 Dom 之后会执行 'invokeInsertHook'函数,把insertedVnodeQueue中保存的mount钩子函数执行一遍,insertedVnodeQueue队列中的钩子函数是在根据VNode Tree递归createElm方法创建真实Dom Tree过程生成的钩子函数顺序队列,因此mounted钩子函数的执行顺序是先子后父;

  11. data changes:数据更新,nextTick中执行flushSchedulerQueue方法,该方法会执行watcher队列中的watcher;

  12. callHook(vm, 'beforeUpdate'):执行watcher时会执行watcher的before方法,即调用beforeUpdate生命周期钩子函数;

  13. Virtual DOM re-render and patch:重新render生成新的Virtual DOM,并且patch到DOM上;

  14. callHook(vm, 'updated'):调用updated生命周期钩子函数;

  15. vm.$destroy():启动卸销毁过程;

  16. callHook(vm, 'beforeDestroy'):调用beforeDestroy生命周期钩子函数;

  17. Teardown watchers, childcomponents and event listeners:执行一系列销毁动作,在 $destroy 的执行过程中,它又会执行vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroyed 钩子函数执行顺序是先子后父,和 mounted 过程一样。

  18. callHook(vm, 'destroyed '):调用destroyed 生命周期钩子函数。

errorCaptured生命周期钩子函数

  • 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
  • 如果一个组件的继承或父级从属链路中存在多个 errorCaptured 钩子,则它们将会被相同的错误逐个唤起。
  • 默认情况下,如果全局的 config.errorHandler 被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。

keep-alive组件

  • keep-alive组件是vue的**内置抽象(abstract)**组件,抽象组件在initLifecycle过程中 组件实例建立父子关系时会被忽略,因此他自身不会渲染一个DOM元素,也不会出现在父组件链中。

  • keep-alive作用是用于包裹动态组件,缓存不活动的组件实例。keep-alive自定义实现了render函数并利用了插槽slot,render函数中先获取它的默认插槽,再获取到它的第一个组件子节点,因此keep-alive组件只处理第一个子元素,所以一般和它搭配使用的有component动态组件或者router-view。

  • keep-alive组件在created钩子中定义了 this.cache 和 this.keys,本质上是去缓存已创建的 vnode,缓存策略采用LRU策略,每次缓存命中时将当前vnode移到缓存数组末尾,需要删除时则删除缓存数组第一个vnode。

  • keep-alive组件接收三个props:

    1. include:数组、字符串或者正则表达式,只有匹配的组件才会缓存。

    2. exclude:数组、字符串或者正则表达式,任何匹配的组件都不会被缓存。

    3. max:字符串或数字,指定可以缓存的组件最大个数,如果个数超过max,则销毁缓存数组中的第一个组件。

  • keep-alive组件子组件渲染机制:

    1. 首次渲染:和普通组件一样执行正常的init生命周期钩子函数,同时将生成的vnode缓存到内存中;

    2. 组件切换:切换到新组件时,旧组件不会销毁,而是变成未激活状态,即不会执行destroy相关的钩子函数,而是执行 deactivated 生命周期钩子函数,如果新组件不在缓存数组中,则执行首次渲染,否则执行缓存渲染;

    3. 缓存渲染:缓存渲染即组件由未激活状态变成激活状态,因此不会执行created、mounted等钩子函数,而是执行 activated 生命周期钩子函数。

4.Vue和React的区别

相同点:

  1. 虚拟 DOM
  2. 组件化
  3. 保持对视图的关注
  4. 数据驱动视图
  5. 都有支持 native 的方案

不同点:

  1. state 状态管理 vs 对象属性 get,set。
  2. vue 实现了数据的双向绑定 v-model,而组件之间的 props 传递是单向的,react 数据流动是单向的。

运行时优化

在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树,开发者不得不手动使用 shouldComponentUpdate 去优化性能。

在 Vue 组件的依赖是在渲染过程中自动追踪的,开发者不再需要考虑此类优化。另外 Vue 还做了很多其他方面的优化,例如:标记静态节点,优化静态循环等。

总结:Vue 在运行时帮我们做了很多优化了处理,开发者可以直接上手,React 则是由开发者自己去处理优化,让程序有更多的定制化。

JSX vs Templates

JSX 中你可以使用完整的编程语言 JavaScript 功能来构建你的视图页面。比如你可以使用临时变量、JS 自带的流程控制、以及直接引用当前 JS 作用域中的值等等。

Templates 对于很多习惯了 HTML 的开发者来说,模板比起 JSX 读写起来更自然。基于 HTML 的模板使得将已有的应用逐步迁移到 Vue 更为容易。你甚至可以使用其他模板预处理器,比如 Pug 来书写 Vue 的模板。

总结:Vue 在模板上实现定制化,可以使用类 HTML 模板,以及可以使用 JSX,React 则是推荐 JSX。

5.Vue项目性能优化

路由懒加载

将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身),使用动态 import语法来定义代码分块点 (split point):

const Foo = () => import('./Foo.vue')
1

代码优化

  • v-if 和 v-show选择调用
  • 细分vuejs组件,组件按需加载
  • 减少watch的数据
  • 利用vue-lazy实现图片懒加载
  • SSR
  • webpack配置:js打包多个文件,压缩图片(npm run build --report查看打包体积问题)

用户体验

  • loading图
  • 骨架屏
  • 点击延迟

6.Vuex解决了什么问题

1.多个组件共享状态时,单向数据流的简洁性很容易被破坏:
2.多个视图依赖于同一状态。
3.来自不同视图的行为需要变更同一状态。
Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex))
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到state
主要有这五个模块:
getter mutation action state module
1
2
3
4
5
6
7

7.讲讲Vue的diff算法

虚拟DOM:用JS对象来表示DOM对象

var Vnode = {
    tag: 'div',
    children: [
        { tag: 'p', text: '123' }
    ]
};
1
2
3
4
5
6

diff比较方式:比较新旧节点只会同层级比较,第一层不一样则不会继续比较(实际上Vue的diff算法是广度优先

先patchVnode新旧节点,不是同一个节点则replace,否则判断子节点:

  • 找到对应的真实dom,称为el
  • 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点

updateChild实现过程:

  • 将Vnode的子节点Vch和oldVnode的子节点oldCh提取出来
  • oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。

8.MVC和MVVM的区别

  • Model用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法;
  • View作为视图层,主要负责数据的展示;
  • Controller定义用户界面对用户输入的响应方式,它连接模型和视图,用于控制应用程序的流程,处理用户的行为和数据上的改变。

MVC将响应机制封装在controller对象中,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。 MVVM把View和Model的同步逻辑自动化了。以前Controller负责的View和Model同步不再手动地进行更新操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。也就是双向数据绑定,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。

9.Vue组件实现方式汇总

  • v-model语法糖实现同步父子组件

  • 表单验证则是通过获取子组件实例进行封装

  • 全局弹窗组件则是通过手动mounted挂载实现

  • 也可以使用Vue的高阶组件实现throttle或者debounce的封装

10.Vue的computed和watch有什么区别?

  • computed特性

1.是计算值, 2.应用:就是简化tempalte里面{{}}计算和处理props或$emit的传值 3.具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数

  • watch特性

1.是观察的动作, 2.应用:监听props,$emit或本组件的值执行异步操作 3.无缓存性,页面重新渲染时值不变化也会执行

computed原理

1. data 属性初始化 getter setter
2. computed 计算属性初始化,提供的函数将用作属性 vm.reversedMessage 的 getter
3. 当首次获取 reversedMessage 计算属性的值时,Dep 开始依赖收集
4. 在执行 message getter 方法时,如果 Dep 处于依赖收集状态,则判定 message 为 reversedMessage 的依赖,并建立依赖关系
5. 当 message 发生变化时,根据依赖关系,触发 reverseMessage 的重新计算
1
2
3
4
5

11.Vue的nextTick实现原理和应用场景?

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

在 created 和 mounted 阶段,如果需要操作渲染后的试图,也要使用 nextTick 方法。

官方文档说明:

注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

nextTick将需要延迟的函数放到了一个异步队列中执行,setTimeout或Promise等,来起到延迟执行的作用。

很多用途都是用于将函数放到Dom更新后执行,比如在created生命周期中拿不到dom因为还没渲染挂载到页面,这时就需要将对dom的操作放到nexttick函数中。那么为什么nexttick中的函数能延迟到dom更新完成后呢?

因为采用的是异步回调,所有异步函数都会在同步函数执行完之后在进行调用,而DOM的更新在同一事件循环中是同步的,所以能在其后执行。