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;
}

ES6之includes方法详解及其与indexOf区别

2020
25/03

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016引入了该方法。

includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false

includes()方法在字符串中使用时,相当于indexOf(),查询成功返回true,失败返回false

'abc'.includes('ab') // true
'abc'.includes('d') // false

在数组中使用时,可以查询某个元素是否包含在数组中(只能查询Number,String类型的元素)

[1 , 2, 3].includes(0) // false
['1' , '2', '3'].includes('1') // true
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4, 但数组长度为3),则会重置为0开始。

[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

if (arr.indexOf(el) !== -1) {
    // ...
}

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等(Strict Equality Comparison )运算符进行判断,这会导致对NaN的误判。includes使用的是不一样的判断算法(SameValueZero策略),就没有这个问题。

[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true

下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

const contains = (() => Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    :(arr, value) => arr.some(el => el === value)
)()

另外,MapSet数据结构有一个has方法需要注意与includes区分。

  1. Map结构的has方法,是用来查找键名的,比如Map.prototype.has(key),WeakMap.prototype.has(key), Reflect.has(target, propertyKey)
  2. Set结构的has方法,是用来查找值的,比如Set.prototype.has(value),WeakSet.prototype.has(value)

DOM之事件触发、冒泡、捕获详解

2020
21/03

DOM触发事件方法

1.html attribute 标签中添加事件属性

<input value="Click me" onclick="alert('Click!')" type="button">
js逻辑在属性里创建,不是很好,写到一个方法里.
<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">
因为html属性不区分大小写,所以你onClick可以随你写,ONCLICK, onCLICK, 最好是onclick.

2.dom property 元素属性赋值

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>
这个方式直接在dom上绑定,上面的html-attribute是浏览器读取他创建函数对象写入dom中。

处理程序始终位于dom property中:html attribute 只是初始化它的一个方法,并且也不被推荐。

如果html attributre 存在事件绑定,并且dom也存在事件绑定,相同的事件将被dom中的给替代。

直接给元素加事件属性(DOM0级模型)是注册事件最简单的方法,将处理程序赋值给相应的事件属性(事件前面带on)。这种方式适用于所有浏览器。但是缺点是元素将最多只有一个事件处理程序。

dom绑定的一些写法.

<input id="elem" type="button" value="Click me">
<script>
  //1
  elem.onclick = function() {
    alert('Thank you');
  }; 
  
  //2
  function thank() {
    alert('thank you')
  }
  elem.onclick = thank;
</script>

// 但是如果是input中也是不同的。
<input id="elem" type="button" value="Click me" onclick="thank()">

3.addEventListener

事件处理程序的注册方法为addEventListener(),它接受3个参数(事件类型,事件回调函数,布尔值),其中最后一个参数默认值为false,将会把程序注册为冒泡事件处理程序。而其若为true,则为捕获事件处理程序。两者的区别是:若为冒泡,表示在冒泡阶段调用事件处理程序,逐渐冒泡到外层。而捕获恰好相反,在捕获阶段调用处理程序,执行顺序相反。在IE中只支持事件冒泡。移除事件使用removeEventListener(),传参如上。

IE8及以下浏览器不支持此方法,使用attachEvent(on+事件类型, 事件回调函数)方法,只有两个参数(不支持事件捕获)。移除事件使用detachEvent()方法,参数相同。

<button id="elem">Click me</button>

<script>
  elem.addEventListener('click', {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  });
</script>
上面的方法使用到了handleEvent, 如果发生事件,handleEvent会被调用。

也可以那么说,当addEventListener接收到处理程序对象时,事件会去调用object.handleEvent(event)

等同于:

<button id="elem">Click me</button>

<script>
  elem.addEventListener('click',function(event) {
      alert(event.type + " at " + event.currentTarget);
  });
</script>

如下,很实用的方法。

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      console.log(1)
    }

    onMouseup() {
      console.log(2)
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

注意:dom添加赋值事件属性方式(普通事件)可以被第二次调用的同一个事件覆盖,而addEventListener事件绑定不会被覆盖,而是会依次执行。

事件流

事件流:事件流是描述从页面中接收事件的顺序。也可以说是事件传播的顺序。

“DOM2事件流”规定的事件流包括三个阶段:

  1. 事件捕获阶段:提供了在事件没有送达目标之前查看事件的机会。与冒泡方向相反,由父向子逐级捕获。
  2. 处于目标阶段:目标对象(元素)本身的时间处理程序调用(执行)。
  3. 事件冒泡阶段。目标元素的事件大部分都会冒泡(传播)到DOM树根。有一些特殊的事件不会冒泡,比如mouseenter和mouseleave事件不会冒泡。若要冒泡,可以使用mouseover和mouseout替代。

事件模型

js包含三种事件模型:DOM0事件模型(始事件模型),DOM2事件模型,IE事件模型

  1. DOM0级模型:事件不会流动传播。事件绑定监听函数方式:① html标签中添加属性如<div onclick="fun()"></div>;② js中获取dom元素后给元素加事件属性:document.onclick = fn,取消监听使用document.onclick = null
  2. IE事件模型:使用attachEvent("onclick", handler)detachEvent("onclick", handler)方法进行绑定事件和移除绑定。事件流只有2个阶段,没有事件捕获阶段。
  3. DOM2级模型:addEventListener()removeEventListener()进行监听和取消。事件流有3个阶段。

事件冒泡

就是一个事件(例如点击),这个点击会往上层元素进行冒泡(往上进行),他是从点击的这个元素(目标元素,即event.target)开始向上去冒泡。

这里开始,先搞清两个点event.targetevent.currentTarget

event.target是当前点击的元素(目标元素)

event.currentTarget也就是this, 是绑定函数操作的元素(addEventListener绑定的元素,有可能是目标元素的父元素)

有的时候你可能并不想他去冒泡,可以通过stopPropagation这个方法去干掉他。

  • event.stopPropagation()

这个方法有个地方需要注意一下,那就是当一个事件有多个处理程序的时候,他只会停止当前程序的冒泡,其他的程序不会收到影响,需要解决这个问题,就需要使用到event.stopImmediatePropagation方法。

尽量不要阻止事件冒泡,除非你知道你自己在干什么。

当你调用return false时会做 3 件事:

  • event.preventDefault() – 它停止浏览器的默认行为。
  • event.stopPropagation() – 它阻止事件传播(或“冒泡”)。
  • 停止回调执行并立即返回。

事件捕获

事件捕获和事件冒泡是不同的,捕获是从上而下。如果需要捕获事件,那就需要将addEventListener的第三个参数(叫做useCapture)设置成true.

默认的false是在冒泡阶段处理事件,true就是捕获阶段处理事件。看下面的代码:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>
运行之后,发现两个顺序是相反的。而且点击的元素是位于捕获阶段的最后,冒泡阶段的开始。