<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<span class="token comment"><!-- 正则表达式 (使用 `v-bind`) --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>keep-alive</span> <span class="token attr-name">:include</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/a|b/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>component</span> <span class="token attr-name">:is</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>view<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>component</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>keep-alive</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- 数组 (使用 `v-bind`) --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>keep-alive</span> <span class="token attr-name">:include</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>[<span class="token punctuation">'</span>a<span class="token punctuation">'</span>, <span class="token punctuation">'</span>b<span class="token punctuation">'</span>]<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>component</span> <span class="token attr-name">:is</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>view<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>component</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>keep-alive</span><span class="token punctuation">></span></span>
但更多场景中,我们会使用keep-alive
来缓存路由:
<keep-alive include='a'>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>router-view</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>router-view</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>keep-alive</span><span class="token punctuation">></span></span>
匹配规则:
- 首先匹配组件的name选项,如果
name
选项不可用。
- 则匹配它的局部注册名称。 (父组件
components
选项的键值)
- 匿名组件,不可匹配。
比如路由组件没有name
选项,并且没有注册的组件名。
- 只能匹配当前被包裹的组件,不能匹配更下面嵌套的子组件。
比如用在路由上,只能匹配路由组件的name
选项,不能匹配路由组件里面的嵌套组件的name
选项。
- 文档:
<keep-alive>
不会在函数式组件中正常工作,因为它们没有缓存实例。
exclude
的优先级大于include
也就是说:当include
和exclude
同时存在时,exclude
生效,include
不生效。
<keep-alive include="a,b" exclude="a">
<!--只有a不被缓存-->
<router-view></router-view>
</keep-alive>
当组件被exclude
匹配,该组件将不会被缓存,不会调用activated
和 deactivated
。
# 组件生命周期钩子:
关于组件的生命周期,是时候放出这张图片了:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/164bbf610b61f3cd?raw=true)
这张图片已经讲得很清楚了,很多人这部分也很清楚了,大部分生命周期并不会用到,这里提一下几点:
-
ajax请求最好放在created
里面,因为此时已经可以访问this
了,请求到数据就可以直接放在data
里面。
这里也碰到过几次,面试官问:ajax请求应该放在哪个生命周期。
-
关于dom的操作要放在mounted
里面,在mounted
前面访问dom会是undefined
。
-
每次进入/离开组件都要做一些事情,用什么钩子:
-
不缓存:
进入的时候可以用created
和mounted
钩子,离开的时候用beforeDestory
和destroyed
钩子,beforeDestory
可以访问this
,destroyed
不可以访问this
。
-
缓存了组件:
缓存了组件之后,再次进入组件不会触发beforeCreate
、created
、beforeMount
、 mounted
,如果你想每次进入组件都做一些事情的话,你可以放在activated
进入缓存组件的钩子中。
同理:离开缓存组件的时候,beforeDestroy
和destroyed
并不会触发,可以使用deactivated
离开缓存组件的钩子来代替。
# 触发钩子的完整顺序:
将路由导航、keep-alive
、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件:
beforeRouteLeave
:路由组件的组件离开路由前钩子,可取消路由离开。
beforeEach
: 路由全局前置守卫,可用于登录验证、全局路由loading等。
beforeEnter
: 路由独享守卫
beforeRouteEnter
: 路由组件的组件进入路由前钩子。
beforeResolve
:路由全局解析守卫
afterEach
:路由全局后置钩子
beforeCreate
:组件生命周期,不能访问this
。
created
:组件生命周期,可以访问this
,不能访问dom。
beforeMount
:组件生命周期
deactivated
: 离开缓存组件a,或者触发a的beforeDestroy
和destroyed
组件销毁钩子。
mounted
:访问/操作dom。
activated
:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行beforeRouteEnter回调函数next。
# 小结
Vue提供了很多钩子,但很多钩子我们几乎不会用到,只有清楚这些钩子函数的触发顺序以及背后的一些限制等,这样我们才能够正确的使用这些钩子,希望看了本文的同学,能对这些钩子有更加清晰的认识,使用起来更加得心应手。
博客链接
# 论普通函数和箭头函数的区别以及箭头函数的注意事项和不适用场景
箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深...
# 普通函数和箭头函数的区别:
# 箭头函数的this指向规则:
# 1. 箭头函数没有prototype
(原型),所以箭头函数本身没有this
let a = () =>{};
console.log(a.prototype); // undefined
# 2. 箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。
下面栗子中在一个函数中定义箭头函数,然后在另一个函数中执行箭头函数。
let a,
barObj = { msg: 'bar的this指向' };
fooObj = { msg: 'foo的this指向' };
bar.call(barObj); // 将bar的this指向barObj
foo.call(fooObj); // 将foo的this指向fooObj
function foo() {
a(); // 结果:{ msg: 'bar的this指向' }
}
function bar() {
a = () => {
console.log(this, 'this指向定义的时候外层第一个普通函数'); //
}; // 在bar中定义 this继承于bar函数的this指向
}
从上面栗子中可以得出两点
- 箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有关系。
- 被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变
# 3. 不能直接修改箭头函数的this指向
上个栗子中的foo函数修改一下,尝试直接修改箭头函数的this指向。
let fnObj = { msg: '尝试直接修改箭头函数的this指向' };
function foo() {
a.call(fnObj); // 结果:{ msg: 'bar的this指向' }
}
很明显,call显示绑定this指向失败了,包括aaply、bind都一样。
它们(call、aaply、bind)会默认忽略第一个参数,但是可以正常传参。
然后我又通过隐式绑定来尝试同样也失败了,new 调用会报错,这个稍后再说。
SO,箭头函数不能直接修改它的this指向。
幸运的是,我们可以通过间接的形式来修改箭头函数的指向:
去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变,这在上一个栗子中有演示。
bar.call(barObj); // 将bar普通函数的this指向barObj 然后内部的箭头函数也会指向barObj
# 4. 箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window
(全局对象)
唔,这个问题实际上是面试官提出来的,当时我认为的箭头函数规则就是:箭头函数的this指向继承自外层第一个普通函数的this,现在看来真是不严谨(少说一个定义的时候),要是面试官问我:定义和执行不在同一个普通函数中,它又指向哪里,肯定歇菜...
既然箭头函数的this指向在定义的时候继承自外层第一个普通函数的this,那么:
当箭头函数外层没有普通函数,它的this会指向哪里?
这里跟我之前写的this绑定规则不太一样(不懂的可以点进去看一下),普通函数的默认绑定规则是:
在非严格模式下,默认绑定的this指向全局对象,严格模式下this指向undefined
如果箭头函数外层没有普通函数继承,它this指向的规则:
经过测试,箭头函数在全局作用域下,严格模式和非严格模式下它的this都会指向window
(全局对象)。
Tip:测试的时候发现严格模式在中途声明无效,必须在全局/函数的开头声明才会生效:
a = 1;
'use strict'; // 严格模式无效 必须在一开始就声明严格模式
b = 2; // 不报错
# 箭头函数的
# 箭头函数的arguments
# 箭头函数的this指向全局,使用arguments会报未声明的错误
如果箭头函数的this指向window
(全局对象)使用arguments
会报错,未声明arguments
。
let b = () => {
console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined
PS:如果你声明了一个全局变量为arguments
,那就不会报错了,但是你为什么要这么做呢?
# 箭头函数的this指向普通函数时,它的argumens
继承于该普通函数
上面是第一种情况:箭头函数的this指向全局对象,会报arguments未声明的错误。
第二种情况是:箭头函数的this如果指向普通函数,它的argumens
继承于该普通函数。
function bar() {
console.log(arguments); // ['外层第二个普通函数的参数']
bb('外层第一个普通函数的参数');
function bb() {
console.log(arguments); // ["外层第一个普通函数的参数"]
let a = () => {
console.log(arguments, 'arguments继承this指向的那个普通函数'); // ["外层第一个普通函数的参数"]
};
a('箭头函数的参数'); // this指向bb
}
}
bar('外层第二个普通函数的参数');
那么应该如何来获取箭头函数不定数量的参数呢?答案是:ES6的rest参数(...
扩展符)
# rest参数获取函数的多余参数
这是ES6的API,用于获取函数不定数量的参数数组,这个API是用来替代arguments
的,API用法如下:
let a = (first, ...abc) => {
console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);
上面的栗子展示了,获取函数除第一个确定的参数,以及用一个变量接收其他剩余参数的示例。
也可以直接接收函数的所有参数,rest参数的用法相对于arguments
的优点:
-
箭头函数和普通函数都可以使用。
-
更加灵活,接收参数的数量完全自定义。
-
可读性更好
参数都是在函数括号中定义的,不会突然出现一个arguments
,以前刚见到的时候,真的好奇怪了!
-
rest是一个真正的数组,可以使用数组的API。
因为arguments
是一个类数组的对象,有些人以为它是真正的数组,所以会出现以下场景:
arguments.push(0); // arguments.push is not a function
如上,如果我们需要使用数组的API,需要使用扩展符/Array.from来将它转换成真正的数组:
arguments = [...arguments]; 或者 :arguments = Array.from(arguments);
rest参数有两点需要注意:
-
rest必须是函数的最后一位参数:
let a = (first, ...rest, three) => {
console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
};
a(1, 2, 3, 4);
-
函数的length属性,不包括 rest 参数
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
扩展运算符还可以用于数组,这里是阮一峰老师的文档
PS:感觉这里写多了,但比较喜欢把一个知识点讲清楚...
# 使用new
调用箭头函数会报错
无论箭头函数的thsi指向哪里,使用new
调用箭头函数都会报错,因为箭头函数没有constructor
let a = () => {};
let b = new a(); // a is not a constructor
# 箭头函数不支持new.target
:
new.target
是ES6新引入的属性,普通函数如果通过new
调用,new.target
会返回该函数的引用。
此属性主要:用于确定构造函数是否为new调用的。
-
箭头函数的this指向全局对象,在箭头函数中使用箭头函数会报错
let a = () => {
console.log(new.target); // 报错:new.target 不允许在这里使用
};
a();
-
箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用。
new bb();
function bb() {
let a = () => {
console.log(new.target); // 指向函数bb:function bb(){...}
};
a();
}
更多关于new.target
可以看一下阮一峰老师关于这部分的解释。
# 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:
function func1(a, a) {
console.log(a, arguments); // 2 [1,2]
}
var func2 = (a,a) => {
console.log(a); // 报错:在此上下文中不允许重复参数名称
};
func1(1, 2); func2(1, 2);
# 箭头函数相对于普通函数语法更简洁优雅:
讲道理,语法上的不同,也属与它们两个的区别!
-
箭头函数都是匿名函数,并且都不用写function
-
只有一个参数的时候可以省略括号:
var f = a => a; // 传入a 返回a
-
函数只有一条语句时可以省略{}
和return
var f = (a,b,c) => a; // 传入a,b,c 返回a
-
简化回调函数,让你的回调函数更优雅:
[1,2,3].map(function (x) {
return x * x;
}); // 普通函数写法
[1,2,3].map(x => x * x); // 箭头函数只需要一行
# 箭头函数的注意事项及不适用场景
# 箭头函数的注意事项
-
一条语句返回对象字面量,需要加括号,或者直接写成多条语句的return
形式,
否则像func中演示的一样,花括号会被解析为多条语句的花括号,不能正确解析
var func1 = () => { foo: 1 }; // 想返回一个对象,花括号被当成多条语句来解析,执行后返回undefined
var func2 = () => ({foo: 1}); // 用圆括号是正确的写法
var func2 = () => {
return {
foo: 1 // 更推荐直接当成多条语句的形式来写,可读性高
};
};
- 箭头函数在参数和箭头之间不能换行!
var func = ()
=> 1; // 报错: Unexpected token =>
- 箭头函数的解析顺序相对靠前
MDN: 虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则
let a = false || function() {}; // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok
# 箭头函数不适用场景:
围绕两点:箭头函数的this意外指向和代码的可读性。
- 定义字面量方法,this的意外指向。
因为箭头函数的简洁
const obj = {
array: [1, 2, 3],
sum: () => {
// 根据上文学到的:外层没有普通函数this会指向全局对象
return this.array.push('全局对象下没有array,这里会报错'); // 找不到push方法
}
};
obj.sum();
上述栗子使用普通函数或者ES6中的方法简写的来定义方法,就没有问题了:
// 这两种写法是等价的
sum() {
return this.array.push('this指向obj');
}
sum: function() {
return this.array.push('this指向obj');
}
还有一种情况是给普通函数的原型定义方法的时候,通常会在普通函数的外部进行定义,比如说继承/添加方法的时候。
这时候因为没有在普通函数的内部进行定义,所以this会指向其他普通函数,或者全局对象上,导致bug!
- 回调函数的动态this
下文是一个修改dom文本的操作,因为this指向错误,导致修改失败:
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
this.innerHTML = 'Clicked button'; // this又指向了全局
});
相信你也知道了,改成普通函数就成了。
-
考虑代码的可读性,使用普通函数
# 文章内容小结:
# 普通函数和箭头函数的区别:
- 箭头函数没有
prototype
(原型),所以箭头函数本身没有this
- 箭头函数的this在定义的时候继承自外层第一个普通函数的this。
- 如果箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向
window
(全局对象)
- 箭头函数本身的this指向不能改变,但可以修改它要继承的对象的this。
- 箭头函数的this指向全局,使用arguments会报未声明的错误。
- 箭头函数的this指向普通函数时,它的
argumens
继承于该普通函数
- 使用
new
调用箭头函数会报错,因为箭头函数没有constructor
- 箭头函数不支持
new.target
- 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
- 箭头函数相对于普通函数语法更简洁优雅
# 箭头函数的注意事项及不适用场景
箭头函数的注意事项:
- 箭头函数一条语句返回对象字面量,需要加括号
- 箭头函数在参数和箭头之间不能换行
- 箭头函数的解析顺序相对
||
靠前
不适用场景:箭头函数的this意外指向和代码的可读性。
# 结语
呕心沥血,可以说是很全了,反正第一次问到我的时候只能想到箭头函数的this是继承而来的,以及语法上的简洁性,其他的我都不知道,希望这篇文章能够帮助各位同学学到知识。
PS:目前找工作中,求大佬们内推,中高级前端,偏JS,Vue,上海杨浦。
博客、前端积累文档、公众号、GitHub、wx:OBkoro1、邮箱:[email protected]
以上2019.03.22
参考资料:
MDN 箭头函数
阮一峰-ES6入门
什么时候你不能使用箭头函数?
博客链接
# 一次弄懂 Object.defineProperty
# 基本用法:
let obj = {
singer: '周杰伦'
};
let value = '青花瓷';
Object.defineProperty(obj, 'music', {
value: value // music的值
// configurable: false, // music默认不能删除 要删除须设置为true 设为true 可删除
// writable: false, // 默认不能修改music 设为true 可修改
// enumerable: false, // music默认是不能被枚举(遍历) 设为true 可遍历
});
delete obj.music;
console.log(obj.music); // 青花瓷 删除无效
obj.music = '听妈妈的话';
console.log(obj.music); // 青花瓷 修改无效
for (let key in obj) {
console.log(key); // singer
}
// music 没有被遍历
# 默认不能修改、不能删除、不能遍历
通过栗子可以发现:通过 defineProperty 设置的属性,默认不能修改,不能删除,不能遍历,当然你可以通过设置更改他们。
# Object.defineProperty
的作用:
完全掌控对象的某个属性,增删改查全都可以设定!
# 设置get
、set
:
不能同时设置
get,set 设置时不能同时设置 writable 和 value, 他们是一对情侣的存在,交叉设置或同时存在,会报错
let obj = {
singer: '周杰伦'
};
let value = '青花瓷';
Object.defineProperty(obj, 'music', {
enumerable: true, // 设置可枚举
get() {
// 获取obj.music的时候就会调用get方法
// let value = "强行设置get的返回值"; // 打开注释 读取属性永远都是‘强行设置get的返回值’
return value;
},
set(val) {
// value = val; // 将修改的值重新赋给song
value = '强行设置修改的值';
}
});
console.log(obj.music); // 青花瓷
delete obj.music; // 删除无效
console.log(obj.music); // 青花瓷
obj.music = '听妈妈的话';
console.log(obj.music); // 强行设置修改的值
for (let key in obj) {
console.log(key); // singer, music 上面设置了enumerable可枚举
}
这个Object.defineProperty
的用法就是上面两个栗子中所展示的那样,可以将栗子copy
到本地自己玩一下。
博客链接
# 仿Vue极简双向绑定
现在的前端面试不管你用的什么框架,总会问你这个框架的双向绑定机制,有的甚至要求你现场实现一个双向绑定出来,那对于没有好好研究过这方面知识的同学来说,当然是很难的,接下来本文用160行代码带你实现一个极简的双向绑定机制。如果喜欢的话可以点波赞/关注,支持一下,希望大家看完本文可以有所收获。
# 效果GIF:
![](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1fsl70vrkj3g30aw09iq3y.gif?raw=true)
# demo地址:
codepen:仿Vue极简双向绑定
Github:仿Vue极简双向绑定
# 了解Object.defineProperty():
这个API是实现双向绑定的核心,最主要的作用是重写数据的get
、set
方法
# 使用方式:
let obj = {
singer: "周杰伦"
};
let default_value = "青花瓷";
Object.defineProperty(obj, "music", {
// value: '七里香', // 设置属性的值 下面设置了get set函数 所以这里不能设置
configurable: false, // 是否可以删除属性 默认不能删除
// writable: true, // 是否可以修改对象 下面设置了get set函数 所以这里不能设置
enumerable: true, // music是否可以被枚举 默认是不能被枚举(遍历)
// ☆ get,set设置时不能设置writable和value,要一对一对设置,交叉设置/同时存在 就会报错
get() {
// 获取obj.music的时候就会调用get方法
// let default_value = "强行设置get的返回值"; // 打开注释 读取属性永远都是‘强行设置get的返回值’
return default_value;
},
set(val) {
// 将修改的值重新赋给song
default_value = val;
}
});
console.log(obj.music); // 青花瓷
delete obj.music; // configurable设为false 删除无效
console.log(obj.music); // 青花瓷
obj.music = "听妈妈的话";
console.log(obj.music); // 听妈妈的话
for (let key in obj) {
// 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
// 需要设置enumerable为true才可以 否则只能拿到singer 属性
console.log(key); // singer, music
}
# 示例demo:
对,这里有个demo。
# 画一下重点:
- get,set设置时不能设置writable和value, 他们是一对情侣的存在,交叉设置或同时存在,会报错
- 通过
defineProperty
设置的属性,默认不能删除,不能遍历,当然你可以通过设置更改他们。
- get、set 是函数,可以做的事情很多。
兼容性:IE 9,Firefox 4, Chorme 5,Opera 11.6,Safari 5.1
更详细的可以看一下MDN
# 实现思路:
# mvvm系列的双向绑定,关键步骤:
- 实现数据监听器Observer,用
Object.defineProperty()
重写数据的get、set,值更新就在set中通知订阅者更新数据。
- 实现模板编译Compile,深度遍历dom树,对每个元素节点的指令模板进行替换数据以及订阅数据。
- 实现Watch用于连接Observer和Compile,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
- mvvm入口函数,整合以上三者。
# 流程图:
![](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1fsl7dygfovj30ka0augpd.jpg?raw=true)
这部分讲的很清楚,现在有点懵逼也没关系,看完代码,自己copy下来玩一玩之后,回头再看实现思路,相信会有收获的。
# 具体代码实现:
# html结构:
<div id="app">
<input type="text" v-model="name">
<h3 v-bind="name"></h3>
<input type="text" v-model="testData1">
<h3>{{ testData1 }}</h3>
<input type="text" v-model="testData2">
<h3>{{ testData2 }}</h3>
</div>
看到这个模板,相信用过Vue的同学都不会陌生。
# 调用方法:
采用类Vue方式来使用双向绑定:
window.onload = function () {
var app = new myVue({
el: '#app', // dom
data: { // 数据
testData1: '仿Vue',
testData2: '极简双向绑定',
name: 'OBKoro1'
}
})
}
# 创建myVue函数:
实际上这里是我们实现思路中的第四步,用于整合数据监听器this._observer()
、指令解析器this._compile()
以及连接Observer和Compile的_watcherTpl的watch池。
function myVue(options = {}) { // 防止没传,设一个默认值
this.$options = options; // 配置挂载
this.$el = document.querySelector(options.el); // 获取dom
this._data = options.data; // 数据挂载
this._watcherTpl = {}; // watcher池
this._observer(this._data); // 传入数据,执行函数,重写数据的get set
this._compile(this.$el); // 传入dom,执行函数,编译模板 发布订阅
};
# Watcher函数:
这是实现思路中的第三步,因为下方数据监听器_observer()
需要用到Watcher函数,所以这里就先讲了。
像实现思路中所说的,这里起到了连接Observer和Compile的作用:
- 在模板编译_compile()阶段发布订阅
- 在赋值操作的时候,更新视图
// new Watcher() 为this._compile()发布订阅+ 在this._observer()中set(赋值)的时候更新视图
function Watcher(el, vm, val, attr) {
this.el = el; // 指令对应的DOM元素
this.vm = vm; // myVue实例
this.val = val; // 指令对应的值
this.attr = attr; // dom获取值,如value获取input的值 / innerHTML获取dom的值
this.update(); // 更新视图
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm._data[this.val]; // 获取data的最新值 赋值给dom 更新视图
}
没有看错,代码量就这么多,可能需要把整个代码连接起来,多看几遍才能够理解。
# 实现数据监听器_observer():
实现思路中的第一步,用Object.defineProperty()
遍历data重写所有属性的get set。
然后在给对象的某个属性赋值的时候,就会触发set。
在set中我们可以监听到数据的变化,然后就可以触发watch更新视图。
myVue.prototype._observer = function (obj) {
var _this = this;
Object.keys(obj).forEach(key => { // 遍历数据
_this._watcherTpl[key] = { // 每个数据的订阅池()
_directives: []
};
var value = obj[key]; // 获取属性值
var watcherTpl = _this._watcherTpl[key]; // 数据的订阅池
Object.defineProperty(_this._data, key, { // 双向绑定最重要的部分 重写数据的set get
configurable: true, // 可以删除
enumerable: true, // 可以遍历
get() {
console.log(`${key}获取值:${value}`);
return value; // 获取值的时候 直接返回
},
set(newVal) { // 改变值的时候 触发set
console.log(`${key}更新:${newVal}`);
if (value !== newVal) {
value = newVal;
watcherTpl._directives.forEach((item) => { // 遍历订阅池
item.update();
// 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图
});
}
}
})
});
}
# 实现Compile 模板编译
这里是实现思路中的第三步,让我们来总结一下这里做了哪些事情:
- 首先是深度遍历dom树,遍历每个节点以及子节点。
- 将模板中的变量替换成数据,初始化渲染页面视图。
- 把指令绑定的属性添加到对应的订阅池中
- 一旦数据有变动,收到通知,更新视图。
myVue.prototype._compile = function (el) {
var _this = this, nodes = el.children; // 获取app的dom
for (var i = 0, len = nodes.length; i < len; i++) { // 遍历dom节点
var node = nodes[i];
if (node.children.length) {
_this._compile(node); // 递归深度遍历 dom树
}
// 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
node.addEventListener('input', (function (key) {
var attVal = node.getAttribute('v-model'); // 获取v-model绑定的值
_this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
node,
_this,
attVal,
'value'
));
return function () {
_this._data[attVal] = nodes[key].value; // input值改变的时候 将新值赋给数据 触发set=>set触发watch 更新视图
}
})(i));
}
if (node.hasAttribute('v-bind')) { // v-bind指令
var attrVal = node.getAttribute('v-bind'); // 绑定的data
_this._watcherTpl[attrVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
node,
_this,
attrVal,
'innerHTML'
))
}
<span class="token keyword">var</span> reg <span class="token operator">=</span> <span class="token regex">/\{\{\s*([^}]+\S)\s*\}\}/g</span><span class="token punctuation">,</span> txt <span class="token operator">=</span> node<span class="token punctuation">.</span>textContent<span class="token punctuation">;</span> <span class="token comment">// 正则匹配{{}} </span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>reg<span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>txt<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
node<span class="token punctuation">.</span>textContent <span class="token operator">=</span> txt<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span>reg<span class="token punctuation">,</span> <span class="token punctuation">(</span>matched<span class="token punctuation">,</span> placeholder<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// matched匹配的文本节点包括双花括号, placeholder 是双花括号中间的属性名</span>
<span class="token keyword">var</span> getName <span class="token operator">=</span> _this<span class="token punctuation">.</span>_watcherTpl<span class="token punctuation">;</span> <span class="token comment">// 所有绑定watch的数据</span>
getName <span class="token operator">=</span> getName<span class="token punctuation">[</span>placeholder<span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// 获取对应watch 数据的值</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>getName<span class="token punctuation">.</span>_directives<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// 没有事件池 创建事件池</span>
getName<span class="token punctuation">.</span>_directives <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
getName<span class="token punctuation">.</span>_directives<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Watcher</span><span class="token punctuation">(</span> <span class="token comment">// 将dom替换成属性的数据并发布订阅 在set的时候更新数据</span>
node<span class="token punctuation">,</span>
_this<span class="token punctuation">,</span>
placeholder<span class="token punctuation">,</span>
<span class="token string">'innerHTML'</span>
<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> placeholder<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span>val<span class="token punctuation">,</span> key<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> _this<span class="token punctuation">.</span>_data<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// 获取数据的值 触发get 返回当前值 </span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> _this<span class="token punctuation">.</span>$el<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
}
# 完整代码&demo地址
GitHub完整代码
codepen:仿Vue极简双向绑定
Github:仿Vue极简双向绑定
如果觉得还不错的话,就给个Star⭐️鼓励一下我吧~
# 小结
本文只是一个简单的实现双向绑定的方法,主要目的是帮助各位同学理解mvvm框架的双向绑定机制,也并没有很完善,这里还是有很多缺陷,比如:没有实现数据的深度对数据进行get
、set
等。希望看完本文,大家能有所收获。
以上2018.6.24
# 参考资料:
剖析Vue原理&实现双向绑定MVVM
面试题:你能写一个Vue的双向数据绑定吗?
不好意思!耽误你的十分钟,让MVVM原理还给你
博客链接
# 数组API解析合集
数组的使用场景非常多,平日中也涉及到很多数组的api
/相关操作,一直也没有对这块内容进行一块整理总结,所以这次对这块内容做一个较为系统的总结,方便自己、也方便他人。
# 创建一个数组:
// 字面量方式:
// 这个方法也是我们最常用的,在初始化数组的时候 相当方便
let a = [3, 11, 8]; // [3,11,8];
// 构造器:
// 实际上 new Array === Array,加不加new 一点影响都没有。
let a = Array(); // []
let a = Array(3); // [,,]
let a = Array(3,11,8); // [ 3,11,8 ]
# ES6 Array.of() 返回由所有参数值组成的数组
定义:返回由所有参数值组成的数组,如果没有参数,就返回一个空数组。
目的:Array.of() 出现的目的是为了解决上述构造器因参数个数不同,导致的行为有差异的问题。
let a = Array.of(3, 11, 8); // [3,11,8]
let a = Array.of(3); // [3]
# ES6 Arrary.from() 将两类对象转为真正的数组
定义:用于将两类对象转为真正的数组(不改变原对象,返回新的数组)。
参数:
第一个参数(必需):要转化为真正数组的对象。
第二个参数(可选): 类似数组的map方法,对每个元素进行处理,将处理后的值放入返回的数组。
第三个参数(可选): 用来绑定this。
// 1. 对象拥有length属性
let obj = {0: 'a', 1: 'b', 2:'c', length: 3};
let arr = Array.from(obj); // ['a','b','c'];
// 2. 部署了 Iterator接口的数据结构 比如:字符串、Set、NodeList对象
let arr = Array.from('hello'); // ['h','e','l','l','o']
let arr = Array.from(new Set(['a','b'])); // ['a','b']
# 方法:
数组原型提供了非常多的方法,这里分为三类来讲,一类会改变原数组的值,一类是不会改变原数组,以及数组的遍历方法。
# 改变原数组的方法(9个):
let a = [1,2,3];
ES5:
a.splice()/ a.sort() / a.pop()/ a.shift()/ a.push()/ a.unshift()/ a.reverse()
ES6:
a.copyWithin() / a.fill
对于这些能够改变原数组的方法,要注意避免在循环遍历中改变原数组的选项,比如: 改变数组的长度,导致遍历的长度出现问题。
# splice() 添加/删除数组元素
定义: splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目
语法: array.splice(index,howmany,item1,.....,itemX)
参数:
- index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
- howmany:可选。要删除的项目数量。如果设置为 0,则不会删除项目。
- item1, ..., itemX: 可选。向数组添加的新项目。
返回值: 如果有元素被删除,返回包含被删除项目的新数组。
eg1:删除元素
let a = [1, 2, 3, 4, 5, 6, 7];
let item = a.splice(0, 3); // [1,2,3]
console.log(a); // [4,5,6,7]
// 从数组下标0开始,删除3个元素
let item = a.splice(-1, 3); // [7]
// 从最后一个元素开始删除3个元素,因为最后一个元素,所以只删除了7
eg2: 删除并添加
let a = [1, 2, 3, 4, 5, 6, 7];
let item = a.splice(0,3,'添加'); // [1,2,3]
console.log(a); // ['添加',4,5,6,7]
// 从数组下标0开始,删除3个元素,并添加元素'添加'
let b = [1, 2, 3, 4, 5, 6, 7];
let item = b.splice(-2,3,'添加1','添加2'); // [6,7]
console.log(b); // [1,2,3,4,5,'添加1','添加2']
// 从数组最后第二个元素开始,删除3个元素,并添加两个元素'添加1'、'添加2'
eg3: 不删除只添加:
let a = [1, 2, 3, 4, 5, 6, 7];
let item = a.splice(0,0,'添加1','添加2'); // [] 没有删除元素,返回空数组
console.log(a); // ['添加1','添加2',1,2,3,4,5,6,7]
let b = [1, 2, 3, 4, 5, 6, 7];
let item = b.splice(-1,0,'添加1','添加2'); // [] 没有删除元素,返回空数组
console.log(b); // [1,2,3,4,5,6,'添加1','添加2',7] 在最后一个元素的前面添加两个元素
从上述三个栗子可以得出:
- 数组如果元素不够,会删除到最后一个元素为止
- 操作的元素,包括开始的那个元素
- 可以添加很多个元素
- 添加是在开始的元素前面添加的
# sort() 数组排序
定义: sort()方法对数组元素进行排序,并返回这个数组。
参数可选: 规定排序顺序的比较函数。
默认情况下sort()方法没有传比较函数的话,默认按字母升序,如果不是元素不是字符串的话,会调用toString()
方法将元素转化为字符串的Unicode(万国码)位点,然后再比较字符。
// 字符串排列 看起来很正常
let a = ["Banana", "Orange", "Apple", "Mango"];
a.sort(); // ["Apple","Banana","Mango","Orange"]
// 数字排序的时候 因为转换成Unicode字符串之后,有些数字会比较大会排在后面 这显然不是我们想要的
let a = [10, 1, 3, 20,25,8];
console.log(a.sort()) // [1,10,20,25,3,8];
比较函数的两个参数:
sort的比较函数有两个默认参数,要在函数中接收这两个参数,这两个参数是数组中两个要比较的元素,通常我们用 a 和 b 接收两个将要比较的元素:
- 若比较函数返回值<0,那么a将排到b的前面;
- 若比较函数返回值=0,那么a 和 b 相对位置不变;
- 若比较函数返回值>0,那么b 排在a 将的前面;
对于sort()方法更深层级的内部实现以及处理机制可以看一下这篇文章深入了解javascript的sort方法
sort排序常见用法:
- 数组元素为数字的升序、降序:
let array = [10, 1, 3, 4,20,4,25,8];
// 升序 a-b < 0 a将排到b的前面,按照a的大小来排序的
// 比如被减数a是10,减数是20 10-20 < 0 被减数a(10)在减数b(20)前面
array.sort(function(a,b){
return a-b;
});
console.log(array); // [1,3,4,4,8,10,20,25];
// 降序 被减数和减数调换了 20-10>0 被减数b(20)在减数a(10)的前面
array.sort(function(a,b){
return b-a;
});
console.log(array); // [25,20,10,8,4,4,3,1];
- 数组多条件排序
let array = [{id:10,age:2},{id:5,age:4},{id:6,age:10},{id:9,age:6},{id:2,age:8},{id:10,age:9}];
array.sort(function(a,b){
if(a.id === b.id){// 如果id的值相等,按照age的值降序
return b.age - a.age
}else{ // 如果id的值不相等,按照id的值升序
return a.id - b.id
}
})
// [{"id":2,"age":8},{"id":5,"age":4},{"id":6,"age":10},{"id":9,"age":6},{"id":10,"age":9},{"id":10,"age":2}]
- 自定义比较函数,天空才是你的极限
类似的:运用好返回值,我们可以写出任意符合自己需求的比较函数
let array = [{name:'Koro1'},{name:'Koro1'},{name:'OB'},{name:'Koro1'},{name:'OB'},{name:'OB'}];
array.sort(function(a,b){
if(a.name === 'Koro1'){// 如果name是'Koro1' 返回-1 ,-1<0 a排在b的前面
return -1
}else{ // 如果不是的话,a排在b的后面
return 1
}
})
// [{"name":"Koro1"},{"name":"Koro1"},{"name":"Koro1"},{"name":"OB"},{"name":"OB"},{"name":"OB"}]
# pop() 删除一个数组中的最后的一个元素
定义: pop() 方法删除一个数组中的最后的一个元素,并且返回这个元素。
参数: 无。
let a = [1,2,3];
let item = a.pop(); // 3
console.log(a); // [1,2]
# shift() 删除数组的第一个元素
定义: shift()方法删除数组的第一个元素,并返回这个元素。
参数: 无。
let a = [1,2,3];
let item = a.shift(); // 1
console.log(a); // [2,3]
# push() 向数组的末尾添加元素
定义:push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。
参数: item1, item2, ..., itemX ,要添加到数组末尾的元素
let a = [1,2,3];
let item = a.push('末尾'); // 4
console.log(a); // [1,2,3,'末尾']
# unshift()
定义:unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
参数: item1, item2, ..., itemX ,要添加到数组开头的元素
let a = [1,2,3];
let item = a.unshift('开头'); // 4
console.log(a); // ['开头',1,2,3]
# reverse() 颠倒数组中元素的顺序
定义: reverse() 方法用于颠倒数组中元素的顺序。
参数: 无
let a = [1,2,3];
a.reverse();
console.log(a); // [3,2,1]
# ES6: copyWithin() 指定位置的成员复制到其他位置
定义: 在当前数组内部,将指定位置的成员复制到其他位置,并返回这个数组。
语法:
array.copyWithin(target, start = 0, end = this.length)
参数:
三个参数都是数值,如果不是,会自动转为数值.
- target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
- end(可选):到该位置前停止读取数据,默认等于数组长度。使用负数可从数组结尾处规定位置。
浏览器兼容(MDN): chrome 45,Edge 12,Firefox32,Opera 32,Safari 9, IE 不支持
eg:
// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
let a=['OB1','Koro1','OB2','Koro2','OB3','Koro3','OB4','Koro4','OB5','Koro5']
// 2位置开始被替换,3位置开始读取要替换的 5位置前面停止替换
a.copyWithin(2,3,5)
// ["OB1","Koro1","Koro2","OB3","OB3","Koro3","OB4","Koro4","OB5","Koro5"]
从上述栗子:
- 第一个参数是开始被替换的元素位置
- 要替换数据的位置范围:从第二个参数是开始读取的元素,在第三个参数前面一个元素停止读取
- 数组的长度不会改变
- 读了几个元素就从开始被替换的地方替换几个元素
# ES6: fill() 填充数组
定义: 使用给定值,填充一个数组。
参数:
第一个元素(必须): 要填充数组的值
第二个元素(可选): 填充的开始位置,默认值为0
第三个元素(可选):填充的结束位置,默认是为this.length
MDN浏览器兼容
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
# 不改变原数组的方法(8个):
ES5:
slice、join、toLocateString、toStrigin、cancat、indexOf、lastIndexOf、
ES7:
includes
# slice() 浅拷贝数组的元素
定义: 方法返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象,且原数组不会被修改。
注意:字符串也有一个slice() 方法是用来提取字符串的,不要弄混了。
语法:
参数:
begin(可选): 索引数值,接受负值,从该索引处开始提取原数组中的元素,默认值为0。
end(可选):索引数值(不包括),接受负值,在该索引处前结束提取原数组元素,默认值为数组末尾(包括最后一个元素)。
let a= ['hello','world'];
let b=a.slice(0,1); // ['hello']
a[0]='改变原数组';
console.log(a,b); // ['改变原数组','world'] ['hello']
b[0]='改变拷贝的数组';
console.log(a,b); // ['改变原数组','world'] ['改变拷贝的数组']
如上:新数组是浅拷贝的,元素是简单数据类型,改变之后不会互相干扰。
如果是复杂数据类型(对象,数组)的话,改变其中一个,另外一个也会改变。
let a= [{name:'OBKoro1'}];
let b=a.slice();
console.log(b,a); // [{"name":"OBKoro1"}] [{"name":"OBKoro1"}]
// a[0].name='改变原数组';
// console.log(b,a); // [{"name":"改变原数组"}] [{"name":"改变原数组"}]
// b[0].name='改变拷贝数组',b[0].koro='改变拷贝数组';
// [{"name":"改变拷贝数组","koro":"改变拷贝数组"}] [{"name":"改变拷贝数组","koro":"改变拷贝数组"}]
原因在定义上面说过了的:slice()是浅拷贝,对于复杂的数据类型浅拷贝,拷贝的只是指向原数组的指针,所以无论改变原数组,还是浅拷贝的数组,都是改变原数组的数据。
# join() 数组转字符串
定义: join() 方法用于把数组中的所有元素通过指定的分隔符进行分隔放入一个字符串,返回生成的字符串。
语法:
参数:
str(可选): 指定要使用的分隔符,默认使用逗号作为分隔符。
let a= ['hello','world'];
let str=a.join(); // 'hello,world'
let str2=a.join('+'); // 'hello+world'
使用join方法或者下文说到的toString方法时,当数组中的元素也是数组或者是对象时会出现什么情况?
let a= [['OBKoro1','23'],'test'];
let str1=a.join(); // OBKoro1,23,test
let b= [{name:'OBKoro1',age:'23'},'test'];
let str2 = b.join(); // [object Object],test
// 对象转字符串推荐JSON.stringify(obj);
所以,join()/toString()
方法在数组元素是数组的时候,会将里面的数组也调用join()/toString()
,如果是对象的话,对象会被转为[object Object]
字符串。
# toLocaleString() 数组转字符串
定义: 返回一个表示数组元素的字符串。该字符串由数组中的每个元素的 toLocaleString() 返回值经调用 join() 方法连接(由逗号隔开)组成。
语法:
参数:无。
let a=[{name:'OBKoro1'},23,'abcd',new Date()];
let str=a.toLocaleString(); // [object Object],23,abcd,2018/5/28 下午1:52:20
如上述栗子:调用数组的toLocaleString
方法,数组中的每个元素都会调用自身的toLocaleString
方法,对象调用对象的toLocaleString
,Date调用Date的toLocaleString
。
# toString() 数组转字符串 不推荐
定义: toString() 方法可把数组转换为由逗号链接起来的字符串。
语法:
参数: 无。
该方法的效果和join方法一样,都是用于数组转字符串的,但是与join方法相比没有优势,也不能自定义字符串的分隔符,因此不推荐使用。
值得注意的是:当数组和字符串操作的时候,js 会调用这个方法将数组自动转换成字符串
let b= [ 'toString','演示'].toString(); // toString,演示
let a= ['调用toString','连接在我后面']+'啦啦啦'; // 调用toString,连接在我后面啦啦啦
# cancat
定义: 方法用于合并两个或多个数组,返回一个新数组。
语法:
let newArr =oldArray.concat(arrayX,arrayX,......,arrayX)
参数:
arrayX(必须):该参数可以是具体的值,也可以是数组对象。可以是任意多个。
eg1:
let a = [1, 2, 3];
let b = [4, 5, 6];
//连接两个数组
let newVal=a.concat(b); // [1,2,3,4,5,6]
// 连接三个数组
let c = [7, 8, 9]
let newVal2 = a.concat(b, c); // [1,2,3,4,5,6,7,8,9]
// 添加元素
let newVal3 = a.concat('添加元素',b, c,'再加一个');
// [1,2,3,"添加元素",4,5,6,7,8,9,"再加一个"]
// 合并嵌套数组 会浅拷贝嵌套数组
let d = [1,2 ];
let f = [3,[4]];
let newVal4 = d.concat(f); // [1,2,3,[4]]
ES6扩展运算符...
合并数组:
因为ES6的语法更简洁易懂,所以现在合并数组我大部分采用...
来处理,...
运算符可以实现cancat
的每个栗子,且更简洁和具有高度自定义数组元素位置的效果。
let a = [2, 3, 4, 5]
let b = [ 4,...a, 4, 4]
console.log(a,b); // [2, 3, 4, 5] [4,2,3,4,5,4,4]
更多关于扩展符的详细内容移步阮一峰大神的ECMAScript 6 入门
# indexOf() 查找数组是否存在某个元素,返回下标
定义: 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
语法:
array.indexOf(searchElement,fromIndex)
参数:
searchElement(必须):被查找的元素
fromIndex(可选):开始查找的位置(不能大于等于数组的长度,返回-1),接受负值,默认值为0。
严格相等的搜索:
数组的indexOf搜索跟字符串的indexOf不一样,数组的indexOf使用严格相等===
搜索元素,即数组元素要完全匹配才能搜索成功。
注意:indexOf()不能识别NaN
eg:
let a=['啦啦',2,4,24,NaN]
console.log(a.indexOf('啦')); // -1
console.log(a.indexOf('NaN')); // -1
console.log(a.indexOf('啦啦')); // 0
使用场景:
- 数组去重
- 根据获取的数组下标执行操作,改变数组中的值等。
- 判断是否存在,执行操作。
# lastIndexOf() 查找指定元素在数组中的最后一个位置
定义: 方法返回指定元素,在数组中的最后一个的索引,如果不存在则返回 -1。(从数组后面往前查找)
语法:
arr.lastIndexOf(searchElement,fromIndex)
参数:
searchElement(必须): 被查找的元素
fromIndex(可选): 逆向查找开始位置,默认值数组的长度-1,即查找整个数组。
关于fromIndex有三个规则:
- 正值。如果该值大于或等于数组的长度,则整个数组会被查找。
- 负值。将其视为从数组末尾向前的偏移。(比如-2,从数组最后第二个元素开始往前查找)
- 负值。其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。
let a=['OB',4,'Koro1',1,2,'Koro1',3,4,5,'Koro1']; // 数组长度为10
// let b=a.lastIndexOf('Koro1',4); // 从下标4开始往前找 返回下标2
// let b=a.lastIndexOf('Koro1',100); // 大于或数组的长度 查找整个数组 返回9
// let b=a.lastIndexOf('Koro1',-11); // -1 数组不会被查找
let b=a.lastIndexOf('Koro1',-9); // 从第二个元素4往前查找,没有找到 返回-1
# ES7 includes() 查找数组是否包含某个元素 返回布尔
定义: 返回一个布尔值,表示某个数组是否包含给定的值
语法:
array.includes(searchElement,fromIndex=0)
参数:
searchElement(必须):被查找的元素
fromIndex(可选):默认值为0,参数表示搜索的起始位置,接受负值。正值超过数组长度,数组不会被搜索,返回false。负值绝对值超过长数组度,重置从0开始搜索。
includes方法是为了弥补indexOf方法的缺陷而出现的:
- indexOf方法不能识别
NaN
- indexOf方法检查是否包含某个值不够语义化,需要判断是否不等于
-1
,表达不够直观
eg:
let a=['OB','Koro1',1,NaN];
// let b=a.includes(NaN); // true 识别NaN
// let b=a.includes('Koro1',100); // false 超过数组长度 不搜索
// let b=a.includes('Koro1',-3); // true 从倒数第三个元素开始搜索
// let b=a.includes('Koro1',-100); // true 负值绝对值超过数组长度,搜索整个数组
兼容性(MDN): chrome47, Firefox 43,Edge 14,Opera 34, Safari 9,IE 未实现。
# 遍历方法(12个):
js中遍历数组并不会改变原始数组的方法总共有12个:
ES5:
forEach、every 、some、 filter、map、reduce、reduceRight、
ES6:
find、findIndex、keys、values、entries
# 关于遍历:
- 关于遍历的效率,可以看一下这篇详解JS遍历
- 尽量不要在遍历的时候,修改后面要遍历的值
- 尽量不要在遍历的时候修改数组的长度(删除/添加)
# forEach
定义: 按升序为数组中含有效值的每一项执行一次回调函数。
语法:
array.forEach(function(currentValue, index, arr), thisValue)
参数:
function(必须): 数组中每个元素需要调用的函数。
// 回调函数的参数
1. currentValue(必须),数组当前元素的值
2. index(可选), 当前元素的索引值
3. arr(可选),数组对象本身
thisValue(可选): 当执行回调函数时this绑定对象的值,默认值为undefined
关于forEach()你要知道:
- 无法中途退出循环,只能用
return
退出本次回调,进行下一次回调。
- 它总是返回 undefined值,即使你return了一个值。
# 下面类似语法同样适用这些规则
- 对于空数组是不会执行回调函数的
- 对于已在迭代过程中删除的元素,或者空元素会跳过回调函数
- 遍历次数再第一次循环前就会确定,再添加到数组中的元素不会被遍历。
- 如果已经存在的值被改变,则传递给 callback 的值是遍历到他们那一刻的值。
eg:
let a = [1, 2, ,3]; // 最后第二个元素是空的,不会遍历(undefined、null会遍历)
let obj = { name: 'OBKoro1' };
let result = a.forEach(function (value, index, array) {
a[3] = '改变元素';
a.push('添加到尾端,不会被遍历')
console.log(value, 'forEach传递的第一个参数'); // 分别打印 1 ,2 ,改变元素
console.log(this.name); // OBKoro1 打印三次 this绑定在obj对象上
// break; // break会报错
return value; // return只能结束本次回调 会执行下次回调
console.log('不会执行,因为return 会执行下一次循环回调')
}, obj);
console.log(result); // 即使return了一个值,也还是返回undefined
// 回调函数也接受接头函数写法
# every 检测数组所有元素是否都符合判断条件
定义: 方法用于检测数组所有元素是否都符合函数定义的条件
语法:
array.every(function(currentValue, index, arr), thisValue)
参数:(这几个方法的参数,语法都类似)
function(必须): 数组中每个元素需要调用的函数。
- currentValue(必须),数组当前元素的值
- index(可选), 当前元素的索引值
- arr(可选),数组对象本身
thisValue(可选): 当执行回调函数时this绑定对象的值,默认值为undefined
方法返回值规则:
- 如果数组中检测到有一个元素不满足,则整个表达式返回 false,且剩余的元素不会再进行检测。
- 如果所有元素都满足条件,则返回 true。=
eg:
function isBigEnough(element, index, array) {
return element >= 10; // 判断数组中的所有元素是否都大于10
}
let result = [12, 5, 8, 130, 44].every(isBigEnough); // false
let result = [12, 54, 18, 130, 44].every(isBigEnough); // true
// 接受箭头函数写法
[12, 5, 8, 130, 44].every(x => x >= 10); // false
[12, 54, 18, 130, 44].every(x => x >= 10); // true
# some 数组中的是否有满足判断条件的元素
定义:数组中的是否有满足判断条件的元素
语法:
array.some(function(currentValue, index, arr), thisValue)
参数:(这几个方法的参数,语法都类似)
function(必须): 数组中每个元素需要调用的函数。
- currentValue(必须),数组当前元素的值
- index(可选), 当前元素的索引值
- arr(可选),数组对象本身
thisValue(可选): 当执行回调函数时this绑定对象的值,默认值为undefined
方法返回值规则:
- 如果有一个元素满足条件,则表达式返回true, 剩余的元素不会再执行检测。
- 如果没有满足条件的元素,则返回false。
function isBigEnough(element, index, array) {
return (element >= 10); //数组中是否有一个元素大于 10
}
let result = [2, 5, 8, 1, 4].some(isBigEnough); // false
let result = [12, 5, 8, 1, 4].some(isBigEnough); // true
# filter 过滤原始数组,返回新数组
定义: 返回一个新数组, 其包含通过所提供函数实现的测试的所有元素。
语法:
let new_array = arr.filter(function(currentValue, index, arr), thisArg)
参数:(这几个方法的参数,语法都类似)
function(必须): 数组中每个元素需要调用的函数。
- currentValue(必须),数组当前元素的值
- index(可选), 当前元素的索引值
- arr(可选),数组对象本身
thisValue(可选): 当执行回调函数时this绑定对象的值,默认值为undefined
eg:
let a = [32, 33, 16, 40];
let result = a.filter(function (value, index, array) {
return value >= 18; // 返回a数组中所有大于18的元素
});
console.log(result,a);// [32,33,40] [32,33,16,40]
# map 对数组中的每个元素进行处理,返回新的数组
定义:创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
语法:
let new_array = arr.map(function(currentValue, index, arr), thisArg)
参数:(这几个方法的参数,语法都类似)
function(必须): 数组中每个元素需要调用的函数。
- currentValue(必须),数组当前元素的值
- index(可选), 当前元素的索引值
- arr(可选),数组对象本身
thisValue(可选): 当执行回调函数时this绑定对象的值,默认值为undefined
eg:
let a = ['1','2','3','4'];
let result = a.map(function (value, index, array) {
return value + '新数组的新元素'
});
console.log(result, a);
// ["1新数组的新元素","2新数组的新元素","3新数组的新元素","4新数组的新元素"] ["1","2","3","4"]
# reduce 为数组提供累加器,合并为一个值
定义:reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,最终合并为一个值。
语法:
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
参数:
function(必须): 数组中每个元素需要调用的函数。
- total(必须),初始值, 或者上一次调用回调返回的值
- currentValue(必须),数组当前元素的值
- index(可选), 当前元素的索引值
- arr(可选),数组对象本身
initialValue(可选): 指定第一次回调 的第一个参数。
回调第一次执行时:
- 如果 initialValue 在调用 reduce 时被提供,那么第一个 total 将等于 initialValue,此时 currentValue 等于数组中的第一个值;
- 如果 initialValue 未被提供,那么 total 等于数组中的第一个值,currentValue 等于数组中的第二个值。此时如果数组为空,那么将抛出 TypeError。
- 如果数组仅有一个元素,并且没有提供 initialValue,或提供了 initialValue 但数组为空,那么回调不会被执行,数组的唯一值将被返回。
eg:
// 数组求和
let sum = [0, 1, 2, 3].reduce(function (a, b) {
return a + b;
}, 0);
// 6
// 将二维数组转化为一维 将数组元素展开
let flattened = [[0, 1], [2, 3], [4, 5]].reduce(
(a, b) => a.concat(b),
[]
);
// [0, 1, 2, 3, 4, 5]
# reduceRight 从右至左累加
这个方法除了与reduce执行方向相反外,其他完全与其一致,请参考上述 reduce 方法介绍。
# ES6:find()& findIndex() 根据条件找到数组成员
find()定义:用于找出第一个符合条件的数组成员,并返回该成员,如果没有符合条件的成员,则返回undefined。
findIndex()定义:返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
这两个方法
语法:
let item = arr.find(function(currentValue, index, arr), thisArg)
let index = arr.findIndex(function(currentValue, index, arr), thisArg)
参数:(这几个方法的参数,语法都类似)
function(必须): 数组中每个元素需要调用的函数。
- currentValue(必须),数组当前元素的值
- index(可选), 当前元素的索引值
- arr(可选),数组对象本身
thisValue(可选): 当执行回调函数时this绑定对象的值,默认值为undefined
这两个方法都可以识别NaN
,弥补了indexOf
的不足.
eg:
// find
let a = [1, 4, -5, 10].find((n) => n < 0); // 返回元素-5
let b = [1, 4, -5, 10,NaN].find((n) => Object.is(NaN, n)); // 返回元素NaN
// findIndex
let a = [1, 4, -5, 10].findIndex((n) => n < 0); // 返回索引2
let b = [1, 4, -5, 10,NaN].findIndex((n) => Object.is(NaN, n)); // 返回索引4
浏览器兼容(MDN):Chrome 45,Firefox 25,Opera 32, Safari 8, Edge yes,
# ES6 keys()&values()&entries() 遍历键名、遍历键值、遍历键名+键值
定义:三个方法都返回一个新的 Array Iterator 对象,对象根据方法不同包含不同的值。
语法:
array.keys()
array.values()
array.entries()
参数:无。
遍历栗子(摘自ECMAScript 6 入门):
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
在for..of
中如果遍历中途要退出,可以使用break
退出循环。
如果不使用for...of
循环,可以手动调用遍历器对象的next方法,进行遍历:
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
entries()浏览器兼容性(MDN):Chrome 38, Firefox 28,Opera 25,Safari 7.1
keys()浏览器兼容性(MDN):Chrome 38, Firefox 28,Opera 25,Safari 8,
博客链接
# js 调用栈机制与ES6尾调用优化介绍
调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快!
博客、前端积累文档、公众号、GitHub
# 目录
- 数据结构:栈
- 调用栈是什么?用来做什么?
- 调用栈的运行机制
- 调用栈优化内存
- 调用栈debug大法
# 数据结构:栈
栈是一种遵从后进先出(LIFO
)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。
生活中的栗子,帮助一下理解:
餐厅里面堆放的盘子(栈),一开始放的都在下面(先进),后面放的都在上面(后进),洗盘子的时候先从上面开始洗(先出)。
# 调用栈是什么?用来做什么?
- 调用栈是一种栈结构的数据,它是由调用侦组成的。
- 调用栈记录了函数的执行顺序和函数内部变量等信息。
# 调用栈的运行机制
机制:
程序运行到一个函数,它就会将其添加到调用栈中,当从这个函数返回的时候,就会将这个函数从调用栈中删掉。
看一下例子帮助理解:
// 调用栈中的执行步骤用数字表示
printSquare(5); // 1 添加
function printSquare(x) {
var s = multiply(x, x); // 2 添加 => 3 运行完成,内部没有再调用其他函数,删掉
console.log(s); // 4 添加 => 5 删掉
// 运行完成 删掉printSquare
}
function multiply(x, y) {
return x * y;
}
调用栈中的执行步骤如下(删除multiply的步骤被省略了):
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/16acb4a439190d49?raw=true)
调用侦:
每个进入到调用栈中的函数,都会分配到一个单独的栈空间,称为“调用侦”。
在调用栈中每个“调用侦”都对应一个函数,最上方的调用帧称为“当前帧”,调用栈是由所有的调用侦形成的。
找到一张图片,调用侦:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/16ace8030a36c8dd?raw=true)
# 调用栈优化内存
调用栈的内存消耗:
如上图,函数的变量等信息会被调用侦保存起来,所以调用侦中的变量不会被垃圾收集器回收。
当函数嵌套的层级比较深了,调用栈中的调用侦比较多的时候,这些信息对内存消耗是非常大的。
针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,ES6提供了“尾调用优化”来解决调用侦过多,引起的内存消耗过大的问题。
何谓尾调用:
尾调用指的是:函数的最后一步是调用另一个函数。
function f(x){
return g(x); // 最后一步调用另一个函数并且使用return
}
function f(x){
g(x); // 没有return 不算尾调用 因为不知道后面还有没有操作
// return undefined; // 隐式的return
}
尾调用优化优化了什么?
尾调用用来删除外层无用的调用侦,只保留内层函数的调用侦,来节省浏览器的内存。
下面这个例子调用栈中的调用侦一直只有一项,如果不使用尾调用的话会出现三个调用侦:
a() // 1 添加a到调用栈
function a(){
return b(); // 在调用栈中删除a 添加b
}
function b(){
return c() // 删除b 添加c
}
防止爆栈:
浏览器对调用栈都有大小限制,在ES6之前递归比较深的话,很容易出现“爆栈”问题(stack overflow)。
现在可以使用“尾调用优化”来写一个“尾递归”,只保存一个调用侦,来防止爆栈问题。
注意:
- 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧。
如果要使用外层函数的变量,可以通过参数的形式传到内层函数中
function a(){
var aa = 1;
let b = val => aa + val // 使用了外层函数的参数aa
return b(2) // 无法进行尾调用优化
}
- 尾调用优化只在严格模式下开启,非严格模式是无效的。
- 如果环境不支持“尾调用优化”,代码还可以正常运行,是无害的!
更多:
关于尾递归以及更多尾调用优化的内容,推荐查阅ES6入门-阮一峰
# 调用栈debug大法
查看调用栈有什么用
-
查看函数的调用顺序是否跟预期一致,比如不同判断调用不同函数。
-
快速定位问题/修改三方库的代码。
当接手一个历史项目,或者引用第三方库出现问题的时候,可以先查看对应API的调用栈,找到其中涉及的关键函数,针对性的修复它。
通过查看调用栈的形式,帮助我快速定位问题,修改三方库的源码。
如何查看调用栈
- 只查看调用栈:
console.trace
a()
function a() {
b();
}
function b() {
c()
}
function c() {
let aa = 1;
console.trace()
}
如图所示,点击右侧还能查看代码位置:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/16ad3f508ab127a9?raw=true)
bugger
打断点形式,这也是我最喜欢的调试方式:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/16ad3ff354f2dac3?raw=true)
# 结语
本文主要讲了这几个方面的内容:
- 理解调用栈的运行机制,对代码背后的一些执行机制也可以更加了解,帮助我们在百尺竿头更进一步。
- 我们应该在日常的code中,有意识的使用ES6的“尾调用优化”,来减少调用栈的长度,节省客户端内存。
- 利用调用栈,对第三方库或者不熟悉的项目,可以更快速的定位问题,提高我们debug速度。
最后:之前写过一篇关于垃圾回收机制与内存泄露的文章,感兴趣的同学可以扩展一下。
以上2019/5/19
参考资料:
JS垃圾回收机制与常见内存泄露的解决方法
ES6入门-阮一峰
JavaScript 如何工作:对引擎、运行时、调用堆栈的概述
浅析javascript调用栈
博客链接
# 简单的弹窗组件实现
最近在使用element-ui框架,用到了Dialog对话框组件,大致实现的效果,跟我之前自己在移动端项目里面弄的一个弹窗组件差不太多。然后就想着把这种弹窗组件的实现方式与大家分享一下,下面本文会带着大家手摸手实现一个弹窗组件。
本文主要内容会涉及到弹窗遮罩的实现,slot
插槽的使用方式,props
、$emit
传参,具体组件代码也传上去了。如果喜欢的话可以点波赞/关注,支持一下,希望大家看完本文可以有所收获。
# 组件最后实现的效果
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/162e6f9251c7c18f?raw=true)
# 实现步骤
- 先搭建组件的html和css样式,遮罩层和内容层。
- 定制弹窗内容:弹窗组件通过
slot
插槽接受从父组件那里传过来弹窗内容。
- 定制弹窗样式:弹窗组件通过
props
接收从父组件传过来的弹窗宽度,上下左右的位置。
- 组件开关:通过父组件传进来的
props
控制组件的显示与隐藏,子组件关闭时通过事件$emit
触发父组件改变值。
# 1.搭建组件的html和css样式。
html结构:一层遮罩层,一层内容层,内容层里面又有一个头部title和主体内容和一个关闭按钮。
下面是组件中的html结构,里面有一些后面才要加进去的东西,如果看不懂的话可以先跳过,
<template>
<div class="dialog">
<!--外层的遮罩 点击事件用来关闭弹窗,isShow控制弹窗显示 隐藏的props-->
<div class="dialog-cover back" v-if="isShow" @click="closeMyself"></div>
<!-- transition 这里可以加一些简单的动画效果 -->
<transition name="drop">
<!--style 通过props 控制内容的样式 -->
<div class="dialog-content" :style="{top:topDistance+'%',width:widNum+'%',left:leftSite+'%'}" v-if="isShow">
<div class="dialog_head back">
<!--弹窗头部 title-->
<slot name="header">提示信息</slot>
</div>
<div class="dialog_main" :style="{paddingTop:pdt+'px',paddingBottom:pdb+'px'}">
<!--弹窗的内容-->
<slot name="main">弹窗内容</slot>
</div>
<!--弹窗关闭按钮-->
<div class="foot_close" @click="closeMyself">
<div class="close_img back"></div>
</div>
</div>
</transition>
</div>
</template>
下面是组件中的主要的css样式,里面都做了充分的注释,主要通过z-index
和background
达到遮罩的效果,具体内容的css可以根据自己的需求来设置。
<style lang="scss" scoped>
/* 最外层 设置position定位 */
.dialog {
position: relative;
color: #2e2c2d;
font-size: 16px;
}
/* 遮罩 设置背景层,z-index值要足够大确保能覆盖,高度 宽度设置满 做到全屏遮罩 */
.dialog-cover {
background: rgba(0,0,0, 0.8);
position: fixed;
z-index: 200;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 内容层 z-index要比遮罩大,否则会被遮盖, */
.dialog-content{
position: fixed;
top: 35%;
/* 移动端使用felx布局 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 300;
}
</style>
# 2. 通过slot
定制弹窗内容
这一步,只要理解了slot
的作用以及用法,就没有问题了。
# 单个插槽:
<slot>这是在没有slot传进来的时候,才显示的弹窗内容</slot>
上面是单个插槽也叫默认插槽,在父组件中使用插槽的正确姿势:
<my-component>
<!--在my-component里面的所有内容片段都将插入到slot所在的DOM位置,并且会替换掉slot标签-->
<!--这两个p标签,将替换整个slot标签里面的内容-->
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
ps:如果子组件里面包含slot
插槽,那么上面的p标签的内容将会被丢弃。
# 具名插槽:
所谓的具名插槽,即为slot
标签赋一个name
属性,具名插槽可以父组件中不同的内容片段放到子组件的不同地方,具名插槽还是可以拥有一个默认插槽。下面可以看一下弹窗组件插槽的使用方式:
<div class="dialog_head back ">
<!--弹窗头部 title-->
<slot name="header">提示信息</slot>
</div>
<div class="dialog_main " :style="{paddingTop:pdt+'px',paddingBottom:pdb+'px'}">
<!--弹窗的内容-->
<slot name="main">弹窗内容</slot>
</div>
在父组件中的使用方式:
- 将弹窗组件引入要使用的组件中,并通过
components
注册成为组件。
- 父组件中弹窗组件插槽的使用方法如下。
<dialogComponent>
<div slot="header">插入到name为header的slot标签里面</div>
<div class="dialog_publish_main" slot="main">
这里是内容插入到子组件的slot的name为main里面,可以在父组件中添加class定义样式,事件类型等各种操作
</div>
</dialogComponent>
关于组件中用到的插槽的介绍就到这里了,插槽在弹窗组件中的应用是一个典型的栗子,可以看到插槽作用相当强大,而插槽本身的使用并不难,同学们爱上插槽了没有?
# 3.通过props
控制弹窗显隐&&定制弹窗style
psops
是Vue中父组件向子组件传递数据的一种方式,不熟悉的小伙伴们可以看一下props文档。
因为弹窗组件都是引到别的组件里面去用的,为了适合不同组件场景中的弹窗,所以弹窗组件必须具备一定的可定制性,否则这样的组件将毫无意义,下面介绍一下props的使用方式,以弹窗组件为例:
- 首先需要在被传入的组件中定义props的一些特性,验证之类的。
- 然后在父组件中绑定props数据。
<script>
export default {
props: {
isShow: {
//弹窗组件是否显示 默认不显示
type: Boolean,
default: false,
required:true, //必须
},
//下面这些属性会绑定到div上面 详情参照上面的html结构
// 如: :style="{top:topDistance+'%',width:widNum+'%'}"
widNum:{
//内容宽度
type: Number,
default:86.5
},
leftSite:{
// 左定位
type: Number,
default:6.5
},
topDistance: {
//top上边距
type: Number,
default:35
},
pdt:{
//上padding
type: Number,
default:22
},
pdb:{
//下padding
type: Number,
default:47
}
},
}
</script>
父组件中使用方式:
<dialogComponent :is-show="status.isShowPublish" :top-distance="status.topNum">
</dialogComponent>
ps:props传递数据不是双向绑定的,而是单向数据流,父组件的数据变化时,也会传递到子组件中,这就意外着我们不应该在子组件中修改props。所以我们在关闭弹窗的时候就需要通过$emit
来修改父组件的数据,然后数据会自动传到子组件中。
现在基本上弹窗组件都已实现的差不多了,还差一个弹窗的关闭事件,这里就涉及到子组件往父组件传参了。
# 4.$emit
触发父组件事件修改数据,关闭弹窗
Vue中在子组件往父组件传参,很多都是通过$emit
来触发父组件的事件来修改数据。
在子组件中,在点击关闭,或者遮罩层的时候触发下面这个方法:
methods: {
closeMyself() {
this.$emit("on-close");
//如果需要传参的话,可以在"on-close"后面再加参数,然后在父组件的函数里接收就可以了。
}
}
父组件中的写法:
<dialogComponent :is-show="status.isShowPublish" :top-distance="status.topNum" @on-close="closeDialog">
</dialogComponent>
//"on-close是监听子组件的时间有没有触发,触发的时候执行closeDialog函数
methods:{
closeDialog(){
// this.status.isShowPublish=false;
//把绑定的弹窗数组 设为false即可关闭弹窗
},
}
可以用弹窗组件实现下列这种信息展示,或者事件交互:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/162e7a7af2e0effa?raw=true)
上面是把弹窗的每个步骤拆分开来,一步步解析的,每一步都说的比较清楚了,具体连起来的话,可以看看代码,再结合文章就能理的很清楚了。
# 小结:
这个弹窗组件,实现起来一点都不难,我这里主要是提供了一个实现方式,当项目中有需要的话,大家也可以自己撸一个出来,以上就是本文的内容了,希望同学们看完能有所收获。
以上2018.4.21
# 参考资料:
Vue文档-插槽
博客链接
# 推箱子
![demo图片](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/1?raw=true)
# 步骤解析:
本文代码已经放在了github上面了,里面也进行了很详细的代码注释,可以copy下来,在本地运行一下看看。
# 1. 渲染地图
-
html结构:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/160d1149856a3714?raw=true)
html结构十分简单,只要弄一堆div,来放置地图的class就可以了,我这里初始化了12*9个div,地图里最多九行高度。
这些div都是同样大小,地图渲染出来区别的只是颜色的不同。
-
地图函数:
var box=$('.box div'); //地图使用的div集合
function create(){ //创建地图函数
box.each(function(index){ //index的数量是固定的,是box div下面div的数量
// 每次创建地图初始化div
box.eq(index).removeClass();
});
box.each(function(index,element){ //循环整个div的数量 二维数组里数量不够的 默认为空白
//level为关卡数 根据关卡渲染地图 builder为二维数组,为地图关卡
if(builder[level][index]){ //过滤0
box.eq(index).addClass('type'+builder[level][index]);
}
});
box.eq(origin[level]).addClass("pusher"); //推箱人 皮卡丘位置
}
//第一关的地图长这样(下面只是栗子,不是代码),0代表不可抵达区域,1代表目标(要被推到的地方),
//2代表普通路径(可以走的),3代表墙,4代表箱子
[0,0,0,0,3,3,3,0,0,0,0,0,
0,0,0,0,3,1,3,0,0,0,0,0,
0,0,0,0,3,2,3,3,3,3,0,0,
0,0,3,3,3,4,2,4,1,3,0,0,
0,0,3,1,2,4,2,3,3,3,0,0,
0,0,3,3,3,3,4,3,0,0,0,0,
0,0,0,0,0,3,1,3,0,0,0,0,
0,0,0,0,0,3,3,3,0,0,0,0]
# 2. 捕获键盘事件,判断是否可以移动
使用$(document).keydown()jqery事件,捕获键盘事件。
- 捕获键盘事件,上下左右以及wsad。
$(document).keydown(function (e) {
var key=e.which;
switch(key){
//col 的值为12,上下移动要12个div为一个周期
//方向键上或者w
case 87:
case 38:
move(-col);//判断移动函数
break;
//方向键下或者s
case 83:
case 40:
move(col);
break;
//方向键左或者a
case 65:
case 37:
move(-1);
break;
//方向键右或者d
case 68:
case 39:
move(1);
break;
}
setTimeout(win,500); //按键之后调判断是否过关
});
- 判断是否可以移动。
分为两个判断条件:一个是推箱子,一个是不推箱子 自然移动,否则不移动皮卡丘。
function move(step){ //是否移动判断
// 分为两个判断条件一个是推箱子,一个是不推箱子 自然移动。 否则不移动皮卡丘
//step 上下是12个div一个周期,左右是1个div positin是皮卡丘的原来位置
var pikaqiu1=box.eq(position);//皮卡丘现在的地方
var pikaqiu2=box.eq(position+step);//皮卡丘要去的下一个地方
var pushBox=box.eq(position+step*2);//箱子要去的下一个地方
if(!pikaqiu2.hasClass('type4')&&( pikaqiu2.hasClass('type1')||pikaqiu2.hasClass('type2'))){ //自然移动
//判断:如果下一个div的class不包含type4(箱子),并且 下一个div含有type1(目标位置),或者type2(普通路径)
//这一步和下一步判断是否有type4的原因是普通路径会变成有type4的路径,这时候就会出现问题
pikaqiu1.removeClass("pusher"); //移除当前皮卡丘
pikaqiu2.addClass("pusher");//移动皮卡丘到下一个位置
position=position+step;//增加position值
}
else if((pikaqiu2.hasClass('type4'))&&(!pushBox.hasClass('type4'))&&(pushBox.hasClass('type1')|| pushBox.hasClass('type2')) ) {
//推箱子
//如果下一个div的class包含type4(箱子)并且 不包含重叠type4(箱子) 并且 包含class type1(目标位置)或者 包含type2(空路)
pikaqiu2.removeClass('type4');//移除当前箱子
pikaqiu1.removeClass("pusher");//移除当前皮卡丘
pushBox.addClass('type4');//移动箱子到下一个位置
pikaqiu2.addClass("pusher").addClass("type2");//
//本来是type4 移除之后,这里没有class了,要变成普通路径
position=position+step;//增加position值
}
}
# 3.胜利判断:
每次移动都要调用这个胜利判断。
function win(){ //胜利条件判断
if($(".type1.type4").length===goal){ //推的箱子与关卡设置通过箱子的数量对比
if(level<9) {
alert("666,挑战下一关吧--OBKoro1");
level++; //关卡+1
goal = goalList[level];
position = origin[level];
create();
}else {
alert("厉害啊 大佬 通关了都");
}
}
}
以上2018.1.7
博客链接
# VsCode保存时自动修复Eslint错误
同一个项目,保持代码风格的一致,是非常重要的一个规范。但事实上项目小组成员的代码校验规则、格式化工具通常都不一致,为了避免项目到后期出现无法维护的问题,项目成员使用同一套校验规则,同一个格式化方式是相当好的步骤之一。
游泳、健身了解一下:博客、前端积累文档、公众号
# 保存时自动统一代码风格:
先通过一些简单的配置,然后:
Ctrl
+s
/ command
+s
时自动修复代码的格式错误
- 自动修复的规则是读取项目根目录的Eslint规则
- 这样就能保证项目成员都是一套验证规则的代码风格
# 配置:
# 1.安装VsCode的EsLint
和vetur
插件
如图安装EsLint
插件:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/165e132647eca15f?raw=true)
# 2.为项目安装EsLint
包:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/165e136abe3b1feb?raw=true)
注意要安装在开发环境上,还有就是如果你使用的是脚手架的话,选了Eslint选项,会自带这些包。
# 3.在项目的根目录下添加.eslintrc.js
用于校验代码格式,根据项目情况,可自行编写校验规则:
module.exports = {
// Eslint规则
}
# 4.首选项设置:
将下面这部分放入首选项设置中:
"eslint.autoFixOnSave": true, // 启用保存时自动修复,默认只支持.js文件
"eslint.validate": [
"javascript", // 用eslint的规则检测js文件
{
"language": "vue", // 检测vue文件
"autoFix": true // 为vue文件开启保存自动修复的功能
},
{
"language": "html",
"autoFix": true
},
],
想了解更多的话,推荐看一下VsCode的EsLint插件
# 大功告成:
点开文件,你可能会看到如下报错,无需一个一个去改,只要保存一下文件,就可以自动修复这些代码格式上的问题了。
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/165e151df42747c4?raw=true)
注意:
如果整个文件都飘红的话,不会一次性修改如果的格式问题,会一下改一部分,你可能需要多按几次保存。
# 一键修复项目格式问题:
遇到下面这两种情况:
- 你刚刚引入这个自动修复,但你项目的文件比较多,且你又比较懒。
- 隔一段时间,修复一下代码格式上的问题
你可以像下面这样,在package.json
里面的scripts
里面新增一条如下命令:
"lint": "eslint --ext .js,.vue src --fix"
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/165e1561a9b92866?raw=true)
--ext
后面跟上的.js
、.vue
是你要检测文件的后缀,.vue
后面的src
是要检测的哪个目录下面的文件。
--fix
的作用是自动修复根据你配置的规则检测出来的格式问题
一键修复:
输入如下命令行,就可以自动修复你src
文件夹下面的所有根据你配置的规则检测出来的格式问题。
# .eslintignore 不检测一些文件:
在项目的根目录创建一个.eslintignore
文件,用于让EsLint
不检测一些文件。
比如引的一些别人的文件,插件等,比如文件中:
文件中的内容像上面这样写,这里第一行是不检测src目录下的test文件夹下面的所有文件。
# 自定义规则:
// .eslintrc.js文件
module.exports = {
"rules": { // 自定义规则
"no-console": 0,
"no-const-assign": 1,
"no-extra-bind": 2,
}
}
0、1、2的意思:
"off"
或 0 - 关闭这项规则
"warn"
或 1 - 将规则视为一个警告
"error"
或 2 - 将规则视为一个错误
# 小结
使用自动VsCode+EsLint格式化代码,在团队内部相互去看别人的代码的时候,就可以更容易的看的懂,能够极大的降低团队的沟通成本和提高心情,设置这么方便,赶紧在团队中用起来吧!
博客链接
在vscode中用于生成文件头部注释和函数注释的插件,经过多版迭代后,插件:支持所有主流语言,功能强大,灵活方便,文档齐全,食用简单!觉得插件不错的话,就给个Star⭐️吧~
# 简介
-
文件头部添加注释:
- 在文件开头添加注释,记录文件信息
- 支持用户高度自定义注释选项
- 保存文件的时候,自动更新最后的编辑时间和编辑人
- 快捷键:
window
:ctrl+alt+i
,mac
:ctrl+cmd+i
-
在光标处添加函数注释:
- 在光标处自动生成一个注释模板,下方有栗子
- 支持用户高度自定义注释选项
- 快捷键:
window
:ctrl+alt+t
,mac
:ctrl+cmd+t
- 快捷键不可用很可能是被占用了,参考这里
-
支持不同语言的注释格式
-
自定义注释符号,再也不用担心冷门语言插件不支持了!
-
自动添加头部注释
-
查看更多配置,以及有更多需求可以给我提issue。
# 安装
在 Vscode 扩展商店中搜索koroFileHeader
,点击安装即可。
# 使用
-
文件头部注释:
在当前编辑文件中使用快捷键:window
:ctrl+alt+t
/mac
:ctrl+cmd+t
,即可生成文件头部注释。
-
函数注释:
- 将光标放在函数行或者将光标放在函数上方的空白行
- 使用快捷键
window
:ctrl+alt+t
,mac
:ctrl+cmd+t
,即可生成函数注释。
- 事实上,函数注释在文件的任意位置都可生成,这里需要自己控制。
# 注释模板的设置
设置也超方便的,传送门
# 支持功能:
- 自定义注释模板信息,自动更新最后编辑时间,最后编辑人。
- 支持几乎所有语言的注释形式
- 自定义注释符号,即使插件不支持的语言,也可以自己定制。
- 自动添加头部注释功能,配合自动添加头部注释黑名单,麻麻再也不用担心我忘记加注释了。
- 头部注释第几行插入,类似
PHP
第一行被占用了,通过设置,可以在第二行里面插入。
- 注释时间格式化
- 在头部注释之前、之后插入一段内容,类似
python
的环境声明: #!/usr/bin/env python
- 特殊字段自定义,类似博客的时间字段有特殊要求。
- 头部注释中输出一段自定义信息,可以是版权声明、个性签名等内容。
- 匹配到换行自动添加注释符号, 生成头部注释自动移动光标到`Description所在行.
# 使用效果:
![](https://raw.githubusercontent.com/OBKoro1/koro1FileHeader/master/images/updateTime.gif)
# wiki文档
更新日志
支持语言
插件设置/配置
常见问题
# 最后
如果觉得还不错的话,就给个 Star ⭐️ 鼓励一下我吧~
博客链接
# H5 notification浏览器桌面通知
![](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1g0brdnk332j30xc0dwjtz.jpg?raw=true)
Notification
是HTML5新增的API,用于向用户配置和显示桌面通知。上次在别的网站上看到别人的通知弹窗,好奇之余也想知道如何实现的。实际去查一下发现并不复杂,且可以说比较简单,故写篇博客分享给大家,希望能帮你们了解这个API。
# npm包:
我还发了一个npm包:notification-Koro1,非常轻量简洁,觉得不错的话,点个Star吧~
# chrome下Notification
的表现:
- 以谷歌为例,一开始需要用户允许通知:
![](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1fzskpfbfmbj30hs078q36.jpg?raw=true)
- 允许通知之后,显示的通知长这样:
![](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1fzyaxnnc25j30kg03wjrs.jpg?raw=true)
# Notification
特性
-
该通知是脱离浏览器的,即使用户没有停留在当前标签页,甚至最小化了浏览器,也会在主屏幕的右上角显示通知,然后在一段时间后消失。
-
我们可以监听通知的显示,点击,关闭等事件,比如点击通知打开一个页面。
博客、前端积累文档、公众号、GitHub
# 栗子:去各个网站里面的控制台去运行
API的具体细节,等下再说,先试试这个API~
下面是一个简单的栗子,大家可以先在各个网站的控制台里面运行查看Notification
的效果:
var options = {
dir: "auto", // 文字方向
body: "通知:OBKoro1评论了你的朋友圈", // 通知主体
requireInteraction: true, // 不自动关闭通知
// 通知图标
icon: "https://upload-images.jianshu.io/upload_images/5245297-818e624b75271127.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"
};
notifyMe('这是通知的标题', options);
function notifyMe(title, options) {
// 先检查浏览器是否支持
if (!window.Notification) {
console.log('浏览器不支持通知');
} else {
// 检查用户曾经是否同意接受通知
if (Notification.permission === 'granted') {
var notification = new Notification(title, options); // 显示通知
} else if (Notification.permission === 'default') {
// 用户还未选择,可以询问用户是否同意发送通知
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('用户同意授权');
var notification = new Notification(title, options); // 显示通知
} else if (permission === 'default') {
console.warn('用户关闭授权 未刷新页面之前 可以再次请求授权');
} else {
// denied
console.log('用户拒绝授权 不能显示通知');
}
});
} else {
// denied 用户拒绝
console.log('用户曾经拒绝显示通知');
}
}
}
# 浏览器支持:
MDN:目前Notification
除了IE浏览器不支持外, 其他浏览器都已支持桌面通知,移动端浏览器基本都未支持。
因为兼容性问题,所以在使用Notification
之前,我们需要查看浏览器是否支持Notification
这个API:
if(window.Notification){
// 桌面通知的逻辑
}
# 通知权限:
为了避免网站滥用通知扰民,在向用户显示通知之前,需要经过用户同意。
Notification.permission
用于表明当前通知显示的授权状态,它有三个值:
default
: 默认值,用户还未选择
granted
: 用户允许该网站发送通知
denied
: 用户拒绝该网站发送通知
# 检测权限:
检测浏览器是否支持Notification
之后,需要检测一下用户通知权限。
if (Notification.permission === 'granted') {
console.log('用户曾经同意授权');
// 随时可以显示通知
} else if (Notification.permission === 'default') {
console.log('用户还未选择同意/拒绝');
// 下一步请求用户授权
} else {
console.log('用户曾经拒绝授权 不能显示通知');
}
# 请求权限
当Notification.permission
为default
的时候,我们需要使用Notification.requestPermission()
来请求用户权限。
Notification.requestPermission()
基于promise语法,then的回调函数参数是用户权限的状态Notification.permission
的值。
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('用户同意授权');
// 随时可以显示通知
} else if (permission === 'default') {
console.log('用户关闭授权 可以再次请求授权');
} else {
console.log('用户拒绝授权 不能显示通知');
}
});
// 老版本使用的是回调函数机制:Notification.requestPermission(callback); 参数一样
# 推送通知
当Notification.permission
为granted
时,请求到用户权限之后,不必立即发送通知,可以在任意时刻,以任意形式来发送通知。
const options = {}; // 传空配置
const title = '这里是标题';
const notification = new Notification(title, options) // 显示通知
上面这段代码就可以显示一个简单的通知了,只要用户允许你弹窗。
# Notification
的参数:
- title:通知的标题
- options:通知的设置选项(可选)。
- body:字符串。通知的body内容。
- tag:代表通知的一个识别标签,相同tag时只会打开一个通知窗口。
- icon:字符串。要在通知中显示的图标的URL。
- data:想要和通知关联的数据,可以在
new Notification
返回的实例中找到。
- renotify: 布尔值。相同tag,新通知出现的时候是否替换之前的(开启此项,tag必须设置)。
- requireInteraction:布尔值。通知不自动关闭,默认为false(自动关闭)。
- 还有一些不太重要的配置可以看张鑫旭老师的博客和MDN的介绍
requireInteraction: 保持通知不自动关闭
默认值为false,通知会在三四秒之后自动关闭。
当设置为true
,并且当有超过两个通知(new Notification(title, options)
)时,会出现如下图的通知叠加状态。
![](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1fzyy3ykl2gj30jy0kc772.jpg?raw=true)
这种情况显然,我们只能默认操作最后一个通知,除非你把每个通知返回的实例都保存下来。
我发布的npm包:notification-koro1,可以自定义一定的时间间隔自动关闭不自动关闭的通知,也可以一次性关闭所有通知
PS:如果没有触发叠加,很可能是因为你两次通知的tag配置项是相同的(相同tag只能出现一个弹窗)。
PS: safari下不支持该选项,默认自动关闭
renotify:相同
默认值为false,chorme下相同tag的通知不替换,还是老的通知
设置为true
, 两个相同tag的通知,新通知替换之前旧的通知。
注意:使用renotify
,必须要同时设置tag
选项,否则将会报错。
PS: safari下不支持该选项,默认两个相同tag的通知,新通知替换之前旧的通知。
# Notification
的实例:
生成通知,会返回一个实例,如下:
const instanceNotification = new Notification(title, options)
instanceNotification
就是当前通知的实例,在该实例上,我们可以查询该通知的配置,监听事件,调用实例方法。
下文都以instanceNotification
指代通知返回的实例。
# 通知的配置:
在通知实例上可以读取到设置通知时的所有配置,比如:
通知标题:instanceNotification. title
、通知内容:instanceNotification. body
、通知图标:instanceNotification. icon
等。
PS: 这些属性都是只读的,不能删除,不能修改,不能遍历。
# 事件处理:
我们可以使用通知的实例来监听通知的事件:
click
: 用户点击通知时被触发
show
: 通知显示的时候被触发
error
: 通知遇到错误时被触发
close
: 用户关闭通知时被触发
instanceNotification.onclick = e => {
// do something 可以是:打开网址,发请求,关闭通知等
}
注意:最好是一发出通知就立即监听事件,否则有些事件可能一开始没被触发或永远不会触发。
例如:用定时器5秒后才监听通知的点击和显示事件,则永远不会触发通知显示的回调,点击事件在5秒后才可以正常起作用但会错误五秒之前用户的点击。
# 关闭通知
instanceNotification.close()
没有设置不自动关闭的话,chrome通知将会在4.5秒左右自动关闭通知,safari则是5秒钟(无法设置不自动关闭)。
notification没有定时控制通知多久后消失的功能,当出现多个通知,也无法统一关闭。
这两个问题,在我发布的NPM包:notification-koro1中,都解决掉了,并提供更清晰的回调
# 应用场景
- 即时通讯软件(邮件、聊天室)
- 体育赛事结果彩票/抽奖结果
- 新闻网站重大新闻通知
- 网站的重大更新,重大新闻等。
# notification其他
这里是一些API/浏览器细节,以及可能会遇到的问题,可以先不看,等真正遇到了,回头再来看。
# 用户拒绝显示通知:
一旦用户禁止网站显示通知,网站就不能再请求用户授权显示通知,需要用户去设置中更改。
chrome浏览器的通知设置位置:设置>高级>内容设置>通知
saafari浏览器:偏好设置>网站>通知>找到网站>修改权限/恢复默认
# 关闭请求权限:
在chorme浏览器中:当用户关闭请求权限的弹窗(右上角的叉叉),页面还没刷新,我们可以再次向用户请求权限。页面刷新过后,浏览器默认用户拒绝。
在safari浏览器下,没有关闭请求权限的选项,用户必须选择同意/拒绝。
# icon不显示问题:
可能是网站进行了同源限制(比如github),不是域名下面的图片,会报错,不能调用。
# tag:
tag
相同的通知,同时只能出现一个,老通知是否会被覆盖取决于:renotify
配置和浏览器。
- chrome下:当通知关闭之后,上次出现过的tag在一段时间内,不能再出现,比如刷新页面再请求相同tag的通知。(在safari下正常出现)
# safari下面不能显示icon
在safari下面,同一个网站(比如谷歌),同样的代码,chorme可以正常显示icon,safari却没有icon,也没有报错。
谷歌之后发现,在stack overflow里面看到safari只支持body和tag选项,并不支持icon选项。
# 连续触发
在safari和chrome下短时间内连续触发通知(不设tag
,不设requireInteraction
),会出现如下表现:
![notification 连续触发](https://github.com/OBKoro1/articleImg_src/blob/master/weibo_img_move/005Y4rCogy1g05d2z0zxej30je03wwep.jpg?raw=true)
这个表现,通知没有icon、标题、内容,就显得没有意义了,浏览器以这种形式,限制开发者不要频繁打扰用户。
# notification-Koro1:
试一下notification-Koro1啦, 持续维护,简单方便~
# 结语
本文写的比较细,可以先mark一下,然后以后真正用到这个API了,可以先通过文中的栗子,然后再查找对应的内容。
还有就是注意浏览器间的差异,我自己就试了chrome和safari,然后这两个浏览器在实现细节上有很多不一样的地方,开发的时候注意一下。
博客、前端积累文档、公众号、GitHub
参考资料:
notification-Koro1
简单了解HTML5中的Web Notification桌面通知
Notification MDN
HTML5 桌面通知:Notification API
博客链接
# 事件循环(Event Loop)机制以及实例
大家都知道js是单线程的脚本语言,在同一时间,只能做同一件事,为了协调事件、用户交互、脚本、UI渲染和网络处理等行为,防止主线程阻塞,Event Loop方案应运而生...
# 为什么js是单线程?
js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。
在js高程中举过一个栗子,如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。
# 执行栈与任务队列
因为js是单线程语言,当遇到异步任务(如ajax操作等)时,不可能一直等待异步完成,再继续往下执行,在这期间浏览器是空闲状态,显而易见这会导致巨大的资源浪费。
# 执行栈
当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,遵循先进先出原则。
# 主线程
要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。
主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。
不太理解的话,可以运行一下下面的代码,或者点击一下这个demo
结果是当a、b、c函数都执行完成之后,三个setTimeout才会依次执行。
let a = () => {
setTimeout(() => {
console.log('任务队列函数1')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('a的for循环')
}
console.log('a事件执行完')
}
let b = () => {
setTimeout(() => {
console.log('任务队列函数2')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('b的for循环')
}
console.log('b事件执行完')
}
let c = () => {
setTimeout(() => {
console.log('任务队列函数3')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('c的for循环')
}
console.log('c事件执行完')
}
a();
b();
c();
// 当a、b、c函数都执行完成之后,三个setTimeout才会依次执行
# js 异步执行的运行机制。
- 所有任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
- 主线程不断重复上面的第三步。
# 宏任务与微任务:
异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。
宏任务(macrotask)::
script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
微任务(microtask):
Promise、 MutaionObserver、process.nextTick(Node.js环境)
# Event Loop(事件循环):
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
- 执行栈选择最先进入队列的宏任务(通常是
script
整体代码),如果有则执行
- 检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
- 更新render(每一次事件循环,浏览器都可能会去更新渲染)
- 重复以上步骤
宏任务 > 所有微任务 > 宏任务,如下图所示:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/164081cfd8400f92?raw=true)
从上图我们可以看出:
- 将所有任务看成两个队列:执行队列与事件队列。
- 执行队列是同步的,事件队列是异步的,宏任务放入事件列表,微任务放入执行队列之后,事件队列之前。
- 当执行完同步代码之后,就会执行位于执行列表之后的微任务,然后再执行事件列表中的宏任务
上面提到的demo结果可以这么理解:先执行script
宏任务,执行完了之后,再执行其他两个定时器宏任务。
# 面试题实践
下面这个题,很多人都应该看过/遇到过,重新来看会不会觉得清晰很多:
// 执行顺序问题,考察频率挺高的,先自己想答案**
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
console.log(4);
根据本文的解析,我们可以得到:
-
先执行script
同步代码
先执行new Promise中的console.log(2),then后面的不执行属于微任务
然后执行console.log(4)
-
执行完script
宏任务后,执行微任务,console.log(3),没有其他微任务了。
-
执行另一个宏任务,定时器,console.log(1)。
根据本文的内容,可以很轻松,且有理有据的猜出写出正确答案:2,4,3,1.
# 小结
类似上文的面试题还有很多,实则都大同小异,只要掌握了事件循环的机制,这些问题都会变得很简单。
博客链接
# input 的一些坑点分享
# 本文内容包括:
- 移动端底部 input 被弹出的键盘遮挡。
- 控制 input 显/隐密码。
- 在 input 中输入 emoji 表情导致请求失败。
- input 多行输入显示换行。
- 输入框首尾清除空格-trim()
- 在 input 中监听键盘事件
# 移动端底部 input 被弹出的键盘遮挡
input 输入框是通过position:fixed
一直放在页面底部,当点击 input 进行输入的时候,就会出现如下图片情况(有的机型会遮挡一些)。
当时这个问题是去年在 ios 中遇到的,在最新版的 ios 系统中,貌似解决了这个 bug,但是为了向下兼容以及防止其他其他机型也出现这个问题,大家可以稍微记一下这个解决方法。
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/16353072dcc21218?raw=true)
在解决这个问题的时候,有试过下面这种方法:
在 input 的 focus 事件中,开启一个定时器,然后每隔 300 毫秒进行一次 document.body.scrollTop=document.body.scrollHeight 的调整,运行 3 次即可。
当时还以为解决了,但是当你底部评论区还有很多内容,你每次点击 input,想要输入的时候,整个页面通过scrollTop
就会不断的向下滚动,这个体验不用说自己也知道是相当失败的,然后就再去找解决方法,结果就有了下面这个。
# Element.scrollIntoView()
Element.scrollIntoView():方法让当前的元素滚动到浏览器窗口的可视区域内。
document.querySelector('#inputId').scrollIntoView();
//只要在input的点击事件,或者获取焦点的事件中,加入这个api就好了
这个 api 还可以设置对齐方法,选择将 input 放在屏幕的上方/下方,类似的 api 还有:Element.scrollIntoViewIfNeeded(),这两个是解决同一个问题的,选择一个用就可以了。
# 控制 input 显/隐密码
这个就很简单了,只需更改 input 的 type 属性值就可以了。可以看一下 codepen 的demo
//点击函数,获取dom,判断更改属性。
show(){
let input=document.getElementById("inputId");
if(input.type=="password"){
input.type='text';
}else{
input.type='password';
}
}
# 在 input 中输入 emoji 表情导致请求失败
现在用户输入 emoji 简直已经成为了习惯,如果前后端没有对 emoji 表情进行处理,那么用户在上传的时候,就会请求失败。
通常这个问题是后端那边处理比较合适的,前端是做不了这件事的,或者说很难做这件事。
之前看过一篇文章,这个文章里面讲了怎么在上传和拿数据下来的时候不会报错,但是不能在显示的时候转换为表情。
ps:之前拿微信用户名的时候,有些人可能在微信昵称上面就会包含表情,如果后端没对表情处理转换,那么普通请求也会出错。
之所以说这个,当表单请求错误的时候各位如果实在找不到问题可以往这方面考虑一下,我真的被坑过的 o(╥﹏╥)o。
# textarea 多行回车换行,显示的时候换行设置:
在使用textarea
标签输入多行文本的时候,如果没有对多行文本显示处理,会导致没有换行的情况,就比如下面这种情况,用户在textarea
是有换行的。
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/1635388bf4dca899?raw=true)
white-space 属性用于设置如何处理元素内的空白,其中包括空白符和换行符。
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/16353927aec80539?raw=true)
只要在显示内容的地方将该属性设置为white-space: pre-line
或者white-space:pre-wrap
,多行文本就可以换行了。
# 设置之后,显示效果:
![](https://github.com/OBKoro1/articleImg_src/blob/master/juejin/1635d9d75136f6d8?raw=true)
# 输入框首尾清除空格-trim()
输入框清除首尾空格是 input 较为常见的需求,通常在上传的时候将首尾空格去除掉。一般使用:字符串的原生方法trim() 从一个字符串的两端删除空白字符。
trim() 方法并不影响原字符串本身,它返回的是一个新的字符串。
# 原生清除方法:
//原生方法获取值,清除首尾空格上传str2
let str2 = document.getElementById('inputId').trim();
# Vue 清除方法:
Vue 提供了修饰符删除首尾空格, 加了修饰符.trim
会自动过滤用户输入的首尾空白字符
<input v-model.trim="msg">
貌似 angular 也提供了类似过滤的方法,感兴趣的可以自己去查一下。
# 在 input 中监听键盘事件
在用户登录或者搜索框的时候,一般都会监听键盘事件绑定回车按键,来执行登录/搜索 等操作。
# 原生绑定:
<input onkeydown="keydownMsg(event)" type="text" />;
function keydownMsg(key) {
keyCode = key.keyCode; //获取按键代码
if (keyCode == 13) {
//判断按下的是否为回车键
// 在input上监听到回车 do something
}
}
# Vue 按键修饰符
Vue 为监听键盘事件,提供了按键修饰符,并且为常用的按键提供了别名,使用方法如下:当回车按键在 input 中被按下的时候,会触发里面的函数。
<input @keyup.enter="enterActive">
博客链接
# [读书笔记]《高性能 JavaScript》
# 缺陷
这本书是 2010 年出版的,这本书谈性能是有时效性的,现在已经是 2018 年了,这几年前端发展的速度是飞快的,书里面还有一些内容考虑 IE6、7、8 的东西,殊不知现在这些都已经不再考虑了,所以不可避免的有一些知识是比较老的。有些解决方法现在已经不是最好的解决方式,比如工具那一章。
# 前言
总的来说,这本书整体给出的性能优化建议,以及作者耐心的实践,对我们开发优化的启发和帮助还是很大的,因为它里边的很多知识,都是作者通过实践总结出来的,都是经验的积累,这在一般的教科书上是学不到的。特别是对于 js 基础比较差一点的,里面有很多知识点尽管在现在还是非常有必要的。
下面我就将各章节的一些重要的知识点总结写出来,争取将干货都提取出来。
# 第一章-加载和执行
- js 的阻塞特性:
当浏览器在执行 js 代码的时候,不能同时做其他事情。(界面 ui 线程和 js 线程用的是同一进程,所以 js 执行越久,网页的响应时间越长。)
- 脚本的位置
如果把脚本<script>
放在<head>
中,页面会等 js 文件全部下载并执行完成后才开始渲染,在这些文件下载和执行的过程中:会导致访问网站的时候有明显的延迟,表现为:页面空白。
性能提升:推荐将所有的<script>
标签尽可能的放到<body>
标签的底部,优先渲染页面,减少页面空白时间。
- 组件脚本。
每个<script>
标签初始下载的时候都会阻塞页面的渲染。性能提升做法:
减少内嵌脚本:减少内嵌的<script>
标签,将代码写在一个标签中。
合并外链的 js 文件:http 请求会带来额外的性能开销,栗子:下载一个 100KB 的 js 文件比下载 4 个 25kb 的 js 文件更快。具体操作方法自行搜索。
- 无阻塞脚本的方法
script 标签的 aync 属性:
async 属性规定一旦脚本可用,则会异步执行。async 属性仅适用于外部脚本(只有在使用 src 属性时)。如果 async="async":脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
script 标签的 defer 属性:
js 文件在页面解析到 script 标签的时候开始下载,但并不会执行,dom 加载完成执行。这两个属性的区别在于执行时机。
动态脚本元素。
js 操作 dom 创建<script>
标签,自定义生成标签的 type、src 属性。文件会在该元素被添加到页面的时候开始下载。ps:如果文件顺序很重要的话,最好按照顺序合成一个文件。然后再添加到页面中。这样:无论何时启动下载。文件的下载和执行过程不会阻塞页面的其他进程。
# 3. XHR ajax 脚本注入、
用 get 请求一个文件,请求好了。然后创建动态脚本,最后添加进去。
缺陷:文件要再请求页面的同一个域。不能从 cdn 下载
# 第一章加载和执行小结:
- 把文件放在 body 标签签名,
- 合并脚本,减少
<script>
标签。
- 采用无阻塞下载 js。使用 script 的 defer 和 async 属性 异步下载。动态创建 script 标签和执行代码。
# 第二章-数据存取
- js 四种基本的数据存取位置。
1、字面量:字符串、数字、布尔、对象、数组、函数、正则、null、undefined,字面量只代表自身,没有存储位置。 2、局部变量。 let var 声明的变量。3、数组元素。4、对象成员。
性能:访问字面量和局部变量的速度是最快的,访问数组和对象成员相对较慢
- 变量标识符解析过程:
搜索执行环境的作用域链,查找同名标识符。搜索过程从作用域链头部开始,也就是当前运行函数的活动对象。如果找到,就使用这个标识符,对应的变量;如果没有找到,继续搜索下面的对象。搜索过程会持续进行,直到找到标识符,若无法搜索到匹配的对象,那么标识符被视为未定义、
性能问题:一个标识符所在的位置越深,它的读写速度也就越慢。因此,函数中读写局部变量总是最快的,而读写全局变量通常是最慢的。
建议:将全局变量存储到局部变量,加快读写速度。
- 闭包,原型,嵌套对象。
优化建议:将常用的跨作用域变量存储到局部变量,然后直接访问局部变量。理由如上,变量标识符解析过程。
# 第二章数据存取小结:
- 访问字面量和局部变量的速度最快,相反,访问数组元素和对象成员相对较慢。
- 由于局部变量存在于作用域链的起始位置,因为访问局部变量比访问跨作用域变量更快。这个道理同样适用于数组,对象,跨作用域变量。
- 把常用的对象,数组,跨域变量保存在局部变量可以改善 js 性能,局部变量访问速度更快。
# 第三章 DOM 编程小结:
- dom 操作天生就慢,尽量减少 dom 操作,减少访问 dom 的次数。
- 使用 document.querySelect 来做选择器,比其他方式快。
- 需要多次访问某个 dom 节点,使用局部变量存储。
- html 集合,把集合长度缓存到一个变量中,然后遍历使用这个变量,如果经常操作集合,建议拷到一个数组中。
- 要留意浏览器的重绘和重排;批量修改样式的时候,‘离线’操作 DOM 树,使用缓存,并减少访问布局信息的次数。
重绘和重排是 DOM 编程优化的一个相当重要概念:重绘和重排。
- 动画中使用绝对定义,使用拖放处理。
- 使用事件委托来减少事件处理器的数量。
# 第四章算法和流程控制小结:
-
for、while 和 do-while 循环性能差不多,for-in 循环速度只有前面几种类型的 1/7,所以尽量避免使用 for-in 循环,除非你需要遍历一个属性数量未知的对象。
forEach 比 for 慢,如果运行速度要求严格,不建议使用。
-
改善循环性能的最佳方式是减少每次迭代的工作量和减少循环迭代的次数。
减少迭代工作量:减少属性查找和倒序循环,循环次数越多,性能优化越明显。
for(var i=0;i<items.length;i++){代码}//正序循环
for(var i=items.length;i--){代码}//倒序循环
//减少属性查找:查找一次属性,把值存在局部变量,在控制条件里面使用这个变量
倒序循环在i>0的时候会自动转换为true,等于0的时候为false。
//倒序循环:控制条件从(迭代数少于总数吗?它是否为true?)变为(它是否为true)
减少迭代的次数:"Duff's Device"循环体展开技术,有兴趣的可以看一下,迭代工作量大于 1000 的时候适用。
-
if-else 与 switch:条件数量越大,越倾向于使用 switch。
优化 if-else:
- 把最可能出现的条件放在首位.
- 使用二分法把值域分成一系列的区间。
-
浏览器的调用栈大小限制了递归算法在 js 中的应用;栈溢出错误会导致其他代码中断运行。
小心使用递归,现在es6递归可以尾递归,在es6中只要使用尾递归就不会发生栈溢出,相对节省性能。
# 第五章字符串和正则表达式小结:
- 字符串合并的时候使用简单的'+'和'+='操作符。
let str+='abc'+'efg;//2个以上的字符串拼接,会产生临时字符串
let str=str+'abc'+'efg';//推荐,提速10%~40%
-
书里面讲的正则原理和回溯原理,这个很重要,找个篇博客:跟书里面讲的差不多,但还是建议大家可以去找找 PDF 好好看看正则表达式这节。
-
提高正则表达式效率的方法:
- 最重要的是:具体化正则表达式!具体化正则表达式!具体化正则表达式!
- 关注如何让匹配更快失败,找出一些必需,特殊的字符
- 正则表达式以简单的、必需的字元开始。
- 使用量词模式,使它们后面的字元互斥。
- 较少分支数量,缩小分支范围
- 使用合适的量词
- 把正则表达式赋值给变量并重用
- 将复杂的正则拆分为简单的片段
//事实上,书里面讲的方法不止这么几个,而且每一个都有详细的解析 大佬们 还是去看看这一章节吧
-
正则表达式并不总是最好的解决方案,当你只是搜索字面字符串或者你事先知道字符串的哪一部分将要被查找时:
- 使用 indexOf()和 lastIndexOf()更适合查找特定字符串的位置或者判断它们是否存在
//例如:判断当前浏览器之类。
# 第六章快速响应的用户界面小结:
js 和用户界面更新在同一个进程中运行,因此一次只能处理一件事情。高效的管理 UI 线程就是要确保 js 不能运行太长时间,以免影响用户体验。
-
浏览器限制了 js 任务的运行时间,这种限制很有必要,它确保某些恶意代码不能通过永不停止的密集操作锁住用户的浏览器。此限制分为两种:调用栈的大小和长时间运行脚本。
-
任何 js 任务都不应当执行超过 100 毫秒。过长的运行时间会导致 UI 更新出现明显延迟,从而对用户体验造成负面影响。
-
定时器可用来安排代码延迟执行,它使得你可以把长时间运行脚本分解成一系列的小任务。
# 第七章 AJAX 小结
这一章节貌似东西都比较老一点。。
-
post 更适合发送大量数据到服务器。
-
get 请求能够被浏览器缓存,Expires 头信息设置浏览器缓存请求多久。可用于从不改变的图片或者其他静态数据集(js、css 等)
-
JSON 是一种使用 js 对象和数组直接量编写的轻量级且易于解析的数据格式,它不仅传输尺寸小,而且解析速度快。JSON 是高性能 AJAX 的基础,尤其在使用动态脚本注入时。
json 应该是近几年一直在用的。。。
- 减少请求数,通过合并 js 和 css 文件。
- 缩短页面的加载时间,页面主要内容加载完成后,用 AJAX 获取那些次要的文件。
# 第八章编程实践小结
- 避免双重求值:避免使用 eval()和 function()构造器来避免双重求值带来的性能消耗,同样的,给 setTimeout()和 setInterval()传递函数而不是字符串作为参数。
//双重求值:就是在js代码中执行另一段js代码,不建议使用下面这些方式
eval('代码')
function构造函数--new function('代码')
setTimeout(‘代码’,100)和setInterval(‘代码’,100)
- 尽量使用直接量创建对象和数组。直接量的创建和初始化都比非直接量形式要快。
- 避免做重复工作,能节省的步骤就节省。
- js 原生方法总会比你写的任何代码都要快。
# 第九章 构建并部署高性能 js 应用小结
构建和部署的过程对基于 js 的 web 应用的性能有着巨大影响。这个过程中最重要的步骤有:
- 合并、压缩 js 文件。可使用 Gzip 压缩,能够减少约 70%的体积!
这些都是在构建过程中完成的工作,不要等到运行时去做,webpack 也是在构建过程中,完成的工作。
- 通过正确设置 HTTP 响应头来缓存 js 文件,通过向文件名增加时间戳来避免缓存问题。
- 使用 CDN 提供 js 文件;CDN 不仅可以提升性能,它也为你管理文件的压缩与缓存,。
# 第十章 工具 小结:
当网页变慢时,分析从网络下载的资源以及分析的资源以及分析脚本的运行性能能让你专注于那些最需要优化的地方。
-
使用网络分析工具找出加载脚本和页面中其他资源的瓶颈,这会帮助你决定那些脚本需要延迟加载,或者需要进一步分析。
- 检查图片、样式表和脚本的加载过程,以及它们对页面整体加载和渲染的影响。
- 针对性的做出优化
-
把脚本尽可能延迟加载,这样做可以加快页面渲染速度,给用户带来更好的体验。
-
确认脚本和其他资源文件的加载过程已经被优化
- 这里主要是说文件从服务器的下载速度,比如服务器那边的配置问题之类的。
- 栗子:我就被后端坑过。一个js文件就200k ,下载下来需要50秒钟!
- 后面发现原来是后端那边nginx没有开启加速配置什么的,导致出现的问题,找问题找半天。
-
测试脚本的运行时间,用一个 Date 实例减去另一个 Date 实例,得到的时间差就是脚本运行消耗的时间。
let start = new Date();
//你的代码
let time = newDate() - start;
- chrome ,火狐 等主流浏览器的控制面板,已经能够反映很多性能问题。仔细分析就能找出很多问题。例如:资源加载,断点等
# 后话
事实上,自认为这本书最宝贵的就是一些提到的细节,比如:
1、字符串合并的时候使用简单的'+'和'+='操作符。
let str+='abc'+'efg'; //2个以上的字符串拼接,会产生临时字符串
let str=str+'abc'+'efg'; //推荐,提速10%~40%
2、避免双重求值:避免使用 eval()和 function()构造器来避免双重求值带来的性能消耗,同样的,给 setTimeout()和 setInterval()传递函数而不是字符串作为参数。
//双重求值:就是在js代码中执行另一段js代码,不建议使用下面这些方式
eval('代码')
function构造函数--new function('代码')
setTimeout(‘代码’,100)和setInterval(‘代码’,100)
# 这些东西可以让我们知道什么是更好的实践,什么样的代码可以跑得更快,让我们养成更好的开发习惯。
书不太厚,如果对里面的内容感兴趣,还是建议买一本回家看一看。
以上 2018.1.9
博客链接
# 防抖函数&节流函数
节流函数和防抖函数是 JS 比较重要的概念,应用好了可以提高很大的性能,在面试中也是很高频的一个考点。下面一起来看看这两种方法是如何使用的:
# 防抖函数(debounce):
# 概念:
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
# 生活中的栗子:
如果有人进电梯(触发事件),那电梯将在 10 秒钟后出发(执行事件),这时如果又有人进电梯了(在 10 秒内再次触发该事件),我们又得等 10 秒再出发(重新计时)。
# 代码栗子:
/**
* @description: 防抖函数:函数被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
* @param {Function} fn 要执行的函数
* @param {Number} wait wait毫秒后执行回调
* @param {*} ...arr 传递给fn的参数
*/
function debounce(fn, wait, ...arr) {
let timer = null;
return () => {
if (timer) {
// 如果有一个函数在等待执行 清除定时器 下面重新计时
clearTimeout(timer);
timer = null; // 清空timer 下次重启定时器
}
// 设定时器/重置定时器
timer = setTimeout(() => {
fn(...arr); // wait时间后 执行回调 期间再触发debounce 需要重新等待
}, wait);
};
}
// 要防抖的函数
let actionFn = function(a, b) {
console.log('回调', a, b);
};
setInterval(debounce(actionFn, 500, 'actionFn参数1', '参数2'), 1000); // 第一次在1500ms后触发,之后每1000ms触发一次
setInterval(debounce(actionFn, 2000), 1000); // 还没执行就一直重复触发,不会执行
可以这样理解
函数触发停止一段时间后(期间不能再触发 debounce,否则将重新计时),再执行回调函数
# 机制:
防抖函数主要利用定时器的延迟执行特性,根据是否有定时器在等待执行:
- 触发了一个事件后:如果有一个定时任务待执行,就清除定时器,重新计时。
- 如果没有任务待执行,就定时执行这个事件。
# 应用场景:
- 表单的连续点击,防止重复提交。比如重复发送一篇文章。
- 类百度的搜索,连续输入等输入停止后再搜索。
- 一直拖动浏览器窗口,只想触发一次事件等。
# 节流函数(throttle):
# 概念:
规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行(单位时间内有事件被多次触发则,只生效一次)。
# 生活中的栗子:
漏水的自来水水龙头,尽管水龙头里面有很多水(一直在触发事件),但还是一滴一滴的往下滴(单位事件内只生效一次)。
# 代码栗子:
/**
* @description: 节流函数:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行
* @param {Function} fn 要执行的函数
* @param {Number} gapTime 单位时间
* @param {*} ...arr 传递给fn的参数
*/
function throttle(fn, gapTime, ...arr) {
let last = 0; // 上次执行时间 第一次马上执行
return () => {
let nowTime = Date.now(); // 当前时间
// 当前时间-上次执行的时间是否超过间隔时间 就执行回调
if (nowTime - last > gapTime) {
fn(...arr); // ...arr为fn的参数
last = nowTime; // 重置上次执行时间为当前时间 方便下次执行
}
};
}
let actionFn = (a, b) => {
console.log('回调', a, b); // 要执行的函数
};
setInterval(throttle(actionFn, 1000, 'actionFn参数1', '参数2'), 10);
// 每隔10毫秒都会触发一次throttle,每隔一秒触发一次actionFn回调(1秒内再次触发被丢弃)
# 机制:
节流函数根据时间差是否超过给定时间(gapTime)来决定是否触发回调。
# 应用场景:
- 自动保存草稿功能,当用户在输入的时候(一直触发事件),单位时间内只保存一次草稿。
- 游戏中的刷新率
# 作用和本质:
# 应用实例,需要加个括号:
因为返回debounce
和throttle
返回的是一个函数,所以如果不是自动执行的事件监听回调,我们应该再后面加个()
,执行返回的闭包函数。
document.onclick = () => {
// throttle(actionFn, 1000) 这样不会执行 只返回了一个闭包函数
throttle(actionFn, 1000, '执行函数参数1', '执行函数参数2')(); // 执行闭包函数
};
博客链接
Recommend Projects
-
-
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
An Open Source Machine Learning Framework for Everyone
-
The Web framework for perfectionists with deadlines.
-
A PHP framework for web artisans
-
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
Some thing interesting about web. New door for the world.
-
A server is a program made to process requests and deliver data to clients.
-
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Some thing interesting about visualization, use data art
-
Some thing interesting about game, make everyone happy.
-
Recommend Org
-
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Open source projects and samples from Microsoft.
-
Google ❤️ Open Source for everyone.
-
Alibaba Open Source for everyone
-
Data-Driven Documents codes.
-
China tencent open source team.
-