Vue.js的通识必修

一、什么是 MVVM?

Model–View–ViewModel (MVVM) 是一个软件架构设计模式。MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。

MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:
在这里插入图片描述

(1)View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。

(2)Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。

(3)ViewModel 层
ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。

需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。

MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维ViewModel更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现:

(1)View 层

<div id="app">
  <p>{{ message }}</p>
  <button v-on:click="showMessage()">Click me</button>
</div>

(2)ViewModel 层

var app = new Vue({
  el: '#app',
  data: {  // 用于描述视图状态   
    message: 'Hello Vue!',
  },
  methods: {  // 用于描述视图行为  
    showMessage() {
      let vm = this;
      alert(vm.message);
    }
  },
  created() {
    let vm = this;
    // Ajax 获取 Model 层的数据
    ajax({
      url: '/your/server/data/api',
      success(res) {
        vm.message = res;
      }
    });
  }
})

(3) Model 层

{
  "url": "/your/server/data/api",
  "res": {
    "success": true,
    "name": "LoveC",
    "domain": "www.cnblogs.com"
  }
}

二、Vue 是如何实现数据双向绑定的(响应式原理)?

Vue.js 的数据响应式原理是基于 JS 标准内置对象方法 Object.defineProperty() 方法来实现,该方法不兼容 IE8 及以下版本浏览器,这也是为什么 Vue.js 只能在这些版本之上的浏览器中才能运行的原因。

Vue 内部通过 Object.defineProperty方法属性拦截的方式,把 data 对象里每个数据的读写转化成 getter/setter,当数据变化时通知视图更新。

1、什么是MVVM数据双向绑定
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
在这里插入图片描述

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

其中,View 变化更新 Data ,可以通过事件监听的方式来实现。

所以我们本文主要讨论如何根据 Data 变化更新 View。我们会通过实现以下 4 个步骤,来实现数据的双向绑定:

  • 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听数据变化

  • 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

  • 实现一个订阅者 WatcherWatcher 订阅者是 ObserverCompile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数

  • 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

以上四个步骤的流程图表示如下:
**加粗样式**

三、Vue3.0中使用的Proxy 与 Object.defineProperty 优劣对比

Vue.js 3.0 使用 proxy 方法实现响应式,两者类似,我们只需搞懂Object.defineProperty()proxy 也就差不多理解了。

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 applyownKeysdeletePropertyhas 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

四、谈谈虚拟DOM

1、虚拟DOM的优缺点

优点:

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

2、虚拟 DOM 实现原理

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JS对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 —— 比较两棵虚拟 DOM 树的差异;
  • patch 算法 —— 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

五、介绍一下Vuex ?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。store基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将store 中的 getter 映射到局部计算属性
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
  • Module:允许将单一的 store 拆分为多个 store 且同时保存在单一的状态树中。
    *在这里插入图片描述

六、vue-router 路由模式有几种?

vue-router 有 3 种路由模式:hashhistoryabstract,对应的源码如下所示:

switch (mode) {
  case 'history':
	this.history = new HTML5History(this, options.base)
	break
  case 'hash':
	this.history = new HashHistory(this, options.base, this.fallback)
	break
  case 'abstract':
	this.history = new AbstractHistory(this, options.base)
	break
  default:
	if (process.env.NODE_ENV !== 'production') {
	  assert(false, `invalid mode: ${mode}`)
	}
}

其中,3 种路由模式的说明如下:

  • hash(浏览器环境): 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;

  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;

  • abstract(Node.js环境) : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式

七、能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?

(1)hash 模式的实现原理

www.test.com/#/ 就是 Hash URL,当 # 后面的哈希值发生变化时,可以通过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面,并且无论哈希值如何变化,服务端接收到的 URL 请求永远是 www.test.com

window.addEventListener('hashchange', () => {
  // ... 具体逻辑
})

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 ‘#search’:

https://www.baidu.com#search

(2)history 模式的实现原理

History 模式是 HTML5 新推出的功能,主要使用history.pushStatehistory.replaceState改变 URL。

这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录后者是直接替换当前的历史记录,如下所示:

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

两种模式对比

  • Hash模式只可以更改 # 后面的内容,History 模式可以通过 API 设置任意的同源 URL
  • History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串
  • Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候

八、Vue 组件间的通信方式?

Vue 组件间通信主要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

(1)props / $emit 适用 父子组件通信
这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

(2)ref$parent / $children 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children:访问父 / 子实例

(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

实现简单的一个事件总线(发布——订阅模式)
JS实现时间总线的本质就是发布-订阅模式,达成任意组件间相互通信的作用。在一个地方触发(发布)事件,然后通过事件中心通知所有订阅者(订阅)。
在这里插入图片描述

class EventBus{
  constructor() {
    // 事件对象,存放订阅的名字和事件
    this.events = {};
  }
  // 订阅事件的方法:监听(发布)
  $on(eventName,callback) {
    if (!this.events[eventName]) {
      // 注意数据,一个名字可以订阅多个事件函数
      this.events[eventName] = [callback];
    } else  {
      // 存在则push到对应事件回调函数数组的尾部保存
      this.events[eventName].push(callback)
    }
  }
  // 触发事件的方法(订阅)
  $emit(eventName, ...args) {
    // 遍历执行所有订阅的事件
    this.events[eventName] && this.events[eventName].forEach(cb => cb(...args));
  }
}

//test
let eventbus = new EventBus();
eventbus.$on('sayHi', function (msg) {
  console.log(msg)
});
eventbus.$emit('sayHi', Math.random())

(4)$attrs/$listeners 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

(5)provide / inject 适用于 隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

// 父组件 A
export default {
  provide: {
    data: 1
  }
}
// 子组件 B
export default {
  inject: ['data'],
  mounted() {
    // 无论跨几层都能获得父组件的 data 属性
    console.log(this.data) // => 1
  }
}

(6)Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

九、v-show 与 v-if 有什么区别?

  • v-show 只是在 display: nonedisplay: block 之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换 CSS,DOM 还是一直保留着的。所以总的来说 v-show 在初始渲染时有更高的开销,但是切换开销很小,更适合于频繁切换的场景。
  • v-if 的话就得说到 Vue 底层的编译了。当属性初始为 false 时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件,所以总的来说在切换时开销更高,更适合不经常切换的场景
  • 并且基于 v-if 的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销

总结:
(1)两者都会导致页面的重绘和重排,但v-show只是改变domcss,而v-if控制的是添加和删除dom,所以v-if在重绘重排前还进行了添加或删除dom元素的操作。
(2)需要多次切换某个元素的显示或隐藏时使用v-show
(3)某个元素在渲染后就一直存在或隐藏时使用v-if
(4)opacity也可以隐藏元素,但它本身的作用并非用来隐藏元素而是设置元素的透明度,并且opacity0时,该dom同样占用着空间。

补充:display:none、visibility:hidden、opacity:0 区别

1、display: none;

  • DOM 结构:浏览器不会渲染 display 属性为 none 的元素,不占据空间;
  • 事件监听:无法进行 DOM 事件监听;
  • 性能:动态改变此属性时会引起重排,性能较差;
  • 继承:不会被子元素继承,毕竟子类也不会被渲染;
  • transition:transition 不支持 display

2、visibility: hidden;

  • DOM 结构:元素被隐藏,但是会被渲染不会消失,占据空间;
  • 事件监听:无法进行 DOM 事件监听;
  • 性 能:动态改变此属性时会引起重绘,性能较高;
  • 继 承:会被子元素继承,子元素可以通过设置 visibility: visible; 来取消隐藏;
  • transition:visibility 会立即显示,隐藏时会延时

opacity: 0;

  • DOM 结构:透明度为 100%,元素隐藏,占据空间;
  • 事件监听:可以进行 DOM 事件监听;
  • 性 能:提升为合成层,不会触发重绘,性能较高;
  • 继 承:会被子元素继承,且,子元素并不能通过 opacity: 1 来取消隐藏;
  • transition:opacity 可以延时显示和隐藏

十、computed 和 watch 的区别和运用的场景?

1、区别

  • computed 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
  • watch 更多的是「观察」的作用,类似于某些数据的监听回调 。监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

2、运用场景

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

  • 对于监听到值的变化需要做一些复杂业务逻辑(执行异步或开销较大的操作)的情况可以使用 watch。使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

十一、谈谈对 keep-alive 的了解?

keep-alive 是 Vue 内置的一个组件,如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;

  • 提供 includeexclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;

  • keep-alive 拥有两个独有的生命周期钩子函数 activateddeactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated

十二、组件中 data 为什么是一个函数?

为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?

// data
data() {
  return {
	message: "子组件",
	childName:this.name
  }
}

// new Vue
new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: {App}
})
  • 组件复用时所有组件实例都会共享 data,如果 data 是对象的话,那么这样作用域没有隔离,就会造成一个组件修改 data 以后会影响到其他所有组件,所以如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;

  • 当我们使用 new Vue() 的方式的时候,无论我们将 data 设置为对象还是函数都是可以的,因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了。

十三、Vue 中的 key 有什么作用?

key 是为 Vuevnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。

Vuediff 过程可以概括为:oldChnewCh 各有两个头尾的变量 oldStartIndexoldEndIndexnewStartIndexnewEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldChnewCh 至少有一个已经遍历完了,就会结束比较。

更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

十四、Vue.nextTick实现原理

在DOM更新完毕之后执行一个回调,用法如下:

watch: {
  playing(newPlaying) {
    const audio = this.$refs.audio
    //DOM 还没更新
    this.$nextTick(() => {
      // DOM 更新了:再进行歌曲的操作
      newPlaying ? audio.play() : audio.pause()
    })
  }
}

nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。

nextTick的实现比较简单,执行的目的是在microtask或者task中推入一个function,在当前栈执行完毕(也许还会有一些排在前面的需要执行的任务)以后执行nextTick传入的function

一共有PromiseMutationObserver(H5)以及setTimeout三种尝试得到timerFunc( 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用)的方法。
优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。 如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。

总结:vue的nextTick方法的实现原理:

  • vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
  • microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  • 因为兼容性问题,vue不得不做microtaskmacrotask的降级方案

源码解读:
Vue.js异步更新DOM策略及nextTick

参考文章

30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)
0 到 1 掌握:Vue 核心之数据双向绑定

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值