基础知识点(一)
原始类型
原始类型有哪几种?null 是对象嘛?
在 JS 中,存在着 6 种原始值,分别是:
- number
- boolean
- string
- null
- undefined
- symbol
首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString()。
虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型消息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
Vue 框架
MVVM
什么是 MVVM?比之 MVC 有什么区别?
首先申明一点,不管是 React 还是 Vue,它们都不是 MVVM 框架,只有借鉴 MVVM 的思路。
接下来先说下 View 和 Model:
- View 很简单,就是用户看到的试图
- Model 同样很简单,一般就是本地数据和数据库中的数据
基本上,我们写的产品就是通过接口从数据库中读取数据,然后将数据经过处理展现到用户看到的试图上。当然我们还可以从视图上读取用户的输入,然后又将用户的输入通过接口写入到数据库中。但是,如何将数据展示到试图上,然后又如何将用户的输入写入到数据中,不同的就产生了不同的看法,从此出现了很多架构设计。
传统的 MVC 架构通常是使用控制器更新模板,视图从模型中获取数据去渲染。当用户有输入时,会通过控制器去更新模板,并且通知视图进行更新。
但是 MVC 有一个巨大的缺陷就是控制器承担的责任太大了,随着项目愈加复杂,控制器中的代码会越来越臃肿,导致出现不利于维护的情况。
在 MVVM 架构中,引入了 ViewModel 的概念。ViewModel 只关心数据的业务的处理,不关心 View 如何处理数据,在这种情况系下,View 和 model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在 ViewModel 中,让多个 View 复用这个 ViewModel。
以 Vue 框架来举例,ViewModel 就是组件的实例。View 就是模板,Model 的话在引入 Vuex 的情况下是完全可以和组件分离的。
除了以上三个部分,其实在 MVVM 中还引入了一个隐式的 Binder 层,实现了 View 和 ViewModel 的绑定。
同样以 Vue 框架来举例,这个隐式的 Binder 层就是 Vue 通过解析模板中的插值和指令从而实现 View 与 ViewModel 的绑定。
对于 MVVM 来说,其实最重要的并不是通过双向绑定或者其他的方式将 View 与 VieModel 绑定起来, 而是通过 ViewModel 将视图中的状态和用户的行为分离出一个抽象,这才是 MVVM 的精髓。
Virtual DOM
什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
为什么操作 DOM 性能很差?因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之前的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
首先 DOM 是一个多叉树的结构,如果需要完整的对比两棵树的差异,那么需要的时间复杂度会是 O(n^3),这个复杂度肯定是不能接受的,于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。所以判断差异的算法就分为了两步:
- 首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异。
- 一旦节点会有子元素,就去判断子元素是否有不同。
在第一步算法中我们需要判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。
在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动。
当然了 Virtual DOM 提高性能是其中一个优势,其实最大的优势还是在于:
- 1、将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发。
- 2、同样的,通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等等。
- 3、实现组件的高度抽象化。
路由原理
前端路由原理?两种实现方式有什么区别?
前端路由实现起来其实很简单,本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新页面。目前前端使用的路由就只有两种实现方式
- Hash 模式
- History 模式
Hash 模式
www.test.com/#/ 就是 Hash URL,当 #后面的哈希值发生变化时,可以通过 hashchange 时间来监听到 URL 的变化,从而进行跳转页面,并且无论哈希值如何为变化,服务器接收到 URL 请求永远是 www.test.com。
window.addEventListener('hashchange', () => {// ... 具体逻辑})
Hash 模式相对来说更简单,并且兼容性也更好。
History 模式
History 模式是 HTML5 新推出的功能,主要使用 history.pushState 和 history.replaceState 改变 URL。
通过 History 模式改变 URL 同样不会引起页面的刷新,只会更新浏览器的历史记录。
// 新增历史记录
history.pushState(stateObject, title, URL)
// 替换当前历史记录
history.replaceState(stateObject, title, URL)
当用户做出浏览器动作时,比如点击后退按钮时会触发 popState 事件
window.addEventListener('popstate', e => {// e.state 就是 pushState(stateObject) 中的 stateObject
console.log(e.state)
})
两种模式对比
- Hash 模式只可以更改 #后面的内容,History 模式可以通过 API 设置任意的同源 URL
- History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串
- Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候。
Vue 基础知识点
生命周期钩子函数
在 beforeCreate 钩子函数调用的时候,是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中。
然后会执行 created 钩子函数,在这一步的时候已经访问之前不能访问到的数据,但是这时候组件还没被挂载,所以是看不到的。
接下来会先执行 beforeMount 钩子函数,开始创建 VDOM,最后执行 mounted 钩子,并将 VDOM 渲染为真实 DOM 并且渲染数据。组件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。
接下来是数据更新时会调用的钩子函数 beforeUpdate 和 updated,这两个钩子函数,分别在数据更新前和更新后会调用。
另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。
最后就是销毁组件的钩子函数 beforeDestroy 和 destroyed。前者适合移除事件、定时器等等,否则可能会引起内存泄露的问题。然后进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,所有子组件都销毁完毕后才会执行根组件的 destroyed 钩子函数。
组件通信
组件通信一般分为以下几种情况:
- 父子组件通信
- 兄弟组件通信
- 跨多层级组件通信
- 任意组件
父子通信
父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现方法。
这种父子通信方式也就是典型的单项数据流,父组件通过 props 传递数据,子组件不能直接修改 props,而是必须通过发送事件的方式告知父组件修改数据。
另外这两种方式还可以使用语法糖 v -model 来直接实现,因为 v -model 默认会解析成名为 value 的 prop 和名为 input 的事件。这种语法糖的方式是典型的双向绑定,常用于 UI 控件上,但是究其根本,还是通过事件的方法让父组件修改数据。
当然我们还可以通过访问 $parent 或者 $children 对象来访问组件实例中的方法和数据。
另外如果你使用 Vue 2.3 及以上版本的话还可以使用 $listeners 和.sync 这两个属性。
$listeners 属性会将父组件中的(不含.native 修饰器的)v-on 事件监听器传递给子组件,子组件可以通过访问 $listeners 来自定义监听器。
.sync 属性是个语法糖,可以很简单的实现子组件与父组件通信
<!-- 父组件中 -->
<input :value.sync="value">
<!-- 以上写法等同于 -->
<input :value="value" @update:value="v => value = v"></comp>
<!-- 子组件中 -->
<script>
this.$emit('update:value', 1)
</script>
兄弟组件通信
对于这种情况可以通过查找父组件中的子组件实现,也就是 this.$parent.$children,在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信。
跨多层次组件通信
对于这种情况可以使用 Vue 2.2 新增的 API provide/inject,虽然文档中不推荐直接使用在业务中,但是如果用得好的话还是很有用的。
假设有父组件 A,然后有一个跨多层级的子组件 B
// 父组件 A
export default {
provide: {data: 1}
}
// 子组件
export default {inject: ['data'],
mounted() {
// 无论跨几层都能获得父组件的 data 属性
console.log(this.data) // => 1
}
}
任意组件
这种方式可以通过 Vuex 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况。
extend 能做什么
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。
mixin 和 mixins 区别
mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
Vue.mixin({beforeCreate() {
// ... 逻辑
// 这种方式会影响到每个组件的 beforeCreate 钩子函数
}
})
虽然文档不建议我们在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。
mixins 应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。
另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
computed 和 watch 区别
computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
所以一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,对于坚挺到值的变化需要做一些复业务逻辑的情况可以使用 watch。
另外 computed 和 watch 还都支持对象的写法。
vm.$watch('obj', {
// 深度遍历
deep: true,
// 立即触发
immediate: true,
// 执行的函数
handler: function(val, oldVal) {}})
var vm = new Vue({data: { a: 1},
computed: {
aPlus: {
// this.aPlus 时触发
get: fucntion() {return this.a + 1},
// this.aPlus = 1 时触发
set: function(v) {this.a = v - 1}
}
}
})
keep-alive 组件有什么作用
如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组价包裹需要保存的组件。
对于 keep-alive 组件来说,它拥有两个独有的生命周期钩子函数,分别微 activated 和 deactivated。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。
v-show 与 v -if 区别
v-show 只是在 display:none 和 display:block 之间切换。
v-if,当属性初始为 false 时,组件就不会被渲染,直到条件为 true。
总的来说,v-show 在初始渲染时有更高的开销,但是切换开销很小,更适用于频繁切换的场景;v-if 在切换时开销更高,更适合不经常切换的场景。
组件中 data 什么时候可以使用对象
组件复用时所有组件实例都会共享 data,如果 data 是对象的话,就会造成一个组件修改 data 以后会影响到其他所有组件,所以需要将 data 写成函数,每次用到就调用一次函数获得新的数据。
当我们使用 new Vuw() 的方式的时候,无论我们将 data 设置为对象还是函数都是可以的,因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了。