【转载】nodejs 与 JavaScript 事件循环的差异

浏览器篇已经对事件循环机制和一些相关的概念作了详细介绍,但主要是针对浏览器端的研究,Node环境是否也一样呢?先看一个demo:
setTimeout(()=>{
console.log(‘timer1’)
Promise.resolve().then(function(){
console.log(‘promise1’)
})
},0)
setTimeout(()=>{
console.log(‘timer2’)
Promise.resolve().then(function(){
console.log(‘promise2’)
})
},0)
肉眼编译运行一下,蒽,在浏览器的结果就是下面这个了,道理都懂,就不累述了。
timer1
promise1
timer2
promise2
那么Node下执行看看,咦。。。奇怪,跟浏览器的运行结果并不一样~
timer1
timer2
promise1
promise2
例子说明,浏览器和 Node.js 的事件循环机制是有区别的,一起来看个究竟吧~

Node.js的事件处理

Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现,核心源码参考
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
// timers阶段
uv__run_timers(loop);
// I/O callbacks阶段
ran_pending = uv__run_pending(loop);
// idle阶段
uv__run_idle(loop);
// prepare阶段
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// poll阶段
uv__io_poll(loop, timeout);
// check阶段
uv__run_check(loop);
// close callbacks阶段
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
根据Node.js官方介绍,每次事件循环都包含了6个阶段,对应到 libuv 源码中的实现,如下图所示
添加图片
 
  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行setImmediate()的回调
  • close callbacks 阶段:执行socket的close事件回调
我们重点看timers、poll、check这3个阶段就好,因为日常开发中的绝大部分异步任务都是在这3个阶段处理的。

timers 阶段

timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout()和setImmediate()的执行顺序是不确定的。
setTimeout(()=>{
console.log(‘timeout’)
},0)
setImmediate(()=>{
console.log(‘immediate’)
})
但是把它们放到一个I/O回调里面,就一定是setImmediate()先执行,因为poll阶段后面就是check阶段。

poll 阶段

poll 阶段主要有2个功能:
  • 处理 poll 队列的事件
  • 当有已超时的 timer,执行它的回调函数
even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:
  1. 若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
  2. 若没有预设的setImmediate(),event loop将阻塞在该阶段等待
注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。

check 阶段

setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

小结

  • event loop 的每个阶段都有一个任务队列
  • 当 event loop 到达某个阶段时,将执行该阶段的任务队列,直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段
  • 当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick
讲得好有道理,可是没有demo我还是理解不全啊,憋急,now!
constfs=require(‘fs’)
fs.readFile(‘test.txt’,()=>{
console.log(‘readFile’)
setTimeout(()=>{
console.log(‘timeout’)
},0)
setImmediate(()=>{
console.log(‘immediate’)
})
})
执行结果应该都没有疑问了
readFile
immediate
timeout

Node.js 与浏览器的 Event Loop 差异

回顾上一篇,浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。
添加图片
 浏览器端
而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
添加图片
Node.js端

demo回顾

回顾文章最开始的demo,全局脚本(main())执行,将2个timer依次放入timer队列,main()执行完毕,调用栈空闲,任务队列开始执行;
添加图片
Node.js下的处理过程
首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
对比浏览器端的处理过程:
添加图片
 Browser下的处理过程
process.nextTick() VS setImmediate()
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate()
来自官方文档有意思的一句话,从语义角度看,setImmediate()应该比process.nextTick()先执行才对,而事实相反,命名是历史原因也很难再变。
process.nextTick()会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用process.nextTick(),会导致出现I/O starving(饥饿)的问题,比如下面例子的readFile已经完成,但它的回调一直无法执行:
constfs=require(‘fs’)
conststarttime=Date.now()
letendtime
fs.readFile(‘text.txt’,()=>{
endtime=Date.now()
console.log(‘finish reading time: ‘,endtime-starttime)
})
letindex=0
functionhandler(){
if(index++>=1000)return
console.log(`nextTick ${index}`)
process.nextTick(handler)
// console.log(`setImmediate ${index}`)
// setImmediate(handler)
}
handler()
process.nextTick()的运行结果:
nextTick 1
nextTick 2
……
nextTick 999
nextTick 1000
finish reading time: 170
替换成setImmediate(),运行结果:
setImmediate 1
setImmediate 2
finish reading time: 80
……
setImmediate 999
setImmediate 1000
这是因为嵌套调用的setImmediate()回调,被排到了下一次event loop才执行,所以不会出现阻塞。

总结

  1. Node.js 的事件循环分为6个阶段
  2. 浏览器和Node 环境下,microtask任务队列的执行时机不同
    • Node.js中,microtask在事件循环的各个阶段之间执行
    • 浏览器端,microtask在事件循环的macrotask执行完之后执行
  3. 递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()
[参考资料]

发表在 JavaScript, NodeJs | 留下评论

H5-postMessage使用

1.不跨域

// parent.html
<iframe src="http://www.duba.com/son2.html" id="son2" style="display:none"></iframe>
<script>
    function concat() {
        console.log('----parent-post-to-son2');
        var iframe = document.getElementById("son2").contentWindow;
        iframe.postMessage('test', 'http://www.duba.com');
    }
    setTimeout(() => {
        concat();
    }, 1000);
    
    window.addEventListener("message", function(event) {        
        console.log('---parent-message:' + event.data);  
        console.log('---parent-url:' + location.href);  
    }, false);
</script>

// son2.html
<script>
 window.addEventListener("message", function( event ) {
    console.log('---son2-message:' + event.data);
    console.log('---son2-url:' + location.href);
}, false);
</script>

结果如下
—parent-message:
—parent-url:http://www.duba.com/parent.html
—-parent-post-to-son2
—son2-message:test
—son2-url:http://www.duba.com/son2.html

2.跨域改造:

// parent.html
<iframe src="http://www.test2.com/son2.html" id="son2" style="display:none"></iframe>
<script>
    function concat() {
        console.log('----parent-post-to-son2');
        var iframe = document.getElementById("son2").contentWindow;
        iframe.postMessage('test', 'http://www.test2.com');
    }
    setTimeout(() => {
        concat();
    }, 1000);
    
    window.addEventListener("message", function(event) {        
        console.log('---parent-message:' + event.data);  
        console.log('---parent-url:' + location.href);  
    }, false);
</script>

// son2.html
<script>
 window.addEventListener("message", function( event ) {
    console.log('---son2-message:' + event.data);
    console.log('---son2-url:' + location.href);
}, false);
</script>

结果如下
—parent-message:
—parent-url:http://www.duba.com/parent.html
—parent-post-to-son2
—son2-message:test
—son2-url:http://www.test2.com/son2.html

为什么要指定targetorigin呢,既然指定了向某个窗口发送消息,不指定targetorigin是否可以呢?继续改造
3.targetorigin的作用

// parent.html
<iframe src="http://www.test.com/son2.html" id="son2" style="display:none"></iframe>
<script>
    function concat() {
        console.log('----parent-post-to-son2');
        var iframe = document.getElementById("son2").contentWindow;
        iframe.postMessage('test', 'http://www.test.com');
    }
    setTimeout(() => {
        concat();
    }, 1000);
    
    window.addEventListener("message", function(event) {        
        console.log('---parent-message:' + event.data);  
        console.log('---parent-url:' + location.href);  
    }, false);
</script>

// son2.html
<script>
 window.addEventListener("message", function( event ) {
    console.log('---son2-message:' + event.data);
    console.log('---son2-url:' + location.href);
}, false);
</script>

配置如下:
http://www.test.com/son2.html 301到http://www.test2.com/son2.html
结果如下:
—parent-message:
—parent-url:http://www.duba.com/parent.html
—parent-post-to-son2
可以看到son2页面未收到消息,为什么向指定窗口发送消息没收到呢?就是因为targetorigin的作用啦。监听message的页面的origin与targetorigin不同,继续改造

// parent.html
<iframe src="http://www.test.com/son2.html" id="son2" style="display:none"></iframe>
<script>
    function concat() {
        console.log('----parent-post-to-son2');
        var iframe = document.getElementById("son2").contentWindow;
        iframe.postMessage('test', 'http://www.test2.com');
    }
    setTimeout(() => {
        concat();
    }, 1000);
    
    window.addEventListener("message", function(event) {        
        console.log('---parent-message:' + event.data);  
        console.log('---parent-url:' + location.href);  
    }, false);
</script>

// son2.html
<script>
 window.addEventListener("message", function( event ) {
    console.log('---son2-message:' + event.data);
    console.log('---son2-url:' + location.href);
}, false);
</script>

配置如下:
http://www.test.com/son2.html 301到http://www.test2.com/son2.html
结果如下:
—parent-message:
—parent-url:http://www.duba.com/parent.html
—parent-post-to-son2
—son2-message:test
—son2-url:http://www.test2.com/son2.html
可以看到son2成功收到消息。所以发送消息指向的是窗口,而targetorigin指向监听message页面的origin

发表在 HTML | 留下评论

【转】掌握 JS Stack Trace

在使用 Roadhog 编译过程中,我的终端崩掉了,怀着好奇的心理,我找到了 JS Stack Trace 这样的提示,怎么理解呢,这节我们来探究一下。

本文并不会告诉你“终端崩溃了,如何解决?”,我只会围绕 “JS Stack Trace” 异常来分析,我们可能碰到了什么问题,以及需要了解的相关技术栈。

一、队列(Queue)与栈(Stack)

最早是在数据结构的线性表中接触到队列(Queue)与栈(Stack),我们先来回顾一下概念:

  • 栈的插入和删除操作只允许在表的尾端进行(在栈中成为“栈顶”),满足 “FIFO:First In Last Out”;
  • 队列只允许在表尾插入数据元素,在表头删除数据元素,满足 “First In First Out”。

这里我们不去纠结栈与队列的异同,而只去用概念去理解 JS 中 Stack。

二、Stack

我们先来看这样一段代码,试想一下输出结果,以及它是如何调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function c() {
    console.log('c');
}

function b() {
    console.log('b');
    c();
}

function a() {
    console.log('a');
    b();
}

a();

对上面的代码做一下简短的分析:

  • 当调用 a 的时候,它会被压到栈顶;
  • 然后,当 b 在 a 中被调用的时候,它会被继续压入栈顶,当 c 在 b 中被调用的时候,也一样;
  • 在运行 c 的时候,栈中包含了 a,b,c,并且其顺序也是 a,b,c;
  • 当 c 调用完毕时,它会被从栈顶移出,随后控制流回到 b。当 b 执行完毕后也会从栈顶移出,控制流交还到 a。最后,当 a 执行完毕后也会从栈中移出。

每当有一个函数调用,就会将其压入栈顶。在调用结束的时候再将其从栈顶移出。

补充一点: 我们可以用 console.trace() 将 Stack Trace 打印到控制台。

三、Error

当 Error 发生的时候,通常会抛出一个 Error 对象。Error 对象也可以被看做一个 Error原型,用户可以扩展其含义,以创建自己的 Error 对象。

Error.prototype 对象通常包含下面属性:

  • constructor – 一个错误实例原型的构造函数
  • message – 错误信息
  • name – 错误名称

一个 Error 的堆栈追踪包含了从其构造函数开始的所有堆栈帧。

四、Stack Trace

好了,介绍了一些基础点,这里重新回到我们的重点:Stack Trace – 堆栈追踪。

Error.captueStackTrace 函数的第一个参数是一个 object 对象,第二个参数是一个可选的 function。捕获堆栈跟踪所做的是要捕获当前堆栈的路径(这是显而易见的),并且在 object对象上创建一个 stack 属性来存储它。如果提供了第二个 function 参数,那么这个被传递的函数将会被看成是本次堆栈调用的终点,本次堆栈跟踪只会展示到这个函数被调用之前。

我们来用几个例子来更清晰的解释下。我们将捕获当前堆栈路径并且将其存储到一个普通 object 对象中。

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
const myObj = {};

function c() {
}

function b() {
    // 这里存储当前的堆栈路径,保存到myObj中
    Error.captureStackTrace(myObj);
    c();
}

function a() {
    b();
}

// 首先调用这些函数
a();

// 这里,我们看一下堆栈路径往 myObj.stack 中存储了什么
console.log(myObj.stack);

// 这里将会打印如下堆栈信息到控制台
//    at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack
//    at a (repl:2:1)
//    at repl:1:1 <-- Node internals below this line
//    at realRunInThisContextScript (vm.js:22:35)
//    at sigintHandlersWrap (vm.js:98:12)
//    at ContextifyScript.Script.runInThisContext (vm.js:24:12)
//    at REPLServer.defaultEval (repl.js:313:29)
//    at bound (domain.js:280:14)
//    at REPLServer.runBound [as eval] (domain.js:293:12)
//    at REPLServer.onLine (repl.js:513:10)

我们从上面的例子中可以看到,我们首先调用了 a( a 被压入栈),然后从 a 的内部调用了 b( b 被压入栈,并且在 a 的上面)。在 b 中,我们捕获到了当前堆栈路径并且将其存储在了 myObj 中。这就是为什么打印在控制台上的只有 a 和 b,而且是下面 a 上面 b。

深入了解请查看:深入理解 JavaScript Errors 和 Stack Traces

五、作用域链

提到作用域链,不得不说说 JS 中的作用域:

作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,在 JS 中变量的作用域有全局作用域和局部作用域。

很好的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
var a=3; //全局变量
function fn(b){ //局部变量
    c=2; //全局变量
    var d=5; //局部变量
    function subFn(){
      var e=d; //父函数的局部变量对子函数可见
      for(var i=0;i<3;i++){
          console.write(i);
      }
      console.log(i);//3, 在for循环内声明,循环外function内仍然可见,没有块作用域
    }
}
console.log(c); //在function内声明但不带var修饰,仍然是全局变量

JS 并没有块及的作用域,只有函数级作用域:变量在声明它们的函数体及其子函数内是可见的。

执行环境(execution context)定义了变量或函数有权访问的其它数据,决定了它们的各自行为。每个执行环境都有一个与之关联的变量对象(variable object, VO),执行环境中定义的所有变量和函数都会保存在这个对象中,解析器在处理数据的时候就会访问这个内部对象。

全局执行环境是最外层的一个执行环境,在 web 浏览器中全局执行环境是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和放大创建的。

每个函数都有自己的执行环境,当执行流进入一个函数的时候,函数的环境会被推入一个函数栈中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

理解了上面这些内容,我们就不难理解作用域链了:

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)来保证对执行环境有权访问的变量和函数的有序访问。

作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。

此处摘录自:JavaScript 作用域链

六、JS 执行机制

从上面我们可以看出,每当有一个函数调用,就会将其压入栈顶,在调用结束的时候再将其从栈顶移出。那这些方法是如何执行的呢,我们就需要了解一下 JavaScript 执行机制,此处用原文中的图来做一下说明:
JS 执行机制
简单分析一下上图中的流程:

  • 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入 Event Table并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop(事件循环)。

举个例子说明一下:

1
2
3
4
5
6
7
8
$.ajax({
  url: 'www.jartto.wang',
  data: {test: 'jarro'},
  success: () => {
      console.log('异步请求');
  }
})
console.log('主线程同步输出');
  • ajax 进入 Event Table,注册回调函数 success
  • 执行 console.log(‘代码执行结束’)。
  • ajax 事件完成,回调函数 success 进入 Event Queue
  • 主线程从 Event Queue 读取回调函数 success 并执行。

上面的例子很简单,我们继续深入。

七、垃圾回收

1.我们先来看看垃圾回收的意义:
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JS 程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JS 的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

2.JavaScript 垃圾回收的机制:

找出不再使用的变量,然后释放掉其占用的内存。由于这个过程不是实时的,而且开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

那么垃圾回收器是怎么工作的,常见的方式有两种,如下:

  • 标记清除
    这是 Javascript 中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
    垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
  • 引用计数
    另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

“引用计数”也是不错的垃圾回收方式,为什么很少有浏览器采用,还会带来内存泄露问题呢?

因为“引用计数”方式没办法解决循环引用问题。

这里举一个例子:

1
2
3
4
5
6
function test(){
    var a = {};
    var b = {};
    a.prop = b;
    b.prop = a;
}

这样 a 和 b 的引用次数都是 2,即使在 test() 执行完成后,两个对象都已经离开环境,在标记清除的策略下是没有问题的,离开环境的就被清除,但是在引用计数策略下不行,因为这两个对象的引用次数仍然是 2,不会变成 0,所以其占用空间不会被清理。

如果这个函数被多次调用,这样就会不断地有空间不会被回收,造成内存泄露。

3.什么时候触发垃圾回收?
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很困难,确定垃圾回收时间间隔就变成了一个值得思考的问题。

我们可以手动去触发垃圾回收,防止内存泄漏:

1
2
a.prop = null;
b.prop = null;

如果这里写的不清楚,可以看看下面这两篇文章:

八、内存泄漏

上面反复提到了内存泄漏,什么会导致内存泄漏,我们又能做什么来避免这种情况呢?
1.什么是内存泄漏?

  • 程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
  • 对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
  • 不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

2.如何识别内存泄漏?
经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。

3.如何避免?
最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。

ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个 "Weak",表示这是弱引用。

弱引用,不会被计入垃圾回收机制。

更多内容请移步阮一峰老师文章:JavaScript 内存泄漏教程

 

 

文章来源:掌握 JS Stack Trace

 

发表在 JavaScript | 留下评论

关于img元素设置高度

关于img元素
img元素的默认display值是inline,但是一般设置为block,MDN也是这么建议。

虽然设置img元素display: block;,但是其表现并不是完全按照块状元素,“可替换元素”有着自己的一套宽高的计算方式。默认情况下,图片是1:1的形式显示在页面上,并没有像div之类的块状元素一样自动铺满整个宽度。因此,为了防止图像宽度超出页面,一般需要再设置max-width: 100%;。

img元素默认宽高比例是和图片保持一致的,即当你只设置了宽度时,页面会自动计算对应的高度并显示图片,单独设置高度时也是一样的计算;如果你同时设置了宽度和高度,则按照你设置的进行显示。

参考文章:
使用img自动撑开高度

发表在 CSS | 留下评论

chrome 模拟尺寸与真机尺寸差别

chrome 模拟尺寸与真机尺寸差别

alert(window.innerWidth);
alert(window.innerHeight);
标准模式下:
375 603 微信 等APP
375 553 safrai
375 559 app
375 667官方 模拟

375*667 指的是屏幕的尺寸。
chrome模拟的时候,模拟的是全屏的尺寸。
而正常真机在展现的时候有上下菜单,也占用一部分尺寸

放大模式下:
320 504 微信 等APP
320 454 safrai
320 460 app
375 667官方 模拟
发现放大模式下 高度占比变小

参考文章:
查看屏幕尺寸
移动端H5页面的设计稿尺寸(下)

发表在 CSS | 留下评论

浏览器对高度的分配

浏览器对高度的分配

浏览器负责分配块级元素宽度,那么浏览器也一定可以分配高度(只是没有做),那么浏览器本身是有宽度和高度的,设置html的height:100%,就可以获取浏览器的定高了,后面的body和div也就有了依赖。

我知道一个事实:一个div块级元素没有主动为其设置宽度和高度,浏览器会为其分配可使用的最大宽度(比如全屏宽度),但是不负责分配高度,块级元素的高度是由子元素堆砌撑起来的。那么,html和body标签的高度也都是由子级元素堆砌撑起来的。

元素高度百分比需要向上遍历父标签要找到一个定值高度才能起作用,如果中途有个height为auto或是没有设置height属性,则高度百分比不起作用

在使用height: 100%;时需要注意的一些事项
1、Margins 和 padding 会让你的页面出现滚动条,也许这是你不希望的。

2、如果你的元素实际高度大于你设定的百分比高度,那元素的高度会自动扩展。

参考文章:
由html,body{height:100%}引发的对html和body的思考

如何让 height:100%; 起作用

发表在 CSS | 留下评论

vConsole

vConsole 是一个由微信公众平台前端团队研发的 Web 前端开发者面板,可用于展示 console 日志,方便开发、调试。

vConsole安装及使用
1.安装

npm install vconsole --save

2.使用

var Vconsole = require('vconsole');
var vConsole = new Vconsole();

3.效果
236272ed20910baf7bfed871c

发表在 工具, 移动端 | 留下评论

【转载】移动端1px实现

网上其实有很多关于1px的实现方法拉。其实关于1px的实现我之前有在实习的时候项目中使用过,但是一直没有对移动端1px的实现做一个小总结。今天呢,就来稍微总结一下吧,给自己做一个备忘录的同时也希望能够给予知友的帮助。
* • 1px实现的难点
* • 1px实现方案
* • height: 0.5px
* • scale
* • linear-gradient
* • box-shadow
* • svg
* • viewport
1px实现的难点

我们知道,像素可以分为物理像素(CSS像素)和设备像素。由于现在手机大部分是Retina高清屏幕,所以在PC端和移动端存在设备像素比的概念。简单说就是你在pc端看到的1px和在移动端看到的1px是不一样的。
在PC端上,像素可以称为CSS像素,PC端上dpr为1。也就说你书写css样式是是多少在pc上就显示多少。而在移动端上,像素通常使用设备像素。往往PC端和移动端上在不做处理的情况下1px显示是不同的。
一个物理像素等于多少个设备像素取决于移动设备的屏幕特性(是否是Retina)和用户缩放比例。
如果是Retina高清屏幕,那么dpr的值可能为2或者3,那么当你在pc端上看到的1px时,在移动端上看到的就会是2px或者3px。
由于业务需求,我们需要一些方法来实现移动端上的1px。
1px实现方法

0.5px

.px05 {
border-bottom: 0.5px solid #000;
// height: 0.5px;
// background-color: #000;
}
可以看到chrome和firefox都会把0.5px转换为1px。目前看来0.5px的方法暂时不行。未来希望可以实现吧。
scale

如果在一个元素上使用scale时会导致整个元素同时缩放,所以应该在该元素的伪元素下设置scale属性。
.scale::after {
display: block;
content: ”;
border-bottom: 1px solid #000;
transform: scaleY(.5);
}
可以看到,使用scale属性将border垂直方向缩小了50%之后可以实现1px的线。而且transform: scale属性在ie9+以上的浏览器都支持。其他主流浏览器兼容性也不错。
linear-gradient

通过线性渐变,也可以实现移动端1px的线。原理大致是使用渐变色,上部分为白色,下部分为黑色。这样就可以将线从视觉上看只有1px。
由于是通过背景颜色渐变实现的,所以这里要使用伪元素并且设置伪元素的高度。 当然,也可以不使用伪元素,但是就会增加一个没有任何意义的空标签了。
div.linear::after {
display: block;
content: ”;
height: 1px;
background: linear-gradient(0, #fff, #000);
}
linear-gradient属性ie10+以上支持。其他主流浏览器兼容性都不错。
box-shadow

通过box-shaodow来实现1px也可以,实现原理是将纵坐标的shadow设置为0.5px即可。box-shadow属性在Chrome和Firefox下支持小数设置,但是在Safari下不支持。所以使用该方法设置移动端1px时应该慎重使用。
div.shadow {
box-shadow: 0 0.5px 0 0 #000;
}
同样的,box-shadow浏览器兼容性不错。
svg

另外,可以使用可缩放矢量图形(svg)来实现。由于是矢量图形,因此在不同设备屏幕特性下具有伸缩性。
.svg::after {
display: block;
content: ”;
height: 1px;
background-image: url(‘data:image/svg+xml;utf-8,‘);
}
在Chrome下可以显示移动端的1px,但是由于Firefox的background-image如果是svg的话,颜色的命名形式只支持英文书写方式,如’black, red’等,所以在Fire下没有1px的线。
解决Firefox下background-image的svg颜色命名问题很简单。可以使用black这种命名方式或者将其转换成base64.
方法一:颜色不采用十六进制,而是用英文方式
.svg::after {
display: block;
content: ”;
height: 1px;
background-image: url(‘data:image/svg+xml;utf-8,‘);
}

方法二:将svg改为base64
.svg::after {
display: block;
content: ”;
height: 1px;
background-image: url(‘data:image/svg+xml;utf-8,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iMTAwJSIgeTI9IjAiIHN0cm9rZT0iYmxhY2siPjwvbGluZT48L3N2Zz4=’);
}
使用以上两种方式即可实现在Firefox显示1px的线。svg的浏览器兼容性还是不错的。
当然,以上的所有方案都是基于dpr=2的情况下实现的,此外还需要考虑dpr=3的情况。因此,可以根据media query来判断屏幕特性,根据不同的屏幕特性进行适当的设置。如下是根据不同的dpr设置scale属性。
@media all and (-webkit-min-device-pixel-ratio: 2) {
.scale::after {
display: block;
content: ”;
border-bottom: 1px solid #000;
transform: scaleY(.5);
}
}

@media all and (-webkit-min-device-pixel-ratio: 3) {
.scale::after {
display: block;
content: ”;
border-bottom: 1px solid #000;
transform: scaleY(.333);
}
}
viewport

当然了,也可以通过meta视口标签根据不同的dpr对不同的页面进行缩放。这需要通过JS进行动态的设置。简单的说,假如想设置1px的线,在dpr=2情况下,页面就缩放到原来的一半,此时就会正常显示1px的线;在dpr=3的情况下,页面就缩放到原来的三分之一,此时也能够正常显示1px的线。具体实现方式如下
const dpr = window.devicePixelRatio
const meta = document.createElement(‘meta’) // 创建meta视口标签
meta.setAttribute(‘name’, ‘viewport’) // 设置name为viewport
meta.setAttribute(‘content’, `width=device-width, user-scalable=no, initial-scale=${1/dpr}, maximum-scale=${1/dpr}, minimum-scale=${1/dpr}`) // 动态初始缩放、最大缩放、最小缩放比例

以上呢,就是在移动端实现1px的一些方案了。总的来说,可以通过transform: scale对伪元素进行缩放,也可以通过linear-gradient对背景进行线性渐变。另外可以通过box-shadow在伪元素进行阴影设置。也可以使用svg,但是要注意将颜色的设置或者将其转换为base64。最后可以根据dpr动态将页面进行缩放。

来源:
移动端1px实现

发表在 CSS | 留下评论

ES6箭头函数

var id = 'Global';

function fun1() {
    // setTimeout中使用普通函数
    setTimeout(function(){
        console.log(this.id);
    }, 2000);
}

function fun2() {
    // setTimeout中使用箭头函数
    setTimeout(() =&gt; {
        console.log(this.id);
    }, 2000)
}

fun1.call({id: 'Obj'});     // 'Global'

fun2.call({id: 'Obj'});     // 'Obj'

上面这个例子,函数fun1中的setTimeout中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this指向Window对象,this.id就指向全局变量id,所以输出’Global’。
但是函数fun2中的setTimeout中使用的是箭头函数,这个箭头函数的this在定义时就确定了,它继承了它外层fun2的执行环境中的this,而fun2调用时this被call方法改变到了对象{id: ‘Obj’}中,所以输出’Obj’。

作者:MagicEyes
链接:https://juejin.im/post/5c979300e51d456f49110bf0
来源:掘金

发表在 JavaScript | 留下评论

【转载】Canvas 最佳实践(性能篇)

Canvas 最佳实践(性能篇)

Canvas 想必前端同学们都不陌生,它是 HTML5 新增的「画布」元素,允许我们使用 JavaScript 来绘制图形。目前,所有的主流浏览器都支持 Canvas。

Canvas 兼容性

Canvas 最常见的用途是渲染动画。渲染动画的基本原理,无非是反复地擦除和重绘。为了动画的流畅,留给我渲染一帧的时间,只有短短的 16ms。在这 16ms 中,我不仅需要处理一些游戏逻辑,计算每个对象的位置、状态,还需要把它们都画出来。如果消耗的时间稍稍多了一些,用户就会感受到「卡顿」。所以,在编写动画(和游戏)的时候,我无时无刻不担忧着动画的性能,唯恐对某个 API 的调用过于频繁,导致渲染的耗时延长。

为此,我做了一些实验,查阅了一些资料,整理了平时使用 Canvas 的若干心得体会,总结出这一片所谓的「最佳实践」。如果你和我有类似的困扰,希望本文对你有一些价值。

本文仅讨论 Canvas 2D 相关问题。

计算与渲染
把动画的一帧渲染出来,需要经过以下步骤:

计算:处理游戏逻辑,计算每个对象的状态,不涉及 DOM 操作(当然也包含对 Canvas 上下文的操作)。
渲染:真正把对象绘制出来。
2.1. JavaScript 调用 DOM API(包括 Canvas API)以进行渲染。
2.2. 浏览器(通常是另一个渲染线程)把渲染后的结果呈现在屏幕上的过程。

之前曾说过,留给我们渲染每一帧的时间只有 16ms。然而,其实我们所做的只是上述的步骤中的 1 和 2.1,而步骤 2.2 则是浏览器在另一个线程(至少几乎所有现代浏览器是这样的)里完成的。动画流畅的真实前提是,以上所有工作都在 16ms 中完成,所以 JavaScript 层面消耗的时间最好控制在 10ms 以内。

虽然我们知道,通常情况下,渲染比计算的开销大很多(3~4 个量级)。除非我们用到了一些时间复杂度很高的算法(这一点在本文最后一节讨论),计算环节的优化没有必要深究。

我们需要深入研究的,是如何优化渲染的性能。而优化渲染性能的总体思路很简单,归纳为以下几点:

在每一帧中,尽可能减少调用渲染相关 API 的次数(通常是以计算的复杂化为代价的)。
在每一帧中,尽可能调用那些渲染开销较低的 API。
在每一帧中,尽可能以「导致渲染开销较低」的方式调用渲染相关 API。
Canvas 上下文是状态机
Canvas API 都在其上下文对象 context 上调用。

var context = canvasElement.getContext(‘2d’);
我们需要知道的第一件事就是,context 是一个状态机。你可以改变 context 的若干状态,而几乎所有的渲染操作,最终的效果与 context 本身的状态有关系。比如,调用 strokeRect 绘制的矩形边框,边框宽度取决于 context 的状态 lineWidth,而后者是之前设置的。

context.lineWidth = 5;
context.strokeColor = ‘rgba(1, 0.5, 0.5, 1)’;

context.strokeRect(100, 100, 80, 80);

说到这里,和性能貌似还扯不上什么关系。那我现在就要告诉你,对 context.lineWidth 赋值的开销远远大于对一个普通对象赋值的开销,你会作如何感想。

当然,这很容易理解。Canvas 上下文不是一个普通的对象,当你调用了 context.lineWidth = 5 时,浏览器会需要立刻地做一些事情,这样你下次调用诸如 stroke 或 strokeRect 等 API 时,画出来的线就正好是 5 个像素宽了(不难想象,这也是一种优化,否则,这些事情就要等到下次 stroke 之前做,更加会影响性能)。

我尝试执行以下赋值操作 106 次,得到的结果是:对一个普通对象的属性赋值只消耗了 3ms,而对 context 的属性赋值则消耗了 40ms。值得注意的是,如果你赋的值是非法的,浏览器还需要一些额外时间来处理非法输入,正如第三/四种情形所示,消耗了 140ms 甚至更多。

somePlainObject.lineWidth = 5; // 3ms (10^6 times)
context.lineWidth = 5; // 40ms
context.lineWidth = ‘Hello World!’; // 140ms
context.lineWidth = {}; // 600ms
对 context 而言,对不同属性的赋值开销也是不同的。lineWidth 只是开销较小的一类。下面整理了为 context 的一些其他的属性赋值的开销,如下所示。

属性 开销 开销(非法赋值)
line[Width/Join/Cap] 40+ 100+
[fill/stroke]Style 100+ 200+
font 1000+ 1000+
text[Align/Baseline] 60+ 100+
shadow[Blur/OffsetX] 40+ 100+
shadowColor 280+ 400+
与真正的绘制操作相比,改变 context 状态的开销已经算比较小了,毕竟我们还没有真正开始绘制操作。我们需要了解,改变 context 的属性并非是完全无代价的。我们可以通过适当地安排调用绘图 API 的顺序,降低 context 状态改变的频率。

分层 Canvas
分层 Canvas 在几乎任何动画区域较大,动画较复杂的情形下都是非常有必要的。分层 Canvas 能够大大降低完全不必要的渲染性能开销。分层渲染的思想被广泛用于图形相关的领域:从古老的皮影戏、套色印刷术,到现代电影/游戏工业,虚拟现实领域,等等。而分层 Canvas 只是分层渲染思想在 Canvas 动画上最最基本的应用而已。

分层 Canvas

分层 Canvas 的出发点是,动画中的每种元素(层),对渲染和动画的要求是不一样的。对很多游戏而言,主要角色变化的频率和幅度是很大的(他们通常都是走来走去,打打杀杀的),而背景变化的频率或幅度则相对较小(基本不变,或者缓慢变化,或者仅在某些时机变化)。很明显,我们需要很频繁地更新和重绘人物,但是对于背景,我们也许只需要绘制一次,也许只需要每隔 200ms 才重绘一次,绝对没有必要每 16ms 就重绘一次。

对于 Canvas 而言,能够在每层 Canvas 上保持不同的重绘频率已经是最大的好处了。然而,分层思想所解决的问题远不止如此。

使用上,分层 Canvas 也很简单。我们需要做的,仅仅是生成多个 Canvas 实例,把它们重叠放置,每个 Canvas 使用不同的 z-index 来定义堆叠的次序。然后仅在需要绘制该层的时候(也许是「永不」)进行重绘。

var contextBackground = canvasBackground.getContext(‘2d’);
var contextForeground = canvasForeground.getContext(‘2d’);

function render(){
drawForeground(contextForeground);
if(needUpdateBackground){
drawBackground(contextBackground);
}
requestAnimationFrame(render);
}
记住,堆叠在上方的 Canvas 中的内容会覆盖住下方 Canvas 中的内容。

绘制图像
目前,Canvas 中使用到最多的 API,非 drawImage 莫属了。(当然也有例外,你如果要用 Canvas 写图表,自然是半句也不会用到了)。

drawImage 方法的格式如下所示:

context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

数据源与绘制的性能
由于我们具备「把图片中的某一部分绘制到 Canvas 上」的能力,所以很多时候,我们会把多个游戏对象放在一张图片里面,以减少请求数量。这通常被称为「精灵图」。然而,这实际上存在着一些潜在的性能问题。我发现,使用 drawImage 绘制同样大小的区域,数据源是一张和绘制区域尺寸相仿的图片的情形,比起数据源是一张较大图片(我们只是把数据扣下来了而已)的情形,前者的开销要小一些。可以认为,两者相差的开销正是「裁剪」这一个操作的开销。

我尝试绘制 104 次一块 320×180 的矩形区域,如果数据源是一张 320×180 的图片,花费了 40ms,而如果数据源是一张 800×800 图片中裁剪出来的 320×180 的区域,需要花费 70ms。

虽然看上去开销相差并不多,但是 drawImage 是最常用的 API 之一,我认为还是有必要进行优化的。优化的思路是,将「裁剪」这一步骤事先做好,保存起来,每一帧中仅绘制不裁剪。具体的,在「离屏绘制」一节中再详述。

视野之外的绘制
有时候,Canvas 只是游戏世界的一个「窗口」,如果我们在每一帧中,都把整个世界全部画出来,势必就会有很多东西画到 Canvas 外面去了,同样调用了绘制 API,但是并没有任何效果。我们知道,判断对象是否在 Canvas 中会有额外的计算开销(比如需要对游戏角色的全局模型矩阵求逆,以分解出对象的世界坐标,这并不是一笔特别廉价的开销),而且也会增加代码的复杂程度,所以关键是,是否值得。

我做了一个实验,绘制一张 320×180 的图片 104 次,当我每次都绘制在 Canvas 内部时,消耗了 40ms,而每次都绘制在 Canvas 外时,仅消耗了 8ms。大家可以掂量一下,考虑到计算的开销与绘制的开销相差 2~3 个数量级,我认为通过计算来过滤掉哪些画布外的对象,仍然是很有必要的。

离屏绘制
上一节提到,绘制同样的一块区域,如果数据源是尺寸相仿的一张图片,那么性能会比较好,而如果数据源是一张大图上的一部分,性能就会比较差,因为每一次绘制还包含了裁剪工作。也许,我们可以先把待绘制的区域裁剪好,保存起来,这样每次绘制时就能轻松很多。

drawImage 方法的第一个参数不仅可以接收 Image 对象,也可以接收另一个 Canvas 对象。而且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎完全一致。我们只需要实现将对象绘制在一个未插入页面的 Canvas 中,然后每一帧使用这个 Canvas 来绘制。

// 在离屏 canvas 上绘制
var canvasOffscreen = document.createElement(‘canvas’);
canvasOffscreen.width = dw;
canvasOffscreen.height = dh;
canvasOffscreen.getContext(‘2d’).drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);

// 在绘制每一帧的时候,绘制这个图形
context.drawImage(canvasOffscreen, x, y);
离屏绘制的好处远不止上述。有时候,游戏对象是多次调用 drawImage 绘制而成,或者根本不是图片,而是使用路径绘制出的矢量形状,那么离屏绘制还能帮你把这些操作简化为一次 drawImage 调用。

第一次看到 getImageData 和 putImageData 这一对 API,我有一种错觉,它们简直就是为了上面这个场景而设计的。前者可以将某个 Canvas 上的某一块区域保存为 ImageData 对象,后者可以将 ImageData 对象重新绘制到 Canvas 上面去。但实际上,putImageData 是一项开销极为巨大的操作,它根本就不适合在每一帧里面去调用。

避免「阻塞」
所谓「阻塞」,可以理解为不间断运行时间超过 16ms 的 JavaScript 代码,以及「导致浏览器花费超过 16ms 时间进行处理」的 JavaScript 代码。即使在没有什么动画的页面里,阻塞也会被用户立刻察觉到:阻塞会使页面上的对象失去响应——按钮按不下去,链接点不开,甚至标签页都无法关闭了。而在包含较多 JavaScript 动画的页面里,阻塞会使动画停止一段时间,直到阻塞恢复后才继续执行。如果经常出现「小型」的阻塞(比如上述提及的这些优化没有做好,渲染一帧的时间超过 16ms),那么就会出现「丢帧」的情况,

CSS3 动画(transition 与 animate)不会受 JavaScript 阻塞的影响,但不是本文讨论的重点。

偶尔的且较小的阻塞是可以接收的,频繁或较大的阻塞是不可以接受的。也就是说,我们需要解决两种阻塞:

频繁(通常较小)的阻塞。其原因主要是过高的渲染性能开销,在每一帧中做的事情太多。
较大(虽然偶尔发生)的阻塞。其原因主要是运行复杂算法、大规模的 DOM 操作等等。
对前者,我们应当仔细地优化代码,有时不得不降低动画的复杂(炫酷)程度,本文前几节中的优化方案,解决的就是这个问题。

而对于后者,主要有以下两种优化的策略。

使用 Web Worker,在另一个线程里进行计算。
将任务拆分为多个较小的任务,插在多帧中进行。
Web Worker 是好东西,性能很好,兼容性也不错。浏览器用另一个线程来运行 Worker 中的 JavaScript 代码,完全不会阻碍主线程的运行。动画(尤其是游戏)中难免会有一些时间复杂度比较高的算法,用 Web Worker 来运行再合适不过了。

Web Worker 兼容性

然而,Web Worker 无法对 DOM 进行操作。所以,有些时候,我们也使用另一种策略来优化性能,那就是将任务拆分成多个较小的任务,依次插入每一帧中去完成。虽然这样做几乎肯定会使执行任务的总时间变长,但至少动画不会卡住了。

看下面这个 Demo,我们的动画是使一个红色的 div 向右移动。Demo 中是通过每一帧改变其 transform 属性完成的(Canvas 绘制操作也一样)。

然后,我创建了一个会阻塞浏览器的任务:获取 4×106 次 Math.random() 的平均值。点击按钮,这个任务就会被执行,其结果也会打印在屏幕上。

如你所见,如果直接执行这个任务,动画会明显地「卡」一下。而使用 Web Worker 或将任务拆分,则不会卡。

以上两种优化策略,有一个相同的前提,即任务是异步的。也就是说,当你决定开始执行一项任务的时候,你并不需要立刻(在下一帧)知道结果。比如,即使战略游戏中用户的某个操作触发了寻路算法,你完全可以等待几帧(用户完全感知不到)再开始移动游戏角色。
另外,将任务拆分以优化性能,会带来显著的代码复杂度的增加,以及额外的开销。有时候,我觉得也许可以考虑优先砍一砍需求。

小结
正文就到这里,最后我们来稍微总结一下,在大部分情况下,需要遵循的「最佳实践」。

将渲染阶段的开销转嫁到计算阶段之上。
使用多个分层的 Canvas 绘制复杂场景。
不要频繁设置绘图上下文的 font 属性。
不在动画中使用 putImageData 方法。
通过计算和判断,避免无谓的绘制操作。
将固定的内容预先绘制在离屏 Canvas 上以提高性能。
使用 Worker 和拆分任务的方法避免复杂算法阻塞动画运行。

来源:
Canvas 最佳实践(性能篇)

发表在 HTML, JavaScript | 留下评论