基础知识点(一)
原始类型
原始类型有哪几种?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的情况了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。