Vue重点知识总结—vue-router

2021
12/03

1.vue-router导航守卫(生命周期钩子)

导航守卫主要⽤来对路由的跳转进⾏监控,控制它的跳转或取消,路由守卫有全局的, 单个路由独享的, 或者组件级的。

导航钩⼦有3个参数

  • to:即将要进⼊的⽬标路由对象;
  • from:当前导航即将要离开的路由对象;
  • next:调⽤该⽅法后,才能进⼊下⼀个钩⼦函数(afterEach)。

具体有哪些钩子

  1. 全局前置守卫:router.beforeEach
  2. 全局解析守卫:router.beforeResolve
  3. 全局后置钩子:router.afterEach
  4. 路由独享钩子:在路由配置中添加beforeEnter钩子
  5. 组件内钩子:beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

简单理解 beforeEach

每次通过跳转路由时,都会触发beforeEach这个钩⼦函数,这个回调函数共有三个参数,to,from,next这三个参数,to表⽰我要跳转的⽬标路由对应的参数,from表⽰来⾃那个路由,就是当前导航即将要离开的路由对象,next是⼀个回调函数,⼀定要调⽤next⽅法来resolve这个钩⼦函数,否则无法进⼊下⼀个钩⼦函数,也无法加载路由。

详见文档

2.导航守卫解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

3.路由懒加载如何实现,原理是什么?

路由懒加载需要导入异步组件,最常用的是通过import()来实现它。

function load(component) {
    return () => import(`views/${component}`)
}
// 箭头函数
const asyncPage = () => import('./views/home.vue')
// import()函数需要作为返回值,其返回Promise

编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由时(访问该路由时)才会去按需加载对应的路由组件js文件。可以加快项⽬的加载速度。

在Vue3中,在组件中使用异步组件时需要使用defineAsyncComponent()函数才能实现懒加载,但在vue-router中同样可以使用import()方法

这时由于在 Vue 3 中,函数组件被定义为纯函数,异步组件定义需要通过将其包装在一个新的 defineAsyncComponent helper 中来显式定义。

import { defineAsyncComponent } from 'vue'
const home = defineAsyncComponent(() => import('@/views/home.vue'))
export default {
  name: 'async-components',
  components:{
    home
  }
};

4.vue-router如何做用户登录权限?

第一次打开页面或刷新页面时候可以在路由前置钩子router.beforeEach中查询用户信息。再根据返回的用户信息和to,from参数判断登录权限(这里要把用户的信息和登录状态做一个缓存),然后使用next回调控制路由跳转方向。具体的代码要根据具体的项目进行实施。

不是第一次打开页面情况,跳转时直接判断缓存中的登录状态和用户信息来判断是否拥有权限。若有权限则继续跳转到指定的路由。

5. vue-router 3.1.0 <router-link>新增的v-slot属性怎么⽤?

router-link 通过⼀个作⽤域插槽暴露底层的定制能⼒。方便我们更自由的定制导航链接的形式。记得把 custom 配置传递给 <router-link>,以防止它将内容包裹在 <a> 元素内。

详见文档

6. <router-view> 的 v-slot

详见文档

7. 如何实现⼀个路径渲染多个组件?

可以通过命名视图(router-view),它容许同⼀界⾯中拥有多个单独命名的视图,⽽不是只有⼀个单独的出⼝。如果router-view 没有设置名字,那么默认为default

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。在配置routes时,确保正确使用 components 配置 (带上 s):

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar 的缩写
        LeftSidebar,
        // 它们与 `<router-view>` 上的 `name` 属性匹配
        RightSidebar,
      },
    },
  ],
})

详见文档

8. 如何实现多个路径共享⼀个组件?

只需将多个路径的component字段的值设置为同⼀个组件即可。

9. 如何监测动态路由的变化

可以通过watch⽅法来对$route进⾏监听,或者通过导航守卫的钩⼦函数beforeRouteUpdate来监听它的变化。

在Vue3中可使用useRoute()onBeforeRouteUpdate

10. Vue 如何去除url中的 #

哈希字符(#)部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。

如果你担心这个问题,可以使用 HTML5 模式,将路由模式改为history

路由切换时本质是向history栈中添加一个路由,也就是添加一个history记录。

vue2(vue-router 3.x,已经弃用):

const router = new VueRouter({
  mode: 'history', // 也可设为 hash
  routes: [...]
})

vue3(vue-router 4.x):

import { createRouter, createWebHashHistory } from 'vue-router'

// hash 模式是用 createWebHashHistory() 创建的:
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})

// 用 createWebHistory() 创建 HTML5 模式,推荐使用这个模式:
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
})

11. $route 和 $router 的区别

$route是用来获取路由信息的:

它是当前路由信息的⼀个对象,⾥⾯包含路由的⼀些基本属性,包括name(路由名称)、meta(配置的路由元信息)、path(当前路径)、hash(hash参数)、query(查询参数)、params(当前传递参数)、fullPath(当前全路径)、matched(匹配项)等等。

每一个路由都会有一个$route对象,是一个局部的对象。

$router是用来操作路由的:

$router是VueRouter的一个实例,他包含了所有的路由,以及路由的跳转方法,钩子函数等,也包含一些子对象(例如history)。

12. Vue-router 使⽤params与query传参有什么区别

⽤法上

query要⽤path来引⼊,params要⽤name来引⼊,接收参数都是类似的,分别是this.$route.query.namethis.$route.params.name

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// `params` 不能与 `path` 一起使用,这时,params无法传递
router.push({ path: '/user', params: { username } }) // -> /user

// 占位符传参时,params参数对应的字段会被拼接到path中
// 使用 `name` 和 `params` 从自动 URL 编码中获益
const routes = [
  { name: 'user',path: '/user/:username' },
]
router.push({ name: 'user', params: { username } }) // -> /user/eduardo

展⽰上

  • query更加类似于我们ajax中get传参,params则类似于post,说的再简单⼀点,前者在浏览器地址栏中显⽰参数,后者则不显⽰
  • params是路由的⼀部分,必须要有,组件才能正常执行。query是拼接在url后⾯的参数,若没有也可正常执行组件。
  • query不设置也可以通过浏览器url进行传参,params不设置的时候,刷新页⾯或者返回参数会丢失
  • 刷新页面后,通过params传参会出现参数丢失的情况,可以通过query的传参⽅式或者在路由匹配规则加⼊占位符即可以解决参数丢失的情况。

13. Vue路由实现的底层原理

Vue的路由实现:hash 模式和 history 模式

hash模式:

早期前端路由的实现是基于window.location.hash 来实现的,window.location.hash 的值就是 URL中#后面的内容。

特点:hash虽然在URL中,但不被包括在HTTP请求中;只是用来指导浏览器动作,对服务端无作用,且hash的切换不会重加载页面。

hash 模式下,仅 # 号之前的内容会被包含在请求中,因此对于后端来说,即使路由切换错误,也不会返回 404 。

但是 hash 模式有两个缺点:

  1. url 不太美观
  2. 对SEO不太友好

history模式:

history采用HTML5的新特性;且提供了两个新方法:history.pushState()history.repalceState()可以对浏览器历史记录栈内存进行修改,且页面不会重载。

history.pushState方法接受三个参数,依次为:

  • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
  • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

这样地址栏就可以看到我们自己存的标识了。历史记录也就存进去了。

每当激活的历史记录发生变化时,都会触发popstate事件,调用history.pushState()或者history.replaceState()不会触发popstate事件。pushState事件只会在其他浏览器操作时触发, 比如点击后退按钮(或者在JavaScript中调用history.back()history.go()方法)。

window.addEventListener('popstate', function(event) {
  //做一些操作
  console.log(event.state) // history.pushState()中传入的state状态对象
});

缺点:若没有对服务器(如nginx)进行配置,刷新本不存在的(pushState新增的历史记录)url页面时会出现404状况。

SPA单页面应用和SSR服务端渲染对比

2021
10/03

1.什么是单页面应用(SPA)?

整个web项目只有一个页面,使用路由机制进行组件之间的切换。

优点:客户端渲染、数据传输量小、减少服务器端压力、交互/响应速度快、前后端完全分离。

缺点:首屏加载慢、对SEO不友好,不利于搜索引擎收录和排名。

2.什么是服务端渲染(SSR)?

将组件或页面通过服务器端生成HTML字符串,再将整体页面数据发送到浏览器端渲染。

优点:对于SEO友好、首屏加载速度快。

缺点:页面重复加载次数高、开发效率低、数据传输量大、服务器压力大。

3.SPA、SSR分别适合什么样的应用场景?

SPA:对项目性能要求高、页面加载速度快、要求客户端渲染、对SEO要求低。

SSR:对项目SEO要求高、首次打开响应速度快。

4.SPA与SSR本质区别是什么?

1.传输数据不同:

SPA与服务器通过API接口拿到局部数据(json对象),然后由客户端拼接为html进行渲染。

SSR直接请求页面拿到整个页面的html进行渲染。

2.SEO优化问题:

SPA在客户端源代码中无法看到动态渲染的html,无法被爬虫爬到。而SSR可以在源代码看到整个页面所有html数据。

js事件循环机制总结

2021
10/03

js是一门单线程语言,但却能优雅地处理异步程序,在于js的事件循环机制。

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 、构造 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

js执行线程:

  1. 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
  2. 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求、定时器等异步事件。

任务队列( Event Queue )

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中立即执行;而异步任务会通过任务队列的机制(先进先出的机制)来进行协调。如图:

事件循环:主线程内的任务先执行完毕,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

宏任务和微任务:任务包含两种,宏任务(Macro Task)和微任务(Micro Task)。微任务要优先于宏任务执行。故主线程任务执行完毕后会先读取任务队列中的微任务,将微任务按照先进先出的原则全部执行且清空后,再去读取任务队列中的宏任务。。。若没有宏任务,则进行下一次事件循环(Next Tick)。在事件循环中,每进行一次循环操作称为tick。(如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。)

宏任务主要包含:script( 整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、setImmediate(IE、Node.js )

微任务主要包含PromiseMutationObserver(变动观察器)是监视DOM变动的接口)、process.nextTick(Node.js )

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

//输出的顺序是:script start, script end, promise1, promise2, setTimeout

//-------------------------------------------

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

//注意new Promise并不是异步,只是一个实例化对象,期回调先被执行。
//setTimeout遵循先进先出执行
//输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

前端热门插件和工具使用

2021
08/02

Webpack

1.webpack的核心概念

  • Entry:入口,Webpack进行打包的起始点(文件)
  • Output:出口,webpack编译打包生成的bundle(打包文件)
  • Loader:模块加载(转换)器,将非js、非json模块包装成webpack能理解的js模块
  • Plugin:插件,在 Webpack 构建流程中的特定时机插入具有特定功能的代码
  • Module:模块,在 Webpack眼里一切皆模块,默认只识别js文件, 如果是其它类型文件利用对应的loader转换为js模块

2.webpack模块化打包的基本流程

  1. 连接: webpack从入口JS开始, 递归查找出所有相关联的模块, 并【连接】起来形成一个图(网)的结构
  2. 编译: 将JS模块中的模块化语法【编译】为浏览器可以直接运行的模块语法(当然其它类型资源也会处理)
  3. 合并: 将所有编译过的模块【合并】成一个或少量的几个bundle文件, 浏览器真正运行是打包生成的bundle文件

3.loader与plugin区别

  • loader: 用于加载编译特定类型的资源文件, webpack本身只能打包js。loader即为文件加载器,操作的是文件,将文件A通过loader转换成文件B,是一个单纯的文件转化过程。
  • plugin: 是一个扩展器,丰富webpack本身,增强功能 ,针对的是在loader结束之后,webpack打包的整个过程,他并不直接操作文件,而是基于事件机制工作,监听webpack打包过程中的某些节点,执行广泛的任务。 如果loader处理不了的资源可以交给插件处理。

4.常用的loader和plugin有哪些

常用的loader:babel-loader、css-loader、css-loader、eslint-loader、file-loader、url-loader、

常用的plugin:clean-webpack-plugin、copy-webpack-plugin、html-webpack-plugin、css分离和压缩插件

5.区别live-reload(自动刷新)与hot-realod/HMR(热更新)

相同点:代码修改后都会自动重新编译打包

不同点:

  • live-reload: 刷新整体页面,从而查看到最新代码的效果, 页面状态全部都是新的。
  • Hot-reload: 没有刷新整个页面,只是加载了修改模块的打包文件并运行,局部更新, 整个界面的其它部分的状态还在。

Echarts

1.Echarts常用方法

  • echarts.init(el) 初始化echarts实例
  • myChart.setOption(option) 通过配置项生成图表

Axios

1.Axios 是什么

Axios 是一个基于 promise 的 HTTP 请求库,可以用在浏览器和 node.js 中(通过判断XMLHttpRequest和process这两个全局变量来判断程序的运行环境的)。

2.Axios 特点

  1. 基于 promise 的异步 ajax 请求库,支持promise所有的API
  2. 浏览器端/node 端都可以使用,浏览器中创建XHR(XMLHttpRequests),在node中二次封装了http模块
  3. 支持请求拦截器/响应拦截器
  4. 可以转换请求数据和响应数据,并对响应内容自动转换成 JSON类型的数据
  5. 安全性更高,客户端支持防御 XSRF

3.请求和响应拦截使用及作用

使用Axios.interceptors.request.use(config=>{return config},err=>{})Axios.interceptors.response.use(response=>{return response.data},err=>{}) 做请求和响应拦截。

作用:请求拦截可以做请求验证、token添加、cookie添加,请求头配置、loading等。响应拦截可以做数据处理、错误提示等。

CSS常见技巧汇总

2021
14/01

1.纯CSS绘制三角形

/* 正三角 */
.up-triangle {
   width: 0;
   height: 0;
   border-style: solid;
   border-width: 0 25px 40px 25px;
   border-color: transparent transparent rgb(245, 129, 127) transparent;
 }
 
 /* 倒三角 */
 .down-triangle {
   width: 0;
   height: 0;
   border-style: solid;
   border-width: 40px 25px 0 25px;
   border-color:  rgb(245, 129, 127) transparent transparent transparent;
 }
 div:last-child {
   margin-top: 1rem;
 }

2.设置input 的placeholder的字体样式

input::-webkit-input-placeholder {    /* Chrome/Opera/Safari */
    color: red;
}
input::-moz-placeholder { /* Firefox 19+ */  
    color: red;
}
input:-ms-input-placeholder { /* IE 10+ */
    color: red;
}
input:-moz-placeholder { /* Firefox 18- */
    color: red;
}

3.设置input聚焦时的样式

input:focus {   
  background-color: red;
}

4.单行和多行文本超出省略号

// 单行文本出现省略号
width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
 
// 多行文本出现省略号
display: -webkit-box; /*重点,不能用block等其他,将对象作为弹性伸缩盒子模型显示*/
-webkit-box-orient: vertical; /*从上到下垂直排列子元素(设置伸缩盒子的子元素排列方式)*/
-webkit-line-clamp: 3; /*行数,超出三行隐藏且多余的用省略号表示...*/
line-clamp: 3;
word-break: break-all;
overflow: hidden;
max-width: 100%;

5.相邻兄弟选择器之常用场景

ul{
  width: 500px;
   margin:auto;
   list-style: none;
   padding:0;
   border:1px solid red;
   text-align: center;
 }
 li+li{
   border-top:1px solid red;
 }

6.隐藏滚动条或更改滚动条样式

<style>
.scroll-container {
   width: 500px;
   height: 150px;
   border: 1px solid #ddd;
   padding: 15px;
   overflow: auto;     /*必须*/
 }
 
 .scroll-container::-webkit-scrollbar {
   width: 8px;
   background: white;
 }
 
 .scroll-container::-webkit-scrollbar-corner,
   /* 滚动条角落 */
 .scroll-container::-webkit-scrollbar-thumb,
 .scroll-container::-webkit-scrollbar-track {      /*滚动条的轨道*/
   border-radius: 4px;
 }
 
 .scroll-container::-webkit-scrollbar-corner,
 .scroll-container::-webkit-scrollbar-track {
   /* 滚动条轨道 */
   background-color: rgba(180, 160, 120, 0.1);
   box-shadow: inset 0 0 1px rgba(180, 160, 120, 0.5);
 }
 
 .scroll-container::-webkit-scrollbar-thumb {
   /* 滚动条手柄 */
   background-color: #00adb5;
 }
</style>
<p class="scroll-container">
        庭院深深,不知有多深?杨柳依依,飞扬起片片烟雾,一重重帘幕不知有多少层。豪华的车马停在贵族公子寻欢作乐的地方,她登楼向远处望去,却看不见那通向章台的大路。春已至暮,三月的雨伴随着狂风大作,再是重门将黄昏景色掩闭,也无法留住春意。泪眼汪汪问落花可知道我的心意,落花默默不语...
</p>

7.表格常用样式(边框合并等)

table {
	border-collapse: collapse;	/* 边框合并属性 */
	margin: 0 auto;
	text-align: center;
}
table td,
table th {
	border: 1px solid #cad9ea;
	color: #666;
	height: 30px;
}
table thead th {
	background-color: #CCE8EB;
	width: 100px;
}
table tr:nth-child(odd) {
	background: #fff;
}
table tr:nth-child(even) {
	background: #F5FAFA;
}

8. 纯CSS制作显隐菜单

使用checkbox选择框和label按钮可以为显隐菜单提供按钮
选定此按钮会出现伪类checked
利用此机制可以使用纯HTML和CSS相邻选择器制作一些点击效果如点击弹出下拉菜单
可以给label添加背景图标 (把checkbox隐藏)

9.自定义字体

@font-face{
    font-family: '字体名称随便起';
    src: url('../font/字体名称.eot');
    src:url('../font/字体名称.woff') format('woff'),
    url('../font/字体名称.ttf') format('truetype'),
    url('../font/字体名称.svg') format('svg');
}

10.媒体查询

/* 直接引文件 */
<link rel="stylesheet" media="screen and (max-width:1220px)" href="media/max1220.css">
/* 样式 */
@media screen and (max-width: 300px) {
    body {
        background-color:lightblue;
    }
}

11.css传参

:root {  /* 任意父元素 */
	--rad-0: 0%;
	--rad-50: 50%;
	--rad-100: 100%;
}
/* 参数字段用--开头,使用var()调用 */
.round {
        /* 子元素 */
	border-radius: var(--rad-50);
}

JAVA二分查找算法

2020
21/10
/**
 * @desc 二分查询(非递归方式)
 * 案例:
 * {1,3,8,10,11,67,100},编程实现二分查找,要求使用非递归方式完成。
 */
public class BinarySearchNonRecursive {
    public static void main(String[] args) {
        int[] arr = {1, 3, 8, 10, 11, 67, 100};
        int index = binarySearch(arr, 1);
        if (index != -1) {
            System.out.println("找到了,下标为:" + index);
        } else {
            System.out.println("没有找到--");
        }
    }
    private static int binarySearch(int[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] > target) {
                right = mid - 1; // 向左找
            } else {
                left = mid + 1; // 向右找
            }
        }
        return -1;
    }
}

Vue重点知识总结—原理篇

2020
19/07

一、响应式数据原理

什么是双向数据绑定:Vue将视图层与数据层做了一个双向的数据同步。甚至是组件之间也可以通过v-model方式进行数据绑定。最大的作用是方便。更效率的实现业务。

vue 实现数据双向绑定主要是:采用数据劫持结合发布-订阅者模式的方式,通过 Object.defineProperty() 来劫持data中对象的各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应回调。每个组件实例都有相应的 watcher (观察者)程序实例,当页面取相应的属性时,会进行依赖收集(收集组件的watcher),之后当依赖项属性的 setter 被调用时,会通知 watcher 重新计算,从而致使组件得以更新。

Object.defineProperty() 的问题主要有三个:不能监听数组的变化(因为数组可能项目过多,遍历数组太吃性能。需要重写数组的一些方法来实现数组的劫持)、必须遍历对象的每个属性、必须深层遍历嵌套的对象。

Vue在监听数组变化时用的不是Object.defineProperty(),而是把数组的原型方法进行了重写。当用户使用数组的一些方法操作数组时,走的就是VUE自己的方法,然后通知依赖更新。如果数组中包含引用类型如对象,会对其再次进行监控。Vue改写了数组的7个方法(push,pop,shift,unshift,splice,sort,reverse),因为只有这7个方法才能改变数组。另外通过引索改变数组是无法监听的,只能使用$set(arr,index,value)来更新。

在新版Vue3中使用Proxy代理取代Object.defineProperty()来劫持,可以监听整个数据的变化,无需进行层级递归。

整理如下:

  1. 观察者Observer(监听器):⾸先通过观察者对data中的属性使⽤object.defineproperty劫持数据的getter和setter,通知订阅者,触发他的update⽅法,对视图进⾏更新
  2. 解析器Compile:⽤来扫描和解析每个节点的相关指令,并替换模板数据,初始化视图,初始化相应的订阅器
  3. 订阅者Watcher:订阅者接到通知后,调⽤对应的更新函数update⽅法更新对应的视图
  4. 订阅器Dep:订阅者可能有多个,因此需要订阅器Dep来专门接收这些订阅者,并统⼀管理

但在vue3中抛弃了object.defineproperty⽅法,因为

  1. Object.defineproperty⽆法监测对象属性的添加和删除、数组索引和长度的变更,因此vue重写了数组的push/pop/shift/unshift/splice/sort/reverse⽅法
  2. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进⾏遍历,这样很消耗性能

Vue3中实现数据双向绑定的原理是数据代理,使⽤proxy实现。Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

二、Vue响应式原理整体叙述(用自己的话)

  1. 通过数据劫持和发布订阅模式来实现。
  2. 初始化时会调用initData,并通过Observer方法对数据进行观测(这里会判断数据是否已被观测,观测时分对象和数组两种观测方法),如果是对象,对象会进行遍历和递归,然后利用Object.defindProperty()方法重新定义了data对象中所有的属性的get和set方法(这个过程如下图)。
  3. 当访问数据时触发对应的get,这时会通过dep.depend()进行依赖收集,收集当前的watcher(观察者,一个组件创建一个watcher,watcher实例与组件实例一一对应)
  4. 修改数据时触发对应的set。如果新的值与旧值不一样,这时会触发dep.notify()回调,通知数据更新。
  5. 由于一个watcher中可能包含有很多个观测的数据,当某个数据更新时怎样找到变化的地方而实现局部响应式更新,这时就需要虚拟DOM和diff算法的支持。
  6. 虚拟DOM是将真实DOM用对象的形式表示,与真实DOM是映射关系。数据变化会修改虚拟DOM对应的节点。然后再通过diff算法,比较新旧虚拟DOM的差异,从而通过打补丁的方式修改真实DOM。(在源码中,patchVnode是diff发生的地方,新旧节点对比方式为:深度优先(递归先找孩子,有孩子先比孩子,没有再比较同层),同层比较。)

三、Vue模板编译原理(template->v-dom->dom)

template –>parse(template) –> ast树(抽象语法树,vue中使用jquery之父的一个作品) –>gencode(ast,options) –>优化树 –> 生成 render 函数 –> 执行 render 函数生成 VNode(虚拟DOM)–> 通过虚拟DOM创建真实DOM

1)什么是模板编译?

把写在<template></template>标签中的类似于原生HTML的内容称之为模板。

在<template></template>标签中除了写一些原生HTML的标签,我们还会写一些变量插值,或者写一些Vue指令,这些东西都是在原生HTML语法中不存在的,不被接受的。render函数会将模板内容生成对应的VNode,VNode经过patch过程从而得到将要渲染的视图中的VNode,最后根据VNode创建真实的DOM节点并插入到视图中, 最终完成视图的渲染更新——模板编译过程。

2)整体渲染过程

在这里插入图片描述

3)模板编译内部流程

借助抽象语法树解析<template></template>标签中写的模板。

4)抽象语法树AST

抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。

将一堆字符串模板解析成抽象语法树AST后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST来生成render函数。

  1. 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
  2. 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
  3. 代码生成阶段:将AST转换成渲染函数;

四、nextTick实现原理

nextTick主要是利用js事件循环机制定义了一个异步方法。在这个异步方法中包含宏任务微任务(主线程先执行,异步线程(工作线程)在主线程执行结束后再将其推入主线程中,异步任务中的微任务会优先于宏任务先执行)。多次调用nextTick会将这些任务存入任务队列,在主线程任务执行完毕后会读取任务队列中的任务进行清空。

用户手动调用的nextTick(cb)也会将cb回调存入任务数组推入任务队列,先进先出原则进行执行。

Vue中的nextTick实现所用的方法:

Vue的nextTick将任务数组推入任务队列所使用的方法是微任务:PromiseMutationObserver;若这些方法不支持,会使用宏任务setImmediate(IE,优先于setTimeout)、setTimeout方法。

原理:

  • 先定义了一个 callbacks 存放所有的 nextTick 里的回调函数
  • 然后判断当前环境是否支持 Promise,如果支持,就用 Promise 来触发回调函数
  • 如果不支持 Promise 就判断是否支持 MutationObserver,通过观察文本节点发生变化,去触发执行所有异步回调函数
  • 如果不支持 MutationObserver 就判断是否支持 setImmediate,如果支持,就通过setImmediate 来触发回调函数
  • 如果以上都不支持就只能用 setTimeout 来完成异步执行

延迟调用优先级如下:

Promise > MutationObserver > setImmediate > setTimeout

$nextTick可以在vue的created生命周期中使用,在回调中可以用$refs获取dom元素。

nextTick作用:

在修改数据之后使⽤nextTick⽤于下次Dom更新循环结束之后执⾏延迟回调。在修改数据之后使⽤nextTick,则可以在回调中获取更新后的DOM。

五、插槽slot原理

插槽,可以分为两种,一种是普通插槽,一种是作用域插槽。普通插槽,又分为默认插槽和具名插槽。具名插槽可以将不同元素插入到不同slot占位符。

作用域插槽,就是使用子作用域数据(子组件属性数据)的插槽。

普通插槽原理:

  1. 父组件先解析,把插槽元素或字符串当做子元素处理,生成含有children的v-dom节点。
  2. 子组件解析,slot 作为一个占位符,会被解析成一个_xxx('default')函数。这个_xxx函数,传入 'default ' 参数并执行。如果给了名字,就传入插槽的名字。
  3. 这个函数的作用,是把第一步解析得到的插槽节点拿到,然后返回,那么子组件的节点就完整了,插槽也成功认了爹。

作用域插槽原理:

  1. 父子组件在生成v-dom节点时,父组件解析成一个含有props参数的函数,直接传入到children中未执行。
  2. 子组件slot占位符同样解析成一个函数,参数除了插槽名称,还有props数据对象。
  3. slot函数在执行时,通过插槽名称找到对应父组件的函数,并执行,将props传入,这样最终返回的就是含有作用域数据的节点

大概原理就是这样,当然在实际源码中,要复杂很多。

六、缓存组件keep-alive原理

用法:

  1. 用于Vue性能优化。缓存组件。
  2. 频繁切换,不需要重复渲染。
  3. keep-alive有include和exclude属性,这两个属性决定了哪些组件可以进入缓存。
  4. keep-alive还有一个max属性,通过它可以设置最大缓存数,当缓存的实例超过max的时候,vue会删除最久没有使用的缓存,属于LRU缓存策略。
  5. keep-alive其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated和deactivated,它们分别在组件激活和失活的时候触发。

可以利用keep-alive提供的include和exclude指定缓存哪些组件不缓存哪些组件,然后配合vuex等状态管理工具实现动态控制。

原理:

  • 维护了一个key数组和一个缓存对象,这个key数组记录目前缓存的组件的key值,
  • 如果这个组件没有指定key值,会自动生成一个唯一的key值
  • 缓存对象会以key值为键,vnode为值,用于缓存组件对应的虚拟DOM
  • 在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有则从缓存中读取到对应的组件实例,没有就把它缓存。

七、路由组件router-view原理

router-view通过判断当前组件的嵌套层次,然后通过这个层次从route.matches数组中获取当前需要渲染的组件,最后调用全局的$createElement来创建对应的VNode完成渲染的。

八、计算属性computed原理

你给 computed 设置的 get 和 set 函数,会与 Object.defineProperty 关联起来。

所以 Vue 能监听捕捉到,读取 computed 和 赋值 computed 的操作。

在读取 computed 时,会执行设置的 get 函数,但是并没有这么简单,因为还有一层缓存的操作。如果数据没有被污染,不为脏数据(标识dirty),那将直接从缓存中取值,而不会去执行 get 函数。

赋值 computed 时,会执行所设置的 set 函数。这个就比较简单,会直接把 set 赋值给 Object.defineProperty – set。

computed缓存原理

  1. 一开始每个 computed 新建自己的 watcher时,会设置 watcher.dirty = true,以便于 computed 被使用时,会计算得到值
  2. 当依赖的数据变化了,通知 computed 时,会赋值 watcher.dirty = true,此时重新读取 computed 时,会执行 get 函数重新计算。
  3. computed 计算完成之后,会设置 watcher.dirty = false,以便于其他地方再次读取时,使用缓存,免于计算。

九、生命周期钩子原理

无非就是回调函数。在不同的节点插入不同的钩子。

十、前端路由URL跳转原理(hash和history路由

history路由:history.pushStatehistory.replaceState方法(Html5新增方法,用于在不刷新页面的情况下切换url路径)

hash路由:hash路由切换本身不会刷新页面。监听hash切换使用onhashchange事件

history.pushState(stateData,nameString,pathUrl)  //把路由页面放入历史记录
history.replaceState(stateData,nameString,pathUrl)  //不放入历史,直接切换
//stateData-传参,nameString-路由名,pathUrl-路由地址

十一、v-if、v-show指令原理

  1. v-if在内部使用with语法,使用三元运算符判断v-if的值的真假。若为真,则渲染节点。若为假,则不渲染。
  2. v-show在内部使用创建指令方式,判断值为假,则改变节点样式display:none,若为真,则使用原有样式。

十二、Vue中事件绑定原理

在template中绑定事件有两种方式。一种是原生dom绑定,如使用<button @click="fn"></button>,另一种是vue组件(添加到组件上的)自定义事件绑定<el-form @click="fn"><el-form>。两种的区别:

  1. 两者编译后的结果不同:原生结果{nativeOnOn:{click}},等价于普通元素on;自定义事件结果{on:{click}},会单独处理。
  2. 原生绑定采用addEventListener方法添加事件。而组件的自定义事件采用vm.$on()方法,使用vue的发布订阅模式绑定事件,需要使然$emit来触发事件。
  3. 在组件上添加原生事件的方法是使用native修饰符:<el-form @click.native="fn"><el-form>
  4. Vue中没有事件代理的机制。如果要在v-for渲染的多个节点上添加事件,会多次调用addEventListener方法,性能不高。可以在外层添加事件,采用代理模式区分每个子节点的事件。

十三、v-model的实现原理

v-model就是value+input事件的语法糖。

原理:会将组件的v-model转换成value+input

除input标签外,select标签等无法使用input事件的标签,其事件会使用change或自定义input

自定义组件实现v-model

Vue.component("el-checkbox",{
    template:`<input type="checkbox" :checked="check" @change="$emit("change",$event.target.checked)"/>`,
    model:{
        prop:"check",  // 默认为value,这里更改为check
        event:"change" // 默认为input,这里改为change
    },
    props:{
        check:boolean
    }
})

普通标签v-model原理:除value+input之外,还会添加指令。会根据元素的type在编译时绑定不同的属性和事件。比如checkbox会绑定checked属性和change事件。

当添加.lazy修饰符之后(v-model.lazy=""),改变input框中的内容并不会使得其他绑定的数据发生变化,当输入框失去焦点后触发change事件才会改变。

十四、vue.$set原理

强制更新。将未被劫持的数据重新劫持,然后再跑一遍Vue的响应式流程。在Vue3中已废弃。

十五、路由懒加载如何实现,原理是什么

路由懒加载需要导入异步组件,最常用的是通过import()来实现它。

function load(component) {
    return () => import(`views/${component}`)
}
// 箭头函数
const asyncPage = () => import('./views/home.vue')
// import()函数需要作为返回值,其返回Promise

编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由时(访问该路由时)才会去按需加载对应的路由组件js文件。可以加快项⽬的加载速度。

在Vue3中,在组件中使用异步组件时需要使用defineAsyncComponent()函数才能实现懒加载,但在vue-router中同样可以使用import()方法

这时由于在 Vue 3 中,函数组件被定义为纯函数,异步组件定义需要通过将其包装在一个新的 defineAsyncComponent helper 中来显式定义。

import { defineAsyncComponent } from 'vue'
const home = defineAsyncComponent(() => import('@/views/home.vue'))
export default {
  name: 'async-components',
  components:{
    home
  }
};

十六. Vue3中proxy的原理

Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

主要通过Proxy对对象进⾏绑定监听处理,通过new Map对对象的属性操作进⾏处理,将要执⾏的函数匹配到存到对应的prop上⾯,通过每次的访问触发get⽅法,进⾏订阅操作,通过修改触发set⽅法,此时通过回调函数通知订阅者,触发他的update⽅法,对视图进⾏更新,达到修改数据和视图的 。

js重点知识总结—设计模式

2020
07/06

假设有一个空房间,我们要日复一日地往里面放一些东西。最简单的办法当然是把这些东西直接扔进去,但是时间久了,就会发现很难从这个房子里找到自己想要的东西,要调整某几样东西的位置也不容易。所以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,就是一种设计模式。

设么是设计模式?找出 程序中变化的地方,并将变化封装起来。这种封装就是设计模式。它的关键是意图,而不是结构。学习设计模式,有助于写出可复用和可维护性高的程序。

1.程序设计的原则

单一职责原则(SRP)

一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

也就是说,应该把对象或方法划分成较小的粒度。

最少知识原则(LKP)

一个软件实体应当尽可能少地与其他实体发生相互作用。

应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系,可以转交给第三方进行处理。

划分模块的一个准则就是高内聚低耦合。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性(内聚性)越差( 降低耦合性,可以提高其独立性)。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改。

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定。

2.常用模式

常用的有:单例模式、工厂模式、装饰模式、发布-订阅模式、观察者模式、中介者模式等。

3.单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。核心:确保只有一个实例,并提供全局访问。

实现方法:使用闭包缓存一个局部变量,这个局部变量用来缓存仅有的实例。

function SetManager(name) {
    this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
};

var SingletonSetManager = (function() {
    var manager = null;

    return function(name) {
        if (!manager) {
            manager = new SetManager(name);
        }

        return manager;
    } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a

4.工厂模式

工厂模式是用来创建对象的一种设计模式。不暴露对象创建的逻辑,而是将逻辑封装在一个函数内,那么这个函数可以成为工厂。

工厂是用构造函数的方法来创建对象。通过使用一个共同的接口来指向新创建的对象。

工厂模式根据抽象程度的不同可以分为:1.简单工厂 2.工厂方法 3.抽象工厂

简单工厂

优点:你只需要传递一个合法的参数,就可以获取到你想要的对象,而无需知道创建的具体的细节。

缺点:但是在函数内包含了所有对象的构造函数和判断逻辑的代码, 每次如果需要添加一个对象,那么我们需要新增一个构造函数,当我们需要维护的对象不是上面这2个,而是20个或者更多,那么这个函数将会成为超级函数,使得我们难以维护。所以简单工厂模式只适用于在创建时对象数量少,以及逻辑简单的情况。

let  factory = function (role) {
    function superman() {
        this.name ='超级管理员',
        this.role = ['修改密码', '发布消息', '查看主页']
    }

    function commonMan() {
        this.name = '普通游客',
        this.role = ['查看主页']
    }

    switch(role) {
        case 'superman':
        return new superman();
        break;
        case 'man':
        return new commonMan();
        break;
        default:
        throw new Error('参数错误')
    }
}

let superman = factory('superman');
let man = factory('man');

工厂方法:

工厂方法模式本意是将实际创造的对象推迟到子类中,这样核心类就变成了抽象类。

let factory = function (role) {
    if(this instanceof factory) {
        var s = new this[role]();
        return s;
    } else {
        return new factory(role);
    }
}

factory.prototype = {
    admin: function() {
        this.name = '平台用户';
        this.role = ['登录页', '主页']

    },
    common: function() {
        this.name = '游客';
        this.role = ['登录页']
    },
    test: function() {
        this.name = '测试';
        this.role =  ['登录页', '主页', '测试页'];
        this.test = '我还有一个测试属性哦'
    }
}

let admin = new factory('admin');
let common = new factory('common');
let test = new factory('test');

如果使用时忘记加new了, 那么我们就获取不到admin,common等对象了,这就比较安全了。

5.装饰模式

定义:以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。

是一种“即用即付”的方式,能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。

核心:是为对象动态加入行为,经过多重包装,可以形成一条装饰链

实现:最简单的装饰者,就是重写对象的属性。

var A = {
    score: 10
};

A.score = '分数:' + A.score;

可以使用传统面向对象的方法来实现装饰,添加技能。

function Person() {}

Person.prototype.skill = function() {
    console.log('数学');
};

// 装饰器,还会音乐
function MusicDecorator(person) {
    this.person = person;
}

MusicDecorator.prototype.skill = function() {
    this.person.skill();
    console.log('音乐');
};

// 装饰器,还会跑步
function RunDecorator(person) {
    this.person = person;
}

RunDecorator.prototype.skill = function() {
    this.person.skill();
    console.log('跑步');
};

var person = new Person();

// 装饰一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);

person.skill(); // 数学
person1.skill(); // 数学 音乐 跑步

6.发布-订阅模式

定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

一个对象不用再显式地调用另外一个对象的某个接口而实现通知。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅。

发布订阅是一种消息范式,消息的发送者(发布者)不会直接将消息传给特定的接收者(订阅者),而是将发布的消息分为不同的类别,无需了解有哪些订阅者。同样,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在,无需了解发布者发出了哪些消息。

优势:时间上的解耦,对象之间解耦。可以用在异步编程中与MVVM框架中。

缺点:1.创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销。2.弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解。

实现:JS中的事件就是经典的发布-订阅模式的实现

// 订阅1
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

// 订阅2
document.body.addEventListener('click', function() {
    console.log('click2');
}, false);

// 发布
document.body.click(); // click1  click2

小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。

一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB。这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布。

// 观察者
var observer = {
    // 订阅集合
    subscribes: [],

    // 订阅
    subscribe: function(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = [];
        }
        
        // 收集订阅者的处理
        typeof fn === 'function' && this.subscribes[type].push(fn);
    },

    // 发布  可能会携带一些信息发布出去
    publish: function() {
        var type = [].shift.call(arguments),
            fns = this.subscribes[type];
        
        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }
        
        // 挨个处理调用
        for (var i = 0; i < fns.length; ++i) {
            fns[i].apply(this, arguments);
        }
    },
    
    // 删除订阅
    remove: function(type, fn) {
        // 删除全部
        if (typeof type === 'undefined') {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }

        if (typeof fn === 'undefined') {
            fns.length = 0;
            return;
        }

        // 挨个处理删除
        for (var i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
};

// 订阅岗位列表
function jobListForA(jobs) {
    console.log('A', jobs);
}

function jobListForB(jobs) {
    console.log('B', jobs);
}

// A订阅了笔试成绩
observer.subscribe('job', jobListForA);
// B订阅了笔试成绩
observer.subscribe('job', jobListForB);


// A订阅了笔试成绩
observer.subscribe('examinationA', function(score) {
    console.log(score);
});

// B订阅了笔试成绩
observer.subscribe('examinationB', function(score) {
    console.log(score);
});

// A订阅了面试结果
observer.subscribe('interviewA', function(result) {
    console.log(result);
});

observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '备用'); // 备用

observer.publish('job', ['前端', '后端', '测试']); // 输出A和B的岗位


// B取消订阅了笔试成绩
observer.remove('examinationB');
// A都取消订阅了岗位
observer.remove('job', jobListForA);

observer.publish('examinationB', 80); // 没有可匹配的订阅,无输出
observer.publish('job', ['前端', '后端', '测试']); // 输出B的岗位

7.观察者模式

观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰。

最大的区别是调度的地方。虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。

// 观察者
class Observer {
    constructor() {
 
    }
    update(val) {
 
    }
}
// 观察者列表
class ObserverList {
    constructor() {
        this.observerList = []
    }
    add(observer) {
        return this.observerList.push(observer);
    }
    remove(observer) {
        this.observerList = this.observerList.filter(ob => ob !== observer);
    }
    count() {
        return this.observerList.length;
    }
    get(index) {
        return this.observerList(index);
    }
}
// 目标
class Subject {
    constructor() {
        this.observers = new ObserverList();
    }
    addObserver(observer) {
        this.observers.add(observer);
    }
    removeObserver(observer) {
        this.observers.remove(observer);
    }
    notify(...args) {
        let obCount = this.observers.count();
        for (let index = 0; index < obCount; index++) {
            this.observers.get(i).update(...args);
        }
    }
}

Vue重点知识总结—Vuex篇

2020
04/06

1.vuex是什么?怎么使用?哪种功能场景使用它?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。简单来说就是:应用遇到多个组件共享状态时,使用vuex。

文档:https://vuex.vuejs.org/zh/

场景:多个组件共享数据或者是跨组件传递数据时

原理:实际上是通过实例化Vue来实现

流程(工作原理):页面通过mapAction异步提交事件到action。action通过commit把对应参数同步提交到mutation,mutation会修改state中对应的值。最后通过getter把对应值跑出去,在页面的计算属性中,通过,mapGetter来动态获取state中的值。(单向数据流,便于管控)

2.vuex有哪几种属性

有五种,分别是State , Getter , Mutation , Action , Module

  1. state:vuex的基本数据,用来存储变量
  2. geeter:从基本数据(state)派生的数据,相当于state的计算属性
  3. mutation:提交更新数据的方法,必须是同步的(如果需要异步使用action)。每个mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,提交载荷作为第二个参数。
  4. action:和mutation的功能大致相同,不同之处在于 ==》1. Action 提交的是 mutation,而不是直接变更状态。 2. Action 可以包含任意异步操作。
  5. modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

4.异步请求代码应该写在组件的methods中还是vuex的actions中?

一、如果请求来的数据是不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里。

二、如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用,并包装成promise返回,在调用处用async await处理返回的数据。

5.mutation中能放异步操作吗?为什么?

最好不要。但不会报错。会造成状态改变的不可追踪。异步操作通过 Action 来提交 mutation实现,这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

这个只是规范,而不是逻辑的不允许,是为了让devtool工具能够追踪数据变化,另外方便我们更好的处理数据逻辑。

6.Vuex中actions和mutations的区别

Mutation 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

Action 类似于 mutation,不同在于:

  1. Action 需要提交 mutation去变更状态,而不是直接变更状态。
  2. Action 可以包含任意异步操作。
  3. mutation是同步更新数据(内部会进行是否异步检测,严格模式会报错,实现检测方法是通过$watch)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

7. commit和dispatch,这两个有什么作用呢?

类似与在vue中通过click事件,触发methods中的方法。

当存在异步时,在vuex中需要dispatch来触发actions中的方法,actions中的commit可以触发mutations中的方法。

同步,则直接在组件中commit触发vuex中mutations中的方法。

通过this.$store.state.属性 的⽅法来访问状态。

通过this.$store.commit(‘mutation中的⽅法’) 来修改状态。

8.双向绑定和vuex是否冲突?

当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 v-model 会比较棘手。因为双向绑定修改状态时没有通过 mutation 函数, 这里会抛出一个错误。

9.Vuex可以直接修改state的值吗?

可以直接修改,但是极其不推荐。

当我们将vuex的模式改成严格模式的时候,我们在通过直接修改state的方式修改状态的时候,控制台会报错。

state的修改必须在mutation来修改,否则⽆法被devtool所监测,⽆法监测数据的来源,⽆法保存状态快照,也就⽆法实现时间漫游、回滚之类的操作。

简单来说,就是使用commit来更改状态主要是为了状态能够很好的跟踪,开发者工具更好的处理状态变化,更好的调试体验。

10.什么情况下使⽤ Vuex?

如果应⽤够简单,最好不要使⽤Vuex ,⼀个简单的 Event Bus 就可实现跨组件通信(公共的vue实例)。

//使用中间vue实例实现子-子通信

//mid
Vue.prototype.$bus = new Vue()

//某子组件发消息
//....
methods: {
    tellname () {
       // 发出事件,传递数据,givename自定义事件
     this.$bus.$emit('givename', this.mybfname) 
  } 
}

//某子组件收消息

// 组件一加载就进行兄弟组件所发出的事件的监听
created () {
    // vm.$on(vue自定义的事件, 处理函数)
    // 处理函数有一个默认参数,就是其它组件所传递的数据
    this.$bus.$on('givename', (data) => {
      console.log(data);
      this.mysbfname = data
    })
}

需要构建⼀个中⼤型单页应⽤时,使⽤Vuex能更好地在组件外部管理状态。

11.页面刷新后Vuex的state数据丢失怎么办?

Vuex只是在页面内存中保存状态,页面刷新后是必然会丢失状态。

当然可以使用localStoragesessionStoragecookie进行存储。我们一般使用localStorage做持久化(Cookie、sessionStorage、localStorage的区别

初始化state数据时可以直接从localStorage中获取备用值,但是需要每次在修改状态时使用localStorage.setItem(key,val)保存最新值,这样感觉不太优雅。

// 初始化
const store = createStore({
    state(){
        return {
            count: localStorage.getItem('count')
        }
    },
    mutations: {
        // 更新值
        updateCount(state, value){
            state.count++
            localStorage.setItem('count',state.count)
        }

    }
})

// 或者在调用时手动更新值
store.commit('updateCount')
localStorage.setItem('count', store.state.count)

存在问题:

  1. 不是所有的状态都需要持久化,所以需要区别对待,有一定心智负担。
  2. 如果需要保存的状态很多,每次手动重设缓存就比较繁琐,不够优雅。

解决方式:

1) 使用Vuex提供的Api –> subscribe做统一处理。(订阅 store 的 mutation。handler 会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数),可以将处理函数封装为插件。

const unsubscribe = store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
  if(mutation.type==='updateCount'){
    localStorage.setItem('count', state.count)
  }
})

// 你可以调用 unsubscribe 来停止订阅。
unsubscribe()

// 封装为插件
const myPlugin = (store) => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}
// 使用插件
const store = createStore({
  // ...
  plugins: [myPlugin]
})

2) 可以使用开源的插件进行持久化,原理同上。插件如 vuex-persist、vuex-persistedstate等。

12.Vuex的缺点

Vuex 可以集中组件的公共状态,利用响应式,很方便我们做开发。但模块化这一块做得比较复杂,使用不是很方便,需要经常看文档去回顾。

另外,如果不打算开发⼤型应⽤,使⽤Vuex可能是繁琐冗余的。Vuex是重量级的,对性能有一些影响。换句话说,Vuex其实不适合中小型应用。

1)模块使用比较繁琐,获取状态模式不统一,复杂度较高,增加心智负担,容易出错

// 使用插件
const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
// 使用
store.state.b // -> moduleB 的状态
store.state.a  // 要带上 moduleA 的 key(.a) ,内嵌模块的话会很长,代码阅读性不佳,不得不配合 mapState 使用
store.getters.c  // moduleA 里的 getters ,没有 namespaced: true 时又变成了全局的
store.getters['a/c']  // namespaced: true 时需要加 path
store.commit('d')  // 没有 namespaced 时又是全局的
store.commit('a/d')  // namespaced: true 时也需要加 path,要使用mapMutations 的话也比较麻烦

2)对TS的支持不太友好,没有友好的类型支持和提示

虽然Vuex在后期版本通过定义类型化的 InjectionKey 使得类型支持有所改善,但使用会很不优雅。

3)体积较大,重量级,小型项目对性能有一定影响。

如今团队又推出了新一代的状态管理插件:Pinia,它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。其库容量和性能要优于Vuex。而且完全支持Ts。

Pinia是Vuex的良好替代品吗?

排序算法

2020
29/03

每个语言一般都有语言自带的排序方法,每个语言的排序内部实现都是不同的。对于 JS 来说,数组长度大于 10 会采用快排,否则使用插入排序。抛开语言自带的排序方法,这里总结了一些排序的算法。

冒泡排序

  1. 从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。
  2. 下一轮重复以上操作,每次只要比较到上一次的最大值位置。
function bubbleSort(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    // 从 0 到 `length - 1` 遍历
    for (let j = 0; j < i; j++) {
        if (arr[j] > arr[j + 1]){
            let temp = arr[j]
            arr[j] = arr[j+1]
            arr[j+1] = temp
        }
    }
  }
  return arr
}

二分法排序

  1. 获取中间元素,以中间元素把原数组切割成左右两个数组
  2. 余下元素和该元素对比,大于的放右边,小于的放左边
  3. 递归迭代

ps:这里的中间值可以是中间,也可以是随机的某个值(快排)。

function twoSort(arr){
    if(arr.length<=1){
        return arr;
    }
    var middle = arr.splice(Math.floor(arr.length/2),1)
    //var middle = arr.splice(parseInt(Math.random() * arr.length),1)   //随机取
    console.log(middle)
    var leftArr = []
    var rightArr = []
    for(var i=0; i<arr.length; i++){
       if(parseInt(arr[i])<=middle){
           leftArr.push(arr[i])      //把比中间值小的放一个数组
       }else{
           rightArr.push(arr[i])     //把比中间值大的放另一个数组
       }
    }
    return twoSort(leftArr).concat(middle,twoSort(rightArr))
}
var arr = [9,5,1,2,7,8,4,6,3];
var newArr = twoSort(arr);
console.log(newArr, 0)

插入法(前插)

  1. 依次找出比前一项小的所有值
  2. 让它们依次和之前已经排序好的数列进行比较,将小值与前一项互换位置
function insertSort(arr) {
    var len = arr.length
    for(var i=0; i<len; i++) {
        for(var j=i; j<len && arr[j+1]<arr[j]; j--) {
            var temp = arr[j]
            arr[j] = arr[j+1]
            arr[j+1] = temp
        }
    }
    return arr
}

选择排序

  1. 假定第一个数为最小值,让余下的第一个作比较,如果比第一个更小,交换位置
  2. 再假定第二个为最小值,依次迭代
function selectSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for(var i=0; i<len; i++) {
        //先给一个索引号,假设i为最小的数
        minIndex = i;
        //循环遍历,如果i之后有比索引i更小的数,则将索引变为j
        for(var j=i; j<len; j++) {
            if(arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if(minIndex!=i){
             //将i索引的数和j索引的数互换
            temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
    return arr;
}