cosyer's Blog

Blog


  • 首页

  • 友链

  • 留言板

  • 归档

  • 关于

  • 搜索

深入理解 标签

11月
25
更新于
11月25
2019
JS

事件机制

发表于 2019-11-24 | 热度 ℃
| 字数统计: 507 (字) | 阅读时长: 3 (分钟)

事件机制的核心就是发布-订阅模式。维护一个对象,对象的 key 存的是事件 type,对应的 value 为触发相应 type 的回调函数,即 listeners,然后 trigger 时遍历通知,即 forEach 进行回调执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class EventTarget {
constructor() {
this.listener = [];
}

// 监听事件
on(type, callback) {
// 如果是第一次监听该事件,则初始化数组
if (!this.listeners[type]) this.listeners[type] = [];
this.listeners[type].push(callback);
}

// 只监听一次
once(type, callback) {
if (!this.listeners[type]) this.listeners[type] = [];
// once 只触发一次,触发后 off 即可 回调函数加上标识
callback._once = true;
this.listeners[type].push(callback);
}

// 取消监听
off(type, callback) {
const listeners = this.listeners[type];
if (Array.isArray(listeners)) {
// filter 返回新的数组,会每次对 this.listeners[type] 分配新的空间
// this.listeners[type] = listeners.filter(l => l !== callback);
// 根据 type 取消对应的回调
const index = listeners.indexOf(callback);
// 用 splice 要好些,直接操作原数组
this.listeners[type].splice(index, 1);
// 如果回调为空,删除对该事件的监听
if (this.listeners[type].length === 0) delete this.listeners[type];
}
}

// 执行 扳机社
trigger(event) {
// type 为必传属性
const { type } = event;
if (!type) throw new Error('没有要触发的事件!');
// 判断是否之前对该事件进行监听了
const listeners = this.listeners[type];
if (!listeners) throw new Error(`没有对象监听 ${type} 事件!`);
if (!event.target) event.target = this;
listeners.forEach(l => {
l(event);
// 如果通过 once 监听,执行一次后取消
if (l._once) this.off(type, l);
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 测试
function handleMessage(event) { console.log(`message received: ${ event.message }`); }

function handleMessage2(event) { console.log(`message2 received: ${ event.message }`); }

const target = new EventTarget();

target.on('message', handleMessage);
target.on('message', handleMessage2);
target.trigger({ type: 'message', message: 'hello custom event' }); // 打印 message,message2

target.off('message', handleMessage);
target.trigger({ type: 'message', message: 'off the event' }); // 只打印 message2

target.once('words', handleMessage);
target.trigger({ type: 'words', message: 'hello2 once event' }); // 打印 words
target.trigger({ type: 'words', message: 'hello2 once event' }); // 报错:没有对象监听 words 事件!
7月
10
更新于
7月10
2020
JS

关于requestAnimationFrame

发表于 2019-09-22 | 热度 ℃
| 字数统计: 1,691 (字) | 阅读时长: 7 (分钟)

在JS中,我们可以使用 setTimeout 和 setIntarval 实现动画,但是 H5 的出现,让我们又多了两种实现动画的方式,分别是 CSS 动画(transition、animation)和 H5的canvas 实现。除此以外,H5还提供了一个专门用于请求动画的API,让 DOM 动画、canvas动画、svg动画、webGL动画等有一个专门的刷新机制。

传统的javascript 动画是通过定时器 setTimeout 或者 setInterval 实现的。但是定时器动画一直存在两个问题

  • 动画的循时间环间隔不好确定(推荐的最佳循环间隔是17ms(大多数电脑的显示器刷新频率是60Hz,1000ms/60))
  • 定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,如果UI线程处于忙碌状态,那么动画不会立刻执行

1. 定义

requestAnimationFrame 方法会告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用回调函数来更新动画。

1
window.requestAnimationFrame(callback)
  • callback:下一次重绘之前更新动画帧所调用的函数,callback仅有一个参数,为DOMHighResTimeStamp参数,表示requestAnimationFrame()开始执行回调函数的时刻。
  • 返回值:一个 long 类型整数,唯一标志元组在列表中的位置,你可以传这个值给cancelAnimationFrame() 以取消动画。

在使用和实现上, requestAnimationFrame 与 setTimeout 类似。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let count = 0;
let rafId = null;
/**
* 回调函数
* @param time requestAnimationFrame 调用该函数时,自动传入的一个时间
*/
function requestAnimation(time) {
console.log(time); // 打印执行requestAnimation函数的时刻
// 动画没有执行完,则递归渲染
if (count < 5) {
count++;
// 渲染下一帧
rafId = window.requestAnimationFrame(requestAnimation);
}
}
// 渲染第一帧
window.requestAnimationFrame(requestAnimation);

2.怎样执行

  • 首先判断 document.hidden 属性是否可见(true),可见状态下才能继续执行以下步骤
  • 浏览器清空上一轮的动画函数
  • requestAnimationFrame 将回调函数追加到动画帧请求回调函数列表的末尾 注意:当执行 requestAnimationFrame(callback)的时候,不会立即调用 callback 回调函数,只是将其放入回调函数队列而已,同时注意,每个 callback回调函数都有一个 cancelled 标志符,初始值为 false,并对外不可见。
  • 当页面可见并且动画帧请求callback回调函数列表不为空时,浏览器会定期将这些回调函数加入到浏览器 UI 线程的队列中(由系统来决定回调函数的执行时机)。当浏览器执行这些 callback 回调函数的时候,会判断每个元组的 callback 的cancelled标志符,只有 cancelled 为 false 时,才执行callback回调函数。

3. 优点

  1. requestAnimationFrame 自带函数节流功能,采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间的过短,造成过度绘制,增加页面开销,也不会因为间隔时间过长,造成动画卡顿,不流程,影响页面美观。 浏览器的重绘频率一般会和显示器的刷新率保持同步。大多数采用 W3C规范,浏览器的渲染页面的标准频率也为 60 FPS(frames/per second)即每秒重绘60次,requestAnimationFrame的基本思想是 让页面重绘的频率和刷新频率保持同步,即每 1000ms / 60 = 16.7ms执行一次。 通过 requestAnimationFrame 调用回调函数引起的页面重绘或回流的时间间隔和显示器的刷新时间间隔相同。所以 requestAnimationFrame 不需要像 setTimeout 那样传递时间间隔,而是浏览器通过系统获取并使用显示器刷新频率。例如在某些高频事件(resize,scroll 等)中,使用 requestAnimationFrame 可以防止在一个刷新间隔内发生多次函数执行,这样保证了流程度,也节省了开销
  2. 另外,该函数的延时效果是精确的,没有setTimeout或setInterval不准的情况(JS是单线程的,setTimeout 任务被放进异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列的任务是否需要开始执行,造成时间延时)。 setTimeout的执行只是在内存中对图像属性进行改变,这个改变必须要等到下次浏览器重绘时才会被更新到屏幕上。如果和屏幕刷新步调不一致,就可能导致中间某些帧的操作被跨越过去,直接更新下下一帧的图像。即 掉帧 使用 requestAnimationFrame 执行动画,最大优势是能保证回调函数在屏幕每一次刷新间隔中只被执行一次,这样就不会引起丢帧,动画也就不会卡顿
  3. 节省资源,节省开销 在之前介绍requestAnimationFrame执行过程,我们知道只有当页面激活的状态下,页面刷新任务才会开始,才执行 requestAnimationFrame,当页面隐藏或最小化时,会被暂停,页面显示,会继续执行。节省了 CPU 开销。 注意:当页面被隐藏或最小化时,定时器setTimeout仍在后台执行动画任务,此时刷新动画是完全没有意义的(实际上 FireFox/Chrome 浏览器对定时器做了优化:页面闲置时,如果时间间隔小于 1000ms,则停止定时器,与requestAnimationFrame行为类似。如果时间间隔>=1000ms,定时器依然在后台执行)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 在浏览器开发者工具的Console页执行下面代码。
    // 当开始输出count后,切换浏览器tab页,再切换回来,可以发现打印的值从离开前的值继续输出
    let count = 0;
    function requestAnimation() {
    if (count < 100) {
    count++;
    console.log(count);
    requestAnimationFrame(requestAnimation);
    }
    }
    requestAnimationFrame(requestAnimation);
  4. 能够在动画流刷新之后执行,即上一个动画流会完整执行

4. 实现

我们可以使用 requestAnimationFrame 实现setInterval及 setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// setInterval实现
function setInterval(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () ={
timer = window.requestAnimationFrame(loop)
endTime = now()
if (endTime - startTime >= interval) {
startTime = endTime = now()
callback(timer)
}
}
timer = window.requestAnimationFrame(loop)
return timer
}

let a = 0
setInterval(timer ={
console.log(a)
a++
if (a === 3) window.cancelAnimationFrame(timer)
}, 1000)
// 0
// 1
// 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// setTimeout 实现
function setTimeout(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () ={
timer = window.requestAnimationFrame(loop)
endTime = now()
if (endTime - startTime >= interval) {
callback(timer)
window.cancelAnimationFrame(timer)
}
}
timer = window.requestAnimationFrame(loop)
return timer
}

let a = 0
setTimeout(timer ={
console.log(a)
a++
}, 1000)
// 0

5. 参考

MDN

2月
24
更新于
2月24
2021
JS

ES6Proxy

发表于 2019-07-31 | 热度 ℃
| 字数统计: 1,586 (字) | 阅读时长: 7 (分钟)

Proxy,代理,是ES6新增的功能,可以理解为代理器(即由它代理某些操作)。

Proxy 对象用于定义或修改某些操作的自定义行为,可以在外界对目标对象进行访问前,对外界的访问进行改写。

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。


阅读全文 »
9月
07
更新于
9月07
2020
JS

EventLoop

发表于 2019-06-16 | 热度 ℃
| 字数统计: 2,091 (字) | 阅读时长: 9 (分钟)

描述事件队列的过程?

  • js是单线程的,会出现阻塞问题,因此有了任务队列的出现
  • 主线程同步执行任务,异步的工作一般会交给其他线程完成,然后回调函数会放到任务队列中
  • 等候主线程执行完毕后再执行任务队列中的操作

event-queue

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。只要异步任务有了运行结果,就在”任务队列”之 中放置一个事件。

task-queue 任务队列中存着的是异步任务,这些异步任务一定要等到执行栈清空后才会执行。

同步就是发出一个请求后什么事都不做,一直等待请求返回后才会继续做事;异步就是发出请求后继续去做其他事,这个请求处理完成后会通知你,这时候就可以处理这个回应了。


阅读全文 »
8月
24
更新于
8月24
2020
JS

PWA手记

发表于 2019-05-26 | 热度 ℃
| 字数统计: 3,523 (字) | 阅读时长: 14 (分钟)

PWA作为2018最火热的技术概念之一,对提升Web应用的安全、性能和体验有着很大的意义,非常值得我们去了解与学习。

PWA是Progressive Web App的英文缩写,也就是渐进式增强WEB应用。目的就是在移动端利用提供的标准化框架,在网页应用中实现和原生应用相近的用户体验。

一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的

安装和离线等功能。

我们需要理解的是,PWA不是某一项技术,或者某一个新的产物;而是一系列Web技术与标准的集合与应用。通过应用这些新的技术与标准,可以从安

全、性能和体验三个方面,优化我们的Web App。所以,其实PWA本质上依然是一个Web App。

阅读全文 »
7月
07
更新于
7月07
2020
JS

ES6Reflect对象

发表于 2019-05-02 | 热度 ℃
| 字数统计: 1,392 (字) | 阅读时长: 6 (分钟)

介绍

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

Reflect这个对象在新版本的chrome是支持的, ff比较早就支持Proxy和Reflect了,要让node支持Reflect可以安装harmony-reflect;

Reflect不是构造函数, 要使用的时候直接通过Reflect.method()调用, Reflect有的方法和Proxy差不多, 而且多数Reflect方法原生的Object已经重新实现了。

与大多数全局对象不同,Reflect没有构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。


阅读全文 »
3月
29
更新于
3月29
2020
JS

JS函数基础

发表于 2019-03-27 | 热度 ℃
| 字数统计: 1,895 (字) | 阅读时长: 7 (分钟)

函数的定义

函数其实就是一个封装一段代码段的对象,那函数名其实仅是用来引用函数对象的一个普通变量

写代码的时候我们避免不了要重复用一些代码,一直重复写很耗时,而且不美观也不利于维护,因此函数的出现就是来让代码重用,便于维护。

一段代码,可能被反复使用,可以定义为函数,然后调用函数来使用这段代码。

在JavaScript中函数就是对象。函数不同于其他对象的决定性特点是,函数存在一个被称为[[Call]]的内部属性。内部属性无法通过代码访问而是

定义了代码执行时的行为。ECMAScript为JavaScript的对象定义了多种内部属性,这些内部属性都用双重中括号来标注。

[[Call]]属性是函数独有的,表明该对象可以被执行。由于仅函数拥有该属性,ECMAScript定义了typeof操作符对任何具有[[Call]]属性的对

象返回 [object Function]


阅读全文 »
6月
02
更新于
6月02
2020
JS

switch的块级作用域

发表于 2019-03-10 | 热度 ℃
| 字数统计: 406 (字) | 阅读时长: 2 (分钟)

ES6 或 TS 引入了块级作用域,通过let和const、class等可以定义块级作用域里的变量,块级作用域内的变量不存在变量提升,且存在暂时性死区(在代码块内,使用let或const声明变量前该变量都是不可改变的)。常见的if语句,for循环的循环体内都可以定义块级变量。那么switch语句中的块级作用域是什么呢? 先给出结论:

switch语句中的块级作用域,在整个switch语句中,而不是对于每一个case生成一个独立的块级作用域。


阅读全文 »
11月
12
更新于
11月12
2019
JS

深入理解instanceof

发表于 2019-03-08 | 热度 ℃
| 字数统计: 530 (字) | 阅读时长: 2 (分钟)

在JS中,大家通常用typeof来判断基本类型,instanceof来判断引用类型。

typeof

typeof一般只能返回如下几个结果:number,boolean,string,function,object,undefined字符串

对于Array,null等特殊对象使用typeof一律返回object,而函数返回function这正是typeof的局限性。

在判断除Object类型的对象(基本类型)时比较方便。

instanceof

object instanceof constructor

instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

换种说法就是左侧的对象是否是右侧对象的实例。

相关练习

1
2
3
'123' instanceof String // true
let str = new String('123')
str instanceof String // true

阅读全文 »
3月
08
更新于
3月08
2019
JS

深入理解JS的继承

发表于 2019-03-07 | 热度 ℃
| 字数统计: 659 (字) | 阅读时长: 3 (分钟)
Summary
阅读全文 »
12
陈宇(cosyer)

陈宇(cosyer)

不去做的话永远也做不到。

159 日志
10 分类
51 标签
RSS
GitHub Twitter E-Mail FB Page
推荐阅读
  • Callmesoul
  • JsTips
  • Personal Site
  • Resume
© 2021 陈宇(cosyer)
终于等到你(UV):   |   欢迎再来(PV):
Blog总字数: 312.5k字
苏ICP备17005342号-1