王兴欣的练功房


  • 首页

  • 归档

Vue响应式----数据响应式原理

发表于 2017-11-20

前言

Vue的数据响应主要是依赖了Object.defineProperty(),那么整个过程是怎么样的呢?以我们自己的想法来走Vue的道路,其实也就是以Vue的原理为终点,我们来逆推一下实现过程。

本文代码皆为低配版本,很多地方都不严谨,比如 if(typeof obj === 'object')这是在判断obj是否为为一个对象,虽然obj也有可能是数组等其他类型的数据,但是本文为了简便,就直接这样写来表示判断对象,对于数组使用Array.isArray()。

改造数据

我们先来尝试写一个函数,用于改造对象:

为什么要先写这个函数呢? 因为改造数据是一个最基础也是最重要的步骤,之后所有的步骤都会依赖这一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 代码 1.1
function defineReactive (obj,key,val) {
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get: function () {
return val;
},
set: function (newVal) {
//判断新值与旧值是否相等
//判断的后半段是为了验证新值与旧值都为NaN的情况 NaN不等于自身
if(newVal === val || (newVal !== newVal && value !== value)){
return ;
}
val = newVal;
}
});
}

例如const obj = {},然后再调用defineReactive(obj,'a',2)方法,此时在函数内,val=2,然后每次获取obj.a的值的时候都是获取val的值,设置obj.a的时候也是设置val的值。(每次调用defineReactive都会产生一个闭包保存了val的值);

流程讨论

经过验证之后,发现这个函数确实可以使用的。然后我们来讨论一下响应的流程:

  1. 输入数据
  2. 改造数据(defineReactive())
  3. 如果数据变动 => 触发事件

我们来看第三步,数据变动如何触发之后的事件呢?仔细思考一下,如果要改变数据,那么必须先set数据,那么我们直接set()里面添加方法就ok了呀。

然后还有一个重要问题:

依赖收集

我们怎么知道数据改变之后要触发的是什么事件呢?在Vue中:

使用数据 => 视图; 使用了数据来渲染视图,那么在获取数据的时候收集依赖是最佳的时机,Vue在改造数据属性的时候生成一个Dep实例,用于收集依赖。

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
// 代码 1.2
class Dep {
constructor(){
//订阅的信息
this.subs = [];
}

addSub(sub){
this.subs.push(sub);
}

removeSub (sub) {
remove(this.subs, sub);
}

//此方法的作用等同于 this.subs.push(Watcher);
depend(){
if (Dep.target) {
Dep.target.addDep(this);
}
}
//这个方法就是发布通知了 告诉你 有改变啦
notify(){
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
Dep.target = null;

代码1.2就是Dep的部分代码,暂时只需要知道2个方法的作用就可以了

  • depend() — 可以理解为收集依赖的事件,不考虑其他方面的话 功能等同于addSub()
  • notify() — 这个方法更为直观了,执行所有依赖的update()方法。就是之后的改变视图啊 等等。

本篇主要讨论数据响应的过程,不深入讨论 Watcher类,所以Dep中的方法知道作用就可以了。

然后就是改变代码1.1了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//代码 1.3
function defineReactive (obj,key,val) {
const dep = new Dep();

Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get: function () {
if(Dep.target){
//收集依赖 等同于 dep.addSub(Dep.target)
dep.depend()
}
return val;
},
set: function (newVal) {
if(newVal === val || (newVal !== newVal && val !== val)){
return ;
}
val = newVal;
//发布改变
dep.notify();
}
});
}

这代码中有一个疑点,Dep.target是什么?为什么要有Dep.target才会收集依赖呢?

  1. Dep是一个类,Dep.target是类的属性,并不是dep实例的属性。
  2. Dep类在全局可用,所以Dep.target在全局能访问到,可以任意改变它的值。
  3. get这个方法使用很平常,不可能每次使用获取数据值的时候都去调用dep.depend()。
  4. dep.depend()实际上就是dep.addSub(Dep.target)。
  5. 那么最好方法就是,在使用之前把Dep.target设置成某个对象,在订阅完成之后设置Dep.target = null。

验证

是时候来验证一波代码的可用性了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//代码 1.4

const obj = {};//这一句是不是感觉很熟悉 就相当于初始化vue的data ---- data:{obj:{}};

//低配的不能再低配的watcher对象(源码中是一个类,我这用一个对象代替了)
const watcher = {
addDep:function (dep) {
dep.addSub(this);
},
update:function(){
html();
}
}
//假装这个是渲染页面的
function html () {
document.querySelector('body').innerHTML = obj.html;
}
defineReactive(obj,'html','how are you');//定义响应式的数据

Dep.target = watcher;
html();//第一次渲染界面
Dep.target = null;

此时浏览器上的界面是这样的

然后在下打开了控制台开始调试,输入:

1
obj.html = 'I am fine thank you'

然后就发现,按下回车的那一瞬间,奇迹发生了,页面变成了

结尾

Vue数据响应的设计模式和订阅发布模式有一点像,但是不同,每一个dep实例就是一个订阅中心,每一次发布都会把所有的订阅全部发布出去。
Vue的响应式原理其实还有很大一部分,本文主要讨论了Vue是如何让数据进行响应,但是实际上,一般的数据都是很多的,一个数据被多处使用,改变数据之后观察新值,如何观察、如何订阅、如何调度,都还有很大一部分没有讨论。主要的三个类Dep(收集依赖)、Observer(观察数据)、Watcher(订阅者,若数据有变化通知订阅者),都只提了一点点。

之前写有一篇Vue响应式—-数组变异方法,针对Vue中对数组的改造进行讨论。当然之后有更多其他的文章,整个数据响应流程还有很多内容,三个主要的类都还没有讨论完。

其实阅读源码不仅仅是为了知道源码是如何工作的,更重要的是学习作者的思路与方法,我写的文章都不长,希望自己能够每次专注一个点,能够真真实实领悟到这一个点的原理。当然也想控制阅读时间,免得大家看到一半就关闭了。

初识Http缓存君

发表于 2017-11-14

前言

二零一七年十一月十三日,就是我开始前端之旅的第n日,我独在卧室外徘徊,遇见程君,前来问我道,“你可曾为http缓存写了一点什么没有?”我说“没有”,他就正告我,“你还是写一点罢,http缓存应该很高兴与你相识,你们可以相互认识多一点”。

可是我实在无话可说。我只觉得所住的并非人间。产品经理不仁,以程序员为绉狗。可真的程序员,敢于直面惨淡的薪资,敢于正视淋漓的Bug。这是怎样的哀痛者和幸福者?然而重构又常常为庸人设计,以时间的流驶,来洗涤旧迹,仅使留下汹涌的加班和微漠的悲哀。在这汹涌的加班和微漠的悲哀中,又给人暂得偷生,维持着这似人非人的世界。我不知道这样的世界何时是一个尽头!

而我还在这样的世界里活着,于是觉得有写点什么的必要了。

缓存与性能优化息息相关,在知乎上,有一篇回答令我印象深刻大公司里怎样开发和部署前端代码? 该回答讲述了在部署阶段如何利用缓存提高性能,节约带宽。

一、相遇

与缓存君的相遇是在一场雪夜,当我看见她,我就知道我们会有故事,害羞的我不敢上前去与她交流。马克步是我的好朋友,本来不是的,自从我见到他在月下西瓜田里与猹不可描述后,我们便成了好朋友。他对我说“别怕,我来给你创造环境,你专心做自己的事情。但在此之前我先告诉关于缓存君的一点小知识”,我高兴极了。

  • 200 :正常的交流,每次你说的话都会在脑海里过一遍,并且找出合适礼貌的答语来回答你。
  • 304 :她知道你想要什么,但是需要大脑给她的身体一个指令,然后做出相应的动作给予你想要的。(协商缓存)
  • from memory cache或者disk cache :本垒打暗示,身体的本能反应,也是最快的。(强缓存)

二、交流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer().listen(3000);

server.on('request',(req,res) => {
const url = path.join(__dirname,'static',req.url);
fs.readFile(url,(err,data) => {
//马克步对我说,你别管其他的,每次有静态请求
//就会执行这里的代码,读取文件然后返回出去。
//之后会在这里设置响应头
res.end(data);
})
});

然而在交流过程中却令我高兴不起来,她老是对我不冷不热的。


无论我和她对话多少次,她总是慢慢的考虑半天才回答我,这令我很伤心。于是马克步对我说,“别难过,我帮你设置一个暗号,每次你使用暗号与她交流的话,这会使你们的交流更愉快一些。暗号有两种哦”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//第一种 以资源内容hash为版本号
server.on('request',(req,res) => {
const url = path.join(__dirname,'static',req.url);
fs.readFile(url,(err,data) => {
//如果之前的请求在响应头里返回了Etag 那么这次请求就可以拿到
//req.headers['if-none-match'] == 之前响应头里的Etag
if(req.headers['if-none-match'] == 'hello'){
res.statusCode = 304;
res.end();
}else{
//响应头里设置Etag,下次请求的时候,会在请求头里加上If-None-Match
res.setHeader('Etag', 'hello');
res.end(data);
}
res.end(data);
})
});

我再次和缓存君攀谈了起来。

这一次依然如同陌生人一样,但是这次我拿到了一个暗号,`Etag:hello`,我激动地赶紧使用暗号重复刚才的话。

呀,缓存君对我态度果然变了呀,而且速度也快了很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//第二种 以资源修改时间为版本号
server.on('request',(req,res) => {
const url = path.join(__dirname,'static',req.url);
fs.readFile(url,(err,data) => {
//如果之前的请求在响应头里返回了Last-Modifiedtag 那么这次请求就可以拿到
//req.headers['if-modified-since'] == 之前响应头里的Last-Modified
if(req.headers['if-modified-since']){
res.statusCode = 304
res.end()
}else{
//在响应头里设置上次修改时间 必需为国际标准时间
res.setHeader('Last-Modified', new Date().toUTCString());
res.end(data);
}
res.end(data);
})
});

第二天再次攀谈,缓存君不认识我了。不过没关系,我拿到了时间暗号

我们像老朋友一样愉快的交流了起来

老姐、我想。。。

马克步最知我心思,他告诉我说,你若想要非常亲密的关系,其实设置一个分手时间就可以了。

1
2
3
4
5
6
7
8
server.on('request',(req,res) => {
const url = path.join(__dirname,'static',req.url);
fs.readFile(url,(err,data) => {
//设置缓存过期时间
res.setHeader('Cache-Control','max-age=5');
res.end(data);
})
});

我怀着忐忑的心徐徐向前。

第一次交谈没什么变化,但是她给了我一个时间

卧槽!瞬间就亲密无间了。

但是五秒后,又不认识我了(max-age以秒记)
马克步心想:我寻思给你五秒钟应该够了呀!

此次经历

Http缓存按强势程度分为:

  • 强缓存 — 有效期内,直接从本地获取资源,不需要发送请求。
  • 弱缓存 — 有效期内同上。在有效期外需要发送请求,如果返回304就继续用本地缓存,返回200则把新版本缓存起来,本地版本扔掉。

这次的经历实在是没什么营养。想要了解详细的话有如下建议:

  1. 大公司里怎样开发和部署前端代码?
  2. 使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control
  3. 浏览器缓存机制剖析
  4. MDN HTTP 缓存

最好能按照顺序阅读1、2、3,并且在阅读过程中辅以MDN文档。


其实马克步并不是他的真名,我曾经询问过他的真名,马克步说“我的名字实在是土的很,因为算命先生说我命中缺土,于是在名字中为我平衡”,我想了想马克步与猹的故事惊讶道:“难道你叫”,马克步说:“你想得不错,我叫闺垚”

Vue响应式----数组变异方法

发表于 2017-11-10

前言

很多初使用Vue的同学会发现,在改变数组的值的时候,值确实是改变了,但是视图却无动于衷,果然是因为数组太高冷了吗?
查看官方文档才发现,不是女神太高冷,而是你没用对方法。

看来想让女神自己动,关键得用对方法。虽然在官方文档中已经给出了方法,但是在下实在好奇的紧,想要解锁更多姿势的话,那就必须先要深入女神的心,于是乎才有了去探索Vue响应式原理的想法。(如果你愿意一层一层地剥开我的心。你会发现,你会讶异…… 沉迷于鬼哭狼嚎 无法自拔QAQ)。

前排提示,Vue的响应式原理主要是使用了ES5的Object.defineProperty,毫不知情的同学可以查看相关资料。

为啥数组不响应?

仔细一想,Vue的响应是基于Object.definePropery的,这个方法主要是对对象属性的描述进行修改。数组其实也是对象,通过定义数组的属性应该也能产生响应的效果呀。先验证一下自己的想法,撸起袖子就开干。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const arr = [1,2,3];

let val = arr[0];

Object.defineProperty(arr,'0',{
enumerable: true,
configurable: true,
get(){
doSomething();
return val;
},
set(a){
val = a;
doSomething();
}
});

function doSomething() {

}

然后在控制台中分别输入arr、arr[0] = 2、arr,可以看到如下图的结果。


咦,一切居然都如预想猜想的一样。
接下来,看到这段代码,有的同学可能会有所疑问,为啥在get()方法里不直接返回this[0]呢?而是要借助val来返回值呢?
仔细一想,卧槽!!!差点特么的死循环了,你想呀,get()本身就是获取当前属性的值,在get()里调用this[0]不是等同于再次调用了get()方法吗? 好可怕好可怕,简直吓死劳资了。

虽然你想象中的女神可能会这种姿势,但是你眼前的这个女神确实不是这种姿势的,像我这种屌丝属性暴露无疑的人怎么可能猜透女神的心思?为什么不这样响应数据呢?或许是因为数组和对象还是有所差别,定义数组的属性可能会产生一些麻烦与Bug。又或许是因为在交互的过程中可能会产生大量的数据,导致整体的性能下降。也有可能是作者权衡利弊之后用其他方法也可以达到数据响应的效果。反正我是猜不透啦。

为啥调用数组原生方法就可以响应了?

为什么使用了这些数组的方法就就能让数据响应了呢?
先看看数组部分的源码吧。

简单的来讲,def的作用就是重新定义对象属性的value值。

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
//array.js
import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
//arrayMethods是对数组的原型对象的拷贝,
//在之后会将该对象里的特定方法进行变异后替换正常的数组原型对象
/**
* Intercept mutating methods and emit events
*/
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
//将上面的方法保存到original中
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})

贴出def部分的代码

1
2
3
4
5
6
7
8
9
10
11
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

array.js是对数组的一些方法进行变异,我们以push方法来举个例子。
首先 就是要用original = arrayProto['push']来保存原生的push方法。

然后就是要定义变异的方法了,对于def函数,如果不深究的话,def(arrayMethods,method,function(){}),这个函数可以粗略的表示为arrayMethods[method] = function mutator(){};
假设在之后调用push方法,实际上调用的是mutator方法,在mutator方法中,第一件事就是调用保存了原生push方法的original,先求出实际的值。
一堆文字看起来实在很抽象,那么写一段低配版的代码来表达源码的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const push = Array.prototype.push;

Array.prototype.push = function mutator (...arg){
const result = push.apply(this,arg);
doSomething();
return result
}

function doSomething(){
console.log('do something');
}

const arr = [];
arr.push(1);
arr.push(2);
arr.push(3);

在控制台中查看结果为:
。

那么源码中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()

这段代码就是对应的doSomething()了

在该代码中,清清楚楚的写了2个单词的注释notify change,不认识这2个单词的同学就百度一下嘛,这里就由我代劳了,这俩单词的意思是发布改变!
每次调用了该方法,都会求出值,然后做一些其他的事情,比如发布改变与观察新增的元素,响应的其他过程在本篇就不讨论了。

1
2
3
4
5
6
7
8
9
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

目前一共有这么些方法,只要用对方法就能改变女神的姿势哟!

小结

对于标题,我一改再改,一开始叫浅析Vue响应原理,但是后来一看 这个标题实在太大,那就从最简单的入手吧,先从数组入手,而且本篇也不会花费太多时间去阅读。如果本篇有什么地方写得有误,误导了他人,请一定指出,万分感激。
最后在光棍节前祝大家光棍节快乐。
我? 我特么当然不是单身狗啦!

哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈
呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜

如何处理4个常见的内存泄漏(译)

发表于 2017-09-26

前言:这篇文章的主要内容由翻译而来,原文链接。但是大体内容与原文不尽相同,删除了一些内容,同时新增部分内容。由于本文大部分内容是翻译而来,若有理解不当之处还望谅解并指出,我会尽快进行修改。(内心:如果有什么不对的地方还希望大家指出,反正我也不会改 。玩笑话玩笑话 别当真!)

概述

在一些语言中,开发人员需要手动的使用原生语句来显示的分配和释放内存。但是在许多高级语言中,这些过程都会被自动的执行。在JavaScript中,变量(对象,字符串,等等)创建的时候为其分配内存,当不再被使用的时候会“自动地”释放这些内存,这个过程被称为垃圾回收。这个看似“自动的”释放资源的本质是一个混乱的来源,给JavaScript(和其他高等级语言)开发者可以不去关心内存管理的错误印象。这是一个很大的错误。

内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

内存生命周期

无论使用哪一种编程语言,内存的生命周期几乎总是一模一样的
分配内存、使用内存、释放内存。
在这里我们主要讨论内存的回收。

引用计数垃圾回收

这是最简单的垃圾回收算法。一个对象在没有被其他的引用指向的时候就被认为“可回收的”。

对JS引用类型不熟悉的请先百度引用类型,理解了值类型(基本类型)和引用类型之后才能理解下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var obj1 = {
obj2: {
x: 1
}
};
//2个对象被创建。 obj2被obj1引用,并且作为obj1的属性存在。这里并没有可以被回收的。
//obj1和obj2都指向了{obj2: {x: 1}}这个对象,这个示例中用`原来的对象`来表示这个对象。

var obj3 = obj1; //obj3也引用了obj1指向的对象。
obj1 = 1; // obj1不引用原来的对象了。此时原来的对象只有obj3在引用。

var obj4 = obj3.obj2; //obj4引用了obj3对象的obj2属性,
//此时obj2对象有2个引用,一个是作为obj3的一个属性,一个是作为obj4变量。

obj3 = 1;
// 咦,obj1原来对象只有obj3在引用,现在obj3也没用在引用了。
// obj1原来的对象就沦为了一只单身狗,于是乎抓狗大队就来带走了它。(好吧、其实内存就可以被回收了)。
// 然而 obj2对象依然有人爱(被obj4引用)。所以obj2的内存就不会被垃圾回收。

obj4 = null;
// obj2内心在呐喊:小姐姐不要离开我 QOQ。现在obj2也没有被引用了,引用计数就是0
也就是可以被回收了。

简而言之~,如果内存有人爱,那就不会被回收。如果是单身狗的话,[手动滑稽]。

循环引用会造成麻烦

引用计数在涉及循环引用的时候有一个缺陷。在下面的例子中,创建了2个对象,并且相互引用,这样创建了一个循环。因此他们实际上是无用的,可以被释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。

1
2
3
4
5
6
7
8
function f() {
var o1 = {};
var o2 = {};
o1.p = o2;
o2.p = o1;
}

f();

单身狗心里千万头草泥马在奔腾(我特么也会自己牵自己手啊,我也会假装情侣拍照啊)

标记清除算法

别以为你假装不是单身狗就拿你没办法了,这个算法确定了对象是否可以被达到。
这个算法包含了以下步骤:

  1. 从‘根’上生成一个列表(通常是以全局变量为根)。在JS中window对象可以作为一个’根’
  2. 所有的’根’都被标记为活跃的,所有的子变量也被递归检查。能够从’根’上到达的都不会被认为成垃圾。
  3. 没有被标记为活跃的就被认为成垃圾。这些内存就会被释放。


上图就是标记清除的动作。

在之前的例子中,虽然两个变量相互引用,但在函数执行完之后,这个两个变量都没有被window对象上的任何对象所引用。因此,他们会被认为不可到达的。

4种常见的JS内存泄漏

1:全局变量
JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是window。换句话说:

1
2
3
4
5
6
7
function foo(arg) {
bar = 'some text';
}
//等同于
function foo(arg) {
window.bar = 'some text';
}

如果bar被假定只在foo函数的作用域里引用,但是你忘记了使用var去声明它,一个意外的全局变量就被声明了。
在这个例子里,泄漏一个简单的字符并不会造成很大的伤害,但是它确实有可能变得更糟。
有时有会通过this来创建意外的全局变量。

为了防止这些问题发生,可以在你的JaveScript文件开头使用'use strict';。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。

如果有时全局变量被用于暂时储存大量的数据或者涉及到的信息,那么在使用完之后应该指定为null或者重新分配。

2:被遗忘的定时器或者回调
还是来个栗子吧,定时器可能会产生对不再需要的DOM节点或者数据的引用。

1
2
3
4
5
6
7
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每五秒会执行一次

renderer对象在将来有可能被移除,让interval没有存在的意义。然而当处理器interval仍然起作用时,renderer并不能被回收(interval在对象被移除时需要被停止),如果interval不能被回收,它的依赖也不可能被回收。这就意味着serverData,大概保存了大量的数据,也不可能被回收。
如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,甚至监听器没有被明确的移除掉。在对象被处理之前,最好也要显式地删除这些观察者。

1
2
3
4
5
6
7
8
9
10
11
12
13
var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
}

element.addEventListener('click', onClick);
// 做一些其他的事情

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用removeEventListener。
框架和插件例如jQuqery在处理节点(当使用具体的api的时候)之前会移除监听器。这个是插件内部的处理可以确保不会产生内存泄漏,甚至运行在有问题的浏览器上(哈哈哈 说的就是IE6)。

3: 闭包
闭包是javascript开发的一个关键方面,一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行的细节,它可能以下面的方式造成内存泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var theThing = null;

var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) console.log('hi') //引用了originalThing
};

theThing = {
longStr: new Array(1000000).jojin('*'),
someMethod: function (){
console.log('message');
}
};
};

setInterval(replaceThing,1000);

这些代码做了一件事情,每次relaceThing被调用,theThing获得一个包含大量数据和新的闭包(someMethod)的对象。同时,变量unused引用了originalThing(theThing是上一次函数被调用时产生的)。已经有点困惑了吧?最重要的事情是一旦为同一父域中的作用域产生闭包,则该作用域是共享的。

在这个案例中,someMethod和unused共享闭包作用域,unused引用了originalThing,这阻止了originalThing的回收,尽管unused不会被使用,但是someMethod依然可以通过theThing来访问replaceThing作用域外的变量(例如某些全局的)。

4:来自DOM的引用
在你要重复的操作DOM节点的时候,存储DOM节点是十分有用的。但是在你需要移除DOM节点的时候,需要确保移除DOM tree和代码中储存的引用。

1
2
3
4
5
6
7
8
9
var element = {
image: document.getElementById('image'),
button: document.getElementById('button')
};

//Do some stuff

document.body.removeChild(document.getElementById('image'));
//这个时候 虽然从dom tree中移除了id为image的节点,但是还保留了一个对该节点的引用。于是image仍然不能被回收。

当涉及到DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表格的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。你也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了整个表格被留在内存中了,所以在移除有被引用的节点时候要移除其子节点。

总结

  1. 小心使用全局变量,尽量不要使用全局变量来存储大量数据,如果是暂时使用,要在使用完成之后手动指定为null或者重新分配
  2. 如果使用了定时器,在无用的时候要记得清除。如果为DOM节点绑定了事件监听器,在移除节点时要先注销事件监听器。
  3. 小心闭包的使用。如果掌握不好,至少在使用大量数据的时候仔细考量。在使用递归的时候也要非常小心(例如用canvas做小游戏)。
  4. 在移除DOM节点的时候要确保在代码中没有对节点的引用,这样才能完全的移除节点。在移除父节点之前要先移除子节点。

王兴欣

纸上得来终觉浅,绝知此事要躬行。

4 日志
3 标签
© 2017 王兴欣
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.3