欢迎大家来到IT世界,在知识的湖畔探索吧!
2023.03.09 – 2023.03.15 更新收集面试问题(45道题)【第3部分】
获取更多面试问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues
目录:
- 高级开发者相关问题【共计 1 道题】
- 77.虚拟 dom 原理是啥,手写一个简答的虚拟 dom 实现?【JavaScript】
- 资深开发者相关问题【共计 4 道题】
- 76.JS 内存泄露问题该如何排查?【JavaScript】
- 78.[vue]: vue2.x 虚拟 dom 是怎么实现的?【web框架】
- 79.[vue]: 是如何实现 MVVM 的?【web框架】
- 80.[Redux]: 看过 Redux 源码吗, 对 Redux 了解多少?【web框架】
高级开发者相关问题【共计 1 道题】
77.虚拟 dom 原理是啥,手写一个简答的虚拟 dom 实现?【JavaScript】
vdom 概念
用JS模拟DOM结构。
DOM变化的对比,放在JS层来做。
提升重绘性能。
比如有abc 三个dom, 如果我们要删除b dom, 以前浏览器的做法是 全部删除abc dom , 然后 在添加b dom 。这样做的成本会非常高。
用JS模拟 dom
例如下面的一个dom 结构:
<ul id="list">
<li class="item">item1</li>
<li class="item">item2</li>
</ul>
欢迎大家来到IT世界,在知识的湖畔探索吧!
这样的dom 结构,可以模拟为下面的JS :
欢迎大家来到IT世界,在知识的湖畔探索吧!let dom = {
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['item1']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['item2']
}
]
}
浏览器操作dom 是花销非常大的。执行JS花销要小非常多,所以这就是为什么虚拟dom 出现的一个根本原因。
jquery实现virtual-dom
一个需求场景
1、数据生成表格。 2、随便修改一个信息,表格也会跟着修改。
<body>
<div id="container"></div>
<br>
<button id="btn-change">change</button>
<script>
let data = [
{
name: 'yanle',
age: '20',
address: '重庆'
},
{
name: 'yanle2',
age: '25',
address: '成都'
},
{
name: 'yanle3',
age: '27',
address: '深圳'
}
];
// 渲染函数
function render(data) {
let $container = document.getElementById('container');
$container.innerHTML = '';
let $table = document.createElement('table');
$table.setAttribute('border', true);
$table.insertAdjacentHTML('beforeEnd', `<tr>
<td>name</td>
<td>age</td>
<td>address</td>
</tr>`);
data.forEach(function (item) {
$table.insertAdjacentHTML('beforeEnd',
`<tr>
<td>${item.name}</td>
<td>${item.age}</td>
<td>${item.address}</td>
</tr>`
)
});
$container.appendChild($table);
}
// 修改信息
let button = document.getElementById('btn-change');
button.addEventListener('click', function () {
data[1].name = '徐老毕';
data[1].age = 30;
data[1].address = '深圳';
render(data);
});
render(data);
</script>
</body>
实际上上面的这段代码也是不符合预期的,因为每次使用render 方法,都会全部渲染整个table, 但是并未没有只渲染我们想要的第二行。
遇到的问题:
DOM 操作是非常 “昂贵” 的, JS 运行效率高。虚拟dom 的核心就是diff算法,对比出不同的dom数据,定点渲染不同的数据。
资深开发者相关问题【共计 4 道题】
76.JS 内存泄露问题该如何排查?【JavaScript】
什么是内存泄露
该问题转载自:https://github.com/zhansingsong/js-leakage-patterns
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。内存泄漏通常情况下只能由获得程序源代码的程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。 ————wikipedia
⚠️ 注:下文中标注的 CG 是 Chrome 浏览器中 Devtools 的【Collect garbage】按钮缩写,表示回收垃圾操作。
意外的全局变量
JavaScript 对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。如果在浏览器中,全局对象就是window对象。
如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。
欢迎大家来到IT世界,在知识的湖畔探索吧!function foo(arg) {
bar = 'this is a hidden global variable with a large of data';
}
等同于:
function foo(arg) {
window.bar = 'this is an explicit global variable with a large of data';
}
另外,通过this创建意外的全局变量:
function foo() {
this.variable = 'potential accidental global';
}
// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'
foo();
解决方法:
在 JavaScript 文件中添加’use strict’,开启严格模式,可以有效地避免上述问题。
function foo(arg) {
'use strict'; // 在foo函数作用域内开启严格模式
bar = 'this is an explicit global variable with a large of data'; // 报错:因为bar还没有被声明
}
如果需要在一个函数中使用全局变量,可以像如下代码所示,在window上明确声明:
function foo(arg) {
window.bar = 'this is a explicit global variable with a large of data';
}
这样不仅可读性高,而且后期维护也方便
谈到全局变量,需要注意那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为 null 或重新赋值。全局变量也常用来做 cache,一般 cache 都是为了性能优化才用到的,为了性能,最好对 cache 的大小做个上限限制。因为 cache 是不能被回收的,越高 cache 会导致越高的内存消耗。
console.log
console.log:向 web 开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能造成内存泄露。
在传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。
实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Leaker</title>
</head>
<body>
<input type="button" value="click">
<script>
!function () {
function Leaker() {
this.init();
};
Leaker.prototype = {
init: function () {
this.name = (Array(100000)).join('*');
console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收
},
destroy: function () {
// do something....
}
};
document.querySelector('input').addEventListener('click', function () {
new Leaker();
}, false);
}()
</script>
</body>
</html>
这里结合 Chrome 的 Devtools–>Performance 做一些分析,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 开启【Performance】项的记录
- 执行一次 CG,创建基准参考线
- 连续单击【click】按钮三次,新建三个 Leaker 对象
- 执行一次 CG
- 停止记录
可以看出【JS Heap】线最后没有降回到基准参考线的位置,显然存在没有被回收的内存。如果将代码修改为:
!(function() {
function Leaker() {
this.init();
}
Leaker.prototype = {
init: function() {
this.name = Array(100000).join('*');
},
destroy: function() {
// do something....
},
};
document.querySelector('input').addEventListener(
'click',
function() {
new Leaker();
},
false,
);
})();
去掉console.log(“Leaking an object %o: %o”, (new Date()), this);语句。重复上述的操作步骤,分析结果如下:
从对比分析结果可知,console.log打印的对象是不会被垃圾回收器回收的。因此最好不要在页面中console.log任何大对象,这样可能会影响页面的整体性能,特别在生产环境中。除了console.log外,另外还有console.dir、console.error、console.warn等都存在类似的问题,这些细节需要特别的关注。
closures(闭包)
当一个函数 A 返回一个内联函数 B,即使函数 A 执行完,函数 B 也能访问函数 A 作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。
function foo(message) {
function closure() {
console.log(message);
}
return closure;
}
// 使用
var bar = foo('hello closure!');
bar(); // 返回 'hello closure!'
在函数 foo 内创建的函数 closure 对象是不能被回收掉的,因为它被全局变量 bar 引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Closure</title>
</head>
<body>
<p>不断单击【click】按钮</p>
<button id="click_button">Click</button>
<script>
function f() {
var str = Array(10000).join('#');
var foo = {
name: 'foo'
}
function unused() {
var message = 'it is only a test message';
str = 'unused: ' + str;
}
function getData() {
return 'data';
}
return getData;
}
var list = [];
document.querySelector('#click_button').addEventListener('click', function () {
list.push(f());
}, false);
</script>
</body>
</html>
这里结合 Chrome 的 Devtools->Memory 工具进行分析,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 选中【Record allocation timeline】选项
- 执行一次 CG
- 单击【start】按钮开始记录堆分析
- 连续单击【click】按钮十多次
- 停止记录堆分析
上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:
查看对象的详细信息:
从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个 str 字段。而 str 字段并没有在返回 getData()中使用过。为什么会存在在作用域中,按理应该被 GC 回收掉, why❓
原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:
function f() {
var str = Array(10000).join('#');
var foo = {
name: 'foo',
};
function unused() {
var message = 'it is only a test message';
// str = 'unused: ' + str; //删除该条语句
}
function getData() {
return 'data';
}
return getData;
}
var list = [];
document.querySelector('#click_button').addEventListener(
'click',
function() {
list.push(f());
},
false,
);
getData()和 unused()内部函数共享 f 函数对应的变量对象,因为 unused()内部函数访问了 f 作用域内 str 变量,所以 str 字段存在于 f 变量对象中。加上 getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的 f 变量对象存在于闭包函数的作用域链中。这里只要将函数 unused 中str = ‘unused: ‘ + str;语句删除便可解决问题。
查看一下闭包信息:
DOM 泄露
在 JavaScript 中,DOM 操作是非常耗时的。因为 JavaScript/ECMAScript 引擎独立于渲染引擎,而 DOM 是位于渲染引擎,相互访问需要消耗一定的资源。如 Chrome 浏览器中 DOM 位于 WebCore,而 JavaScript/ECMAScript 位于 V8 中。假如将 JavaScript/ECMAScript、DOM 分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript 每次访问 DOM 时,都需要交纳“过桥费”。因此访问 DOM 次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️
为了减少 DOM 访问次数,一般情况下,当需要多次访问同一个 DOM 方法或属性时,会将 DOM 引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的 DOM 引用,这样会造成 DOM 内存泄露。
实例——>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Dom-Leakage</title>
</head>
<body>
<input type="button" value="remove" class="remove" style="display:none;">
<input type="button" value="add" class="add">
<div class="container">
<pre class="wrapper"></pre>
</div>
<script>
// 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点,将其缓存到本地变量中,
var wrapper = document.querySelector('.wrapper');
var container = document.querySelector('.container');
var removeBtn = document.querySelector('.remove');
var addBtn = document.querySelector('.add');
var counter = 0;
var once = true;
// 方法
var hide = function(target){
target.style.display = 'none';
}
var show = function(target){
target.style.display = 'inline-block';
}
// 回调函数
var removeCallback = function(){
removeBtn.removeEventListener('click', removeCallback, false);
addBtn.removeEventListener('click', addCallback, false);
hide(addBtn);
hide(removeBtn);
container.removeChild(wrapper);
}
var addCallback = function(){
wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));
// 显示删除操作按钮
if(once){
show(removeBtn);
once = false;
}
}
// 绑定事件
removeBtn.addEventListener('click', removeCallback, false);
addBtn.addEventListener('click', addCallback, false);
</script>
</body>
</html>
这里结合 Chrome 浏览器的 Devtools–>Performance 做一些分析,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 开启【Performance】项的记录
- 执行一次 CG,创建基准参考线
- 连续单击【add】按钮 6 次,增加 6 个文本节点到 pre 元素中
- 单击【remove】按钮,删除刚增加 6 个文本节点和 pre 元元素
- 执行一次 CG
- 停止记录堆分析
从分析结果图可知,虽然 6 次 add 操作增加 6 个 Node,但是 remove 操作并没有让 Nodes 节点数下降,即 remove 操作失败。尽管还主动执行了一次 CG 操作,Nodes 曲线也没有下降。因此可以断定内存泄露了!那问题来了,如何去查找问题的原因呢?这里可以通过 Chrome 浏览器的 Devtools–>Memory 进行诊断分析,执行如下操作步骤:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 选中【Take heap snapshot】选项
- 连续单击【add】按钮 6 次,增加 6 个文本节点到 pre 元素中
- 单击【Take snapshot】按钮,执行一次堆快照
- 单击【remove】按钮,删除刚增加 6 个文本节点和 pre 元元素
- 单击【Take snapshot】按钮,执行一次堆快照
- 选中生成的第二个快照报告,并将视图由”Summary”切换到”Comparison”对比模式,在[class filter]过滤输入框中输入关键字:Detached
从分析结果图可知,导致整个 pre 元素和 6 个文本节点无法别回收的原因是:代码中存在全局变量wrapper对 pre 元素的引用。知道了产生的问题原因,便可对症下药了。对代码做如下就修改:
// 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点,将其缓存到本地变量中,
var wrapper = document.querySelector('.wrapper');
var container = document.querySelector('.container');
var removeBtn = document.querySelector('.remove');
var addBtn = document.querySelector('.add');
var counter = 0;
var once = true;
// 方法
var hide = function(target) {
target.style.display = 'none';
};
var show = function(target) {
target.style.display = 'inline-block';
};
// 回调函数
var removeCallback = function() {
removeBtn.removeEventListener('click', removeCallback, false);
addBtn.removeEventListener('click', addCallback, false);
hide(addBtn);
hide(removeBtn);
container.removeChild(wrapper);
wrapper = null; //在执行删除操作时,将wrapper对pre节点的引用释放掉
};
var addCallback = function() {
wrapper.appendChild(
document.createTextNode('\t' + ++counter + ':a new line text\n'),
);
// 显示删除操作按钮
if (once) {
show(removeBtn);
once = false;
}
};
// 绑定事件
removeBtn.addEventListener('click', removeCallback, false);
addBtn.addEventListener('click', addCallback, false);
在执行删除操作时,将 wrapper 对 pre 节点的引用释放掉,即在删除逻辑中增加wrapper = null;语句。再次在 Devtools–>Performance 中重复上述操作:
小试牛刀
再来看看网上的一个实例,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Practice</title>
</head>
<body>
<div id="refA"><ul><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#" id="refB"></a></li></ul></div>
<div></div>
<div></div>
<script>
var refA = document.getElementById('refA');
var refB = document.getElementById('refB');
document.body.removeChild(refA);
// #refA不能GC回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
refA = null;
// 还存在变量refB对#refA的间接引用(refB引用了#refB,而#refB属于#refA)。将变量refB对#refB的引用释放,#refA就可以被GC回收。
refB = null;
</script>
</body>
</html>
整个过程如下图所演示:
有兴趣的同学可以使用 Chrome 的 Devtools 工具,验证一下分析结果,实践很重要~~~
timers
在 JavaScript 常用setInterval()来实现一些动画效果。当然也可以使用链式setTimeout()调用模式来实现:
setTimeout(function() {
// do something. . . .
setTimeout(arguments.callee, interval);
}, interval);
如果在不需要setInterval()时,没有通过clearInterval()方法移除,那么setInterval()会不停地调用函数,直到调用clearInterval()或窗口关闭。如果链式setTimeout()调用模式没有给出终止逻辑,也会一直运行下去。因此再不需要重复定时器时,确保对定时器进行清除,避免占用系统资源。另外,在使用setInterval()和setTimeout()来实现动画时,无法确保定时器按照指定的时间间隔来执行动画。为了能在 JavaScript 中创建出平滑流畅的动画,浏览器为 JavaScript 动画添加了一个新 API-requestAnimationFrame()。关于 setInterval、setTimeout 与 requestAnimationFrame 实现动画上的区别 ➹ 猛击
实例
如下通过setInterval()实现一个 clock 的小实例,不过代码存在问题的,有兴趣的同学可以先尝试找一下问题的所在~~~~~ 操作:
- 单击【start】按钮开始 clock,同时 web 开发控制台会打印实时信息
- 单击【stop】按钮停止 clock,同时 web 开发控制台会输出停止信息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>setInterval</title>
</head>
<body>
<input type="button" value="start" class="start">
<input type="button" value="stop" class="stop">
<script>
var counter = 0;
var clock = {
start: function () {
setInterval(this.step.bind(null, ++counter), 1000);
},
step: function (flag) {
var date = new Date();
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
console.log("%d-----> %d:%d:%d", flag, h, m, s);
}
}
document.querySelector('.start').addEventListener('click', clock.start.bind(clock), false);
document.querySelector('.stop').addEventListener('click', function () {
console.log('----> stop <----');
clock = null;
}, false);
</script>
</body>
</html>
上述代码存在两个问题:
- 如果不断的单击【start】按钮,会断生成新的 clock。
- 单击【stop】按钮不能停止 clock。
输出结果:
针对暴露出的问题,对代码做如下修改:
var counter = 0;
var clock = {
timer: null,
start: function() {
// 解决第一个问题
if (this.timer) {
clearInterval(this.timer);
}
this.timer = setInterval(this.step.bind(null, ++counter), 1000);
},
step: function(flag) {
var date = new Date();
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
console.log('%d-----> %d:%d:%d', flag, h, m, s);
},
// 解决第二个问题
destroy: function() {
console.log('----> stop <----');
clearInterval(this.timer);
node = null;
counter = void 0;
},
};
document
.querySelector('.start')
.addEventListener('click', clock.start.bind(clock), false);
document
.querySelector('.stop')
.addEventListener('click', clock.destroy.bind(clock), false);
EventListener
做移动开发时,需要对不同设备尺寸做适配。如在开发组件时,有时需要考虑处理横竖屏适配问题。一般做法,在横竖屏发生变化时,需要将组件销毁后再重新生成。而在组件中会对其进行相关事件绑定,如果在销毁组件时,没有将组件的事件解绑,在横竖屏发生变化时,就会不断地对组件进行事件绑定。这样会导致一些异常,甚至可能会导致页面崩掉。
实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>callbacks</title>
</head>
<body>
<div class="container"></div>
<script>
var container = document.querySelector('.container');
var counter = 0;
var createHtml = function (n, counter) {
var template = `${(new Array(n)).join(`<div>${counter}: this is a new data <input type="button" value="remove"></div>`)}`
container.innerHTML = template;
}
var resizeCallback = function (init) {
createHtml(10, ++counter);
// 事件委托
container.addEventListener('click', function (event){
var target = event.target;
if(target.tagName === 'INPUT'){
container.removeChild(target.parentElement)
}
}, false);
}
window.addEventListener('resize', resizeCallback, false);
resizeCallback(true);
</script>
</body>
</html>
页面是存在问题的,这里结合 Devtools–>Performance 分析一下问题所在,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 开启 Performance 项的记录
- 执行一次 CG,创建基准参考线
- 对窗口大小进行调整
- 执行一次 CG
- 停止记录
如分析结果所示,在窗口大小变化时,会不断地对container添加代理事件。
同一个元素节点注册了多个相同的 EventListener,那么重复的实例会被抛弃。这么做不会让得 EventListener 被重复调用,也不需要用 removeEventListener 手动清除多余的 EventListener,因为重复的都被自动抛弃了。而这条规则只是针对于命名函数。对于匿名函数,浏览器会将其看做不同的 EventListener,所以只要将匿名的 EventListener,命名一下就可以解决问题:
var container = document.querySelector('.container');
var counter = 0;
var createHtml = function(n, counter) {
var template = `${new Array(n).join(
`<div>${counter}: this is a new data <input type="button" value="remove"></div>`,
)}`;
container.innerHTML = template;
};
//
var clickCallback = function(event) {
var target = event.target;
if (target.tagName === 'INPUT') {
container.removeChild(target.parentElement);
}
};
var resizeCallback = function(init) {
createHtml(10, ++counter);
// 事件委托
container.addEventListener('click', clickCallback, false);
};
window.addEventListener('resize', resizeCallback, false);
resizeCallback(true);
在 Devtools–>Performance 中再重复上述操作,分析结果如下:
在开发中,开发者很少关注事件解绑,因为浏览器已经为我们处理得很好了。不过在使用第三方库时,需要特别注意,因为一般第三方库都实现了自己的事件绑定,如果在使用过程中,在需要销毁事件绑定时,没有调用所解绑方法,就可能造成事件绑定数量的不断增加。如下链接是我在项目中使用 jquery,遇见到类似问题:jQuery 中忘记解绑注册的事件,造成内存泄露 ➹ 猛击
总结
本文主要介绍了几种常见的内存泄露。在开发过程,需要我们特别留意一下本文所涉及到的几种内存泄露问题。因为这些随时可能发生在我们日常开发中,如果我们对它们不了解是很难发现它们的存在。可能在它们将问题影响程度放大时,才会引起我们的关注。不过那时可能就晚了,因为产品可能已经上线,接着就会严重影响产品的质量和用户体验,甚至可能让我们承受大量用户流失的损失。作为开发的我们必须把好这个关,让我们开发的产品带给用户最好的体验。
参考文章:
- An interesting kind of JavaScript memory leak
- Memory Leaks in Microsoft Internet Explorer
- Memory leak when logging complex objects
78.[vue]: vue2.x 虚拟 dom 是怎么实现的?【web框架】
virtual-dom 实现之一: snabbdom
vue2.0就是使用的snabbdom
一个简单的使用实例:
var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
snabbdom 核心api
- snabbdom.init: The core exposes only one single function snabbdom.init. This init takes a list of modules and returns a patch function that uses the specified set of modules.
var patch = snabbdom.init([
require('snabbdom/modules/class').default,
require('snabbdom/modules/style').default,
]);
- patch:
patch(oldVnode, newVnode);
- snabbdom/h: It is recommended that you use snabbdom/h to create vnodes. h accepts a tag/selector as a string, an optional data object and an optional string or array of children.
var h = require('snabbdom/h').default;
var vnode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
- snabbdom/tovnode: Converts a DOM node into a virtual node. Especially good for patching over an pre-existing, server-side generated content.
var snabbdom = require('snabbdom')
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var toVNode = require('snabbdom/tovnode').default;
var newVNode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
patch(toVNode(document.querySelector('.container')), newVNode)
h函数 和 patch 的使用
例如下面的一个dom 结构:
<ul id="list">
<li class="item">item1</li>
<li class="item">item2</li>
</ul>
用h函数来表示,就如下形式:
let vnode = h('ul#list', {}, [
h('li.item', {}, 'item1'),
h('li.item', {}, 'item2')
])
作用就是模拟的一个真实节点。
patch的使用方式:
第一种方式 patch(‘容器’, vnode); // 这种使用方式是直接渲染dom
第二种是用方式: patch(oldVnode, newVnode); // 这种方式会自动对比dom的差异性,然后只渲染我们需要dom;
一个简单的使用实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>snabbdom</title>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
</head>
<body>
<div id="container"></div><br>
<button id="btn-change">change</button>
<script>
let snabbdom = window.snabbdom;
let container = document.getElementById('container');
let buttonChange = document.getElementById('btn-change');
// 定义patch
let patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
// 定义h
let h = snabbdom.h;
// 生成vnode
let vnode = h('ul#list', {}, [
h('li.item', {}, 'item1'),
h('li.item', {}, 'item2')
]);
patch(container, vnode);
// 模拟一个修改的情况
buttonChange.addEventListener('click', function () {
let newVnode = h('ul#list', {}, [
h('li.item', {}, 'item1'),
h('li.item', {}, 'item B'),
h('li.item', {}, 'item 3')
]);
patch(vnode, newVnode);
})
</script>
</body>
</html>
snabbdom 的使用实例
<body>
<div id="container"></div>
<br>
<button id="btn-change">change</button>
<script>
let snabbdom = window.snabbdom;
let container = document.getElementById('container');
let buttonChange = document.getElementById('btn-change');
// 定义patch
let patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
// 定义h
let h = snabbdom.h;
let data = [
{
name: 'yanle',
age: '20',
address: '重庆'
},
{
name: 'yanle2',
age: '25',
address: '成都'
},
{
name: 'yanle3',
age: '27',
address: '深圳'
}
];
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
});
let vnode;
function render(data) {
let newVnode = h('table', {style: {'font-size': '16px'}}, data.map(function (item) {
let tds = [];
let i ;
for (i in item) {
if(item.hasOwnProperty(i)) {
tds.push(h('td', {}, h('a', {props: {href: '/foo'}}, item[i])))
}
}
return h('tr', {}, tds)
}));
if(vnode) {
patch(vnode, newVnode);
} else {
patch(container, newVnode);
}
vnode = newVnode;
}
// 初次渲染
render(data);
buttonChange.addEventListener('click', function () {
data[1].age=30;
data[1].address = '非洲';
render(data);
});
</script>
</body>
diff算法
概念
就是找出两个文件的不同
diff 算法是非常复杂的,实现难度非常大, 源码两非常大。 所以需要去繁就简,明白流程,不关心细节。
在vdom中,需要找出本次dom 必须更新的节点来更新,其他的不用更新。找出这个过程就是diff算法实现的。找出前后两个虚拟dom的差异。
diff实现的过程
这里以snabbdom为例子:
patch(container, vnode); patch(vnode, newVnode); 这两个方法里面就使用到了diff算法。 用patch方法来解析diff算法流程核心。
patch(container, vnode)
如果上面的数据, 我们怎么构建真正的dom 结构:
let createElement = function(vnode) {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || {};
if(!tag) return null;
// 创建元素
let elem = document.createElement(tag);
// 属性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加元素
elem.appendChild(createElement(childVnode))
});
return elem;
};
patch(vnode, newVnode)
伪代码实现如下
let createElement = function(vnode) {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || {};
if(!tag) return null;
// 创建元素
let elem = document.createElement(tag);
// 属性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加元素
elem.appendChild(createElement(childVnode))
});
return elem;
};
diff算法的其他内容
- 节点的新增和删除
- 节点重新排序
- 节点属性、样式、事件绑定
- 如果极致压榨性能
79.[vue]: 是如何实现 MVVM 的?【web框架】
引入:使用jquery和其他框架的区别
原生JS实现一个todo-list
<body>
<div>
<input type="text" name="" id="txt-title"> <br>
<button id="btn-submit">submit</button>
</div>
<div>
<ul id="ul-list"></ul>
</div>
<script>
let $txtTitle = document.getElementById('txt-title');
let $buttonSubmit = document.getElementById('btn-submit');
let $ulList = document.getElementById('ul-list');
$buttonSubmit.addEventListener('click', function () {
let title = $txtTitle.value;
if(!title) return false;
let $li = document.createElement('li');
$li.innerText = title;
$ulList.appendChild($li);
$txtTitle.value = '';
})
</script>
</body>
vue实现todo-list
<body>
<div id="app">
<div>
<input v-model="title"> <br>
<button id="btn-submit" v-on:click="add">submit</button>
</div>
<div>
<ul id="ul-list">
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</div>
<script>
let vm = new window.Vue({
el: '#app',
data: {
title: '',
list: []
},
methods: {
add: function () {
this.list.push(this.title);
this.title = '';
}
}
})
</script>
</body>
两者之间的区别
- 数据和视图分离(开放封闭原则: 扩展开放,修改封闭)
- 数据驱动视图
对mvvm的理解
具体的理解自己再去整理
MVVM框架的三大要素:
响应式、模板引擎、渲染
响应式的实现
修改data属性之后,立马就能监听到。
data属性挂在到vm实例上面。
有下面的一个问题,我们是如何监听属性的获取和属性的赋值的。
let obj = {
name: 'yanle',
age: 25
};
console.log(obj.name);
obj.age = 26;
是通过Object.defineProperty 实现的, 下面的代码就可以实现一个完整的属性修改和获取的监听。
let vm = {};
let data = {
name: 'yanle',
age: 25
};
let key, value;
for (key in data) {
(function (key) {
Object.defineProperty(vm, key, {
get: function () {
console.log('get', data[key]);
return data[key]; // data的属性代理到vm 上
},
set: function (newValue) {
console.log('set', newValue);
data[key] = newValue;
}
})
})(key)
}
vue中的模板
模板
本质就是字符串;
有逻辑: if for 等;
跟html格式很像, 但是区别很大;
最终要转为HTML来现实;
模板需要用JS代码来实现, 因为有逻辑,只能用JS来实现;
render函数-with用法:
let obj = {
name: 'yanle',
age: 20,
getAddress: function () {
alert('重庆')
}
};
// 不用with 的情况
// function fn() {
// alert(obj.name);
// alert(obj.age);
// obj.getAddress();
// }
// fn();
// 使用with的情况
function fn1() {
with (obj) {
alert(name);
alert(age);
getAddress();
}
}
fn1();
这种with 的使用方法就如上所述。但是尽量不要用,因为《JavaScript语言精粹》中,作者说过,这种使用方式会给代码的调试带来非常大的困难。
但是vue源码中的render 就是用的这个;
render函数:
模板中的所有信息都包含在了render 函数中。
一个特别简单的示例:
let vm = new Vue({
el: '#app',
data: {
price: 200
}
});
// 一下是手写的
function render() {
with (this) { // 就是vm
_c(
'div',
{
attr: {'id': 'app'}
},
[
_c('p', [_v(_s(price))])
]
)
}
}
function render1() {
return vm._c(
'div',
{
attr: {'id': 'app'}
},
[
_c('p', [vm._v(vm._s(vm.price))]) // vm._v 是创建文本, _s 就是toString
]
)
}
如果我们用一个复杂的例子来描述这个东西。在源码中, 搜索code.render, 然后在在此之前打印render 函数,就可以看看这个到底是什么东西了。
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
console.log(code.render);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
然后运行, 就可以看到到底render 函数是什么东西了。 就可以截取源码出来看了。
相对应的模板如下:
<div id="app">
<div>
<input v-model="title"> <br>
<button id="btn-submit" v-on:click="add">submit</button>
</div>
<div>
<ul id="ul-list">
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</div>
截取的render函数如下:
function codeRender() {
with (this) {
return _c('div',
{attrs: {"id": "app"}},
[
_c('div', [
_c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (title), // 渲染 指定数据
expression: "title"
}],
domProps: {"value": (title)}, // 渲染 指定数据
on: { // 通过input输入事件, 修改title
"input": function ($event) {
if ($event.target.composing) return;
title = $event.target.value
}
}
}),
_v(" "), // 文本节点
_c('br'),
_v(" "),
_c('button', { // dom 节点
attrs: {"id": "btn-submit"},
on: {"click": add} // methods 里面的东西也都挂在this上面去了
},
[_v("submit")])]),
_v(" "),
_c('div', [
_c('ul',
{attrs: {"id": "ul-list"}},
_l((list), function (item) { // 数组节点
return _c('li', [_v(_s(item))])
})
)
])
])
}
}
从vue2.0开始支持预编译, 我们在开发环境下,写模板, 编译打包之后, 模板就变成了JS代码了。vue已经有工具支持这个过程。
vue中的渲染
vue的渲染是直接渲染为虚拟dom ,这一块儿的内容,其实是借鉴的snabbdom, 非常相似,可以去看看snabbdom 就可以一目了然了。
vue中的具体渲染实现:
整体流程的实现
第一步: 解析模板形成render 函数
- with 用法
- 模板中的所有数据都被render 函数包含
- 模板中data的属性,变成了JS变量
- 模板中的v-model、v-for、v-on都变成了JS的逻辑
- render函数返回vnode
第二步: 响应式开始监听数据变化
- Object.defineProperty 的使用
- 讲data中的属性代理到vm 上
第三步: 首次渲染,显示页面,而且绑定数据和依赖
- 初次渲染, 执行updateComponent, 执行vm._render();
- 执行render函数, 会访问到vm.list和vm.title等已经绑定好了的数据;
- 会被详情是的get 方法监听到
为何一定要监听get, 直接监听set 不行吗? data中有很多的属性,有的被用到了,有的没有被用到。被用到的会走get, 不被用到的不会走get。 没有被get监听的属性,set的时候也不会被坚挺。为的就是减少不必要的重复渲染,节省性能。 - 执行updateComponent的时候,会执行vdom的patch方法
- patch 讲vnode渲染为DOM, 初次渲染完成
第四步: data属性变化,出发render
- 修改属性值, 会被响应式的set监听到
- set中会执行updateComponent, 重新执行vm.render()
- 生成vnode和prevVnode, 通过patch进行对比
- 渲染到html中
80.[Redux]: 看过 Redux 源码吗, 对 Redux 了解多少?【web框架】
深入Redux架构
目录:
- 1、关于redux
- 2、API
- 3、中间件与异步操作
- 4、异步操作的基本思路
- 5、React-Redux的用法
参考文章:http://www.cnblogs.com/MuYunyun/p/6530715.html
1、关于redux
1.1、什么情况需要用redux?
- 用户的使用方式复杂
- 不同身份的用户有不同的使用方式(比如普通用户和管理员)
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了WebSocket
- View要从多个来源获取数据
简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。多交互、多数据源场景就比较适合使用Redux。
1.2、设计思想
- Web 应用是一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面。
1.3、Redux工作流程
首先,用户发出 Action。
store.dispatch(action);
然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。
let nextState = todoApp(previousState, action);
State 一旦有变化,Store 就会调用监听函数。
// 设置监听函数
store.subscribe(listener);
listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
function listerner() {
let newState = store.getState();
component.setState(newState);
}
如果现在没理解以上流程,不要急,看完以下API就差不多能懂得Redux的核心机制了。
2、API
Store
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Redux 提供createStore这个函数,用来生成 Store。
下面代码中,createStore函数接受另一个函数作为参数,返回新生成的 Store 对象。
import { createStore } from 'redux';
const store = createStore(fn);
State
Store对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
当前时刻的 State,可以通过store.getState()拿到。
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
Action
State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置,社区有一个规范可以参考。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串Learn Redux。
可以这样理解,Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。
Action Creator
View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
store.dispatch()
store.dispatch()是 View 发出 Action 的唯一方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。
结合 Action Creator,这段代码可以改写如下。
store.dispatch(addTodo('Learn Redux'));
Reducer
Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。下面是一个实际的例子
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
const state = reducer(1, {
type: 'ADD',
payload: 2
});
上面代码中,reducer函数收到名为ADD的 Action 以后,就返回一个新的 State,作为加法的计算结果。 其他运算的逻辑(比如减法),也可以根据 Action 的不同来实现。
实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch方法会触发 Reducer 的自动执行。 为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。
import { createStore } from 'redux';
const store = createStore(reducer);
上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。 以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。
store.subscribe()
Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。
store.subscribe方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
unsubscribe();
3、中间件与异步操作
一个关键问题没有解决:异步操作怎么办?Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。
为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
想来想去,只有发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。
中间件的用法
本文不涉及如何编写中间件,因为常用的中间件都有现成的,只要引用别人写好的模块即可。
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
上面代码中,redux-logger提供一个生成器createLogger,可以生成日志中间件logger。 然后,将它放在applyMiddleware方法之中,传入createStore方法,就完成了store.dispatch()的功能增强。
这里有两点需要注意: (1)createStore方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware就是第三个参数了。
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
);
(2)中间件的次序有讲究。
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
上面代码中,applyMiddleware方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如,logger就一定要放在最后,否则输出结果会不正确。
4、异步操作的基本思路
理解了中间件以后,就可以处理异步操作了。
同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。
- 操作发起时的 Action
- 操作成功时的 Action
- 操作失败时的 Action
以向服务器取出数据为例,三种 Action 可以有两种不同的写法。
// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。
let state = {
// ...
isFetching: true,
didInvalidate: true,
lastUpdated: 'xxxxxxx'
};
上面代码中,State 的属性isFetching表示是否在抓取数据。didInvalidate表示数据是否过时,lastUpdated表示上一次更新时间。
现在,整个异步操作的思路就很清楚了。
- 操作开始时,送出一个 Action,触发 State 更新为”正在操作”状态,View 重新渲染
- 操作结束后,再送出一个 Action,触发 State 更新为”操作结束”状态,View 再一次重新渲染
总结
在异步请求的时候,其实很多时候都是直接发出请求如果请求成功了之后,在存入reducers, 并不是不管成功与否,都存入reducers。
redux-thunk中间件
异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢? 奥妙就在 Action Creator 之中。
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(getApplyList(selectedPost))
}
}
// ...
上面代码是一个异步组件的例子。加载成功后(componentDidMount方法),它送出了(dispatch方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit)。 这里的fetchPosts就是 Action Creator。
下面就是getApplyList的代码,关键之处就在里面, 这是我在公司的代码风格写法。
export function getApplyList(query) {
return function(dispatch) {
dispatch(modalUpdate({
loadingTable: true
}));
fetch('apply', query)
.then(function(res) {
dispatch(updateApply(res.data)); // 这个是调用的action Mppper
dispatch(modalUpdate({
loadingTable: false
}));
}).catch(function(err) {
dispatch(modalUpdate({
pageWarn: err.message,
loadingTable: false
}));
});
};
}
// 对应的action Mapper
export function updateApply(data) {
return {
type: UPDATE_APPLY,
data
};
}
这里是博客文章的代码风格写法
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
上面代码中,fetchPosts是一个Action Creator(动作生成器),返回一个函数。 这个函数执行后,先发出一个Action(requestPosts(postTitle)),然后进行异步操作。 拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json))。
上面代码中,有几个地方需要注意。
- (1)fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。
- (2)返回的函数的参数是dispatch和getState这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
- (3)在返回的函数之中,先发出一个 Action(requestPosts(postTitle)),表示操作开始。
- (4)异步操作结束之后,再发出一个 Action(receivePosts(postTitle, json)),表示操作结束。
这样的处理,就解决了自动发送第二个 Action 的问题。但是,又带来了一个新的问题,Action 是由store.dispatch方法发送的。 而store.dispatch方法正常情况下,参数只能是对象,不能是函数。
这时,就要使用中间件redux-thunk。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);
上面代码使用redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。
因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用redux-thunk中间件改造store.dispatch。
5、React-Redux的用法
为了方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux,本文主要介绍它。
这个库是可以选用的。实际项目中,你应该权衡一下,是直接使用 Redux,还是使用 React-Redux。后者虽然提供了便利,但是需要掌握额外的 API,并且要遵守它的组件拆分规范。
本人项目中使用的最多的就是 react-redux;
React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。
UI组件
UI 组件有以下几个特征。
- 只负责 UI 的呈现,不带有任何业务逻辑
- 没有状态(即不使用this.state这个变量)
- 所有数据都由参数(this.props)提供
- 不使用任何 Redux 的 API
下面就是一个 UI 组件的例子。
const Title = value => <h1>{value}</h1>;
因为不含有状态,UI 组件又称为”纯组件”,即它纯函数一样,纯粹由参数决定它的值。
容器组件
容器组件的特征恰恰相反。
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态
- 使用 Redux 的 API
总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。
你可能会问,如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。
React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。
connect()
React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。
connect方法的完整 API 如下。下面这个例子是我在项目中使用的一个完整结构示例
/* eslint-disable react/jsx-no-target-blank */
import React, {Component} from 'react';
import {push} from 'react-router-redux';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {Button, message} from 'antd';
// mapStateToProps
function propMap(state, ownProps) {
return {
modal: state.modal,
routing: ownProps
};
}
class InvoiceList extends Component {
constructor() {
super();
this.state = {
invoiceListData: {}
};
this.handleGetList = this.handleGetList.bind(this);
}
componentDidMount() {
// 每次刷新空拉数据一次
this.handleGetList();
}
render() {
const {routing, modal} = this.props;
return (
<div className="app-reimbursement-invoice-list">
<ReimbursementHeaderNav current="invoice-list"/>
{/*.......*/}
</div>
);
}
// 点击查询数据
handleGetList(filters, type) {
console.log('点击查询数据')
}
}
InvoiceList.propTypes = {
routing: PropTypes.object.isRequired,
modal: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired
};
export default connect(propMap)(InvoiceList);
InvoiceList就是由 React-Redux 通过connect方法自动生成的容器组件。 connect方法接受两个参数:mapStateToProps和mapDispatchToProps。 它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。 通常我们只使用了第一个参数;
mapStateToProps
mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。 作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数, 后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。
下面就是getVisibleTodos的一个例子,用来算出todos。
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。
mapDispatchToProps()
mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。 也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator , 返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
}
}
总结,实际上项目开发过程中, 只用得上第一个参数,第二个参数一般来说是封装在reducers 层次里面的。不建议直接放置在组建成此调用。因为会导致使用和数据上的紊乱。
组件
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。React-Redux 提供Provider组件,可以让容器组件拿到state。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。
React-Router路由库
使用React-Router的项目,与其他项目没有不同之处,也是使用Provider在Router外面包一层,毕竟Provider的唯一功能就是传入store对象。
const Root = ({ store }) => (
<Provider store={store}>
<Router>
<Route path="/" component={App} />
</Router>
</Provider>
);
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/35261.html