JavaScript设计模式 - 技巧型设计模式
永无尽头-链模式
链模式(Operate of Responsibility)
通过在对象方法中将当前对象返回,实现对同一个对象多个方法的链式调用。从而简化对该对象的多个方法的多次调用时,对该对象的多次引用。
深究jQuery(参考版本: 2.2.4版本
)
jQuery的点语法是基于原型继承实现的,并且在每一个原型方法的实现上都返回当前对象this,使当前对象一直处于原型链作用域的顶端。
实现类似于jQuery
的框架语法
<p id="text"></p>
// 实现目标如下:
let text = $('#text');
console.log(text); // jQuery.fn.init [p#text, context: document, selector: '#text']
console.log(text.size()); // 1
text
.css({
padding: '10px',
'line-height': '30px',
'background-color': 'red'
})
.attr('class', 'user-name')
.html('ProsperLee')
.on('click', function () {
console.log('clicked');
});
console.log(text); // jQuery.fn.init [p#text.user-name, context: document, selector: '#text']
原型式继承
let A = function () {
};
A.prototype = {
length: 0,
size: function () {
return this.length;
}
};
let a = new A();
console.log(a); // A{}
console.log(a.__proto__); // {length: 0, size: ƒ}
a.size(); // 0
A.prototype.size(); // 0
A(); // undefined
A().size(); // 【Error】 Uncaught TypeError: Cannot read properties of undefined (reading 'size')
A.size(); // 【Error】 Uncaught TypeError: A.size is not a function
找位助手
此时因为A()
返回的是undefined
,所以使用A().size();
访问不到.于是我们可以借助第三方对象进行访问
let A = function () {
return B;
};
let B = A.prototype = {
length: 0,
size: function () {
return this.length;
}
};
A().size(); // 0
而在jQuery
中,为了减少变量的定义,将想使用的变量作为了A
的一个属性
let A = function () {
return A.fn;
};
A.fn = A.prototype = {
length: 0,
size: function () {
return this.length;
}
};
A().size(); // 0
获取元素
jQuery的目的是为了获取元素,返回的是一组元素簇(元素的聚合对象),但现在返回的却是一个A.fn对象,显然达不到我们的需求;所以,如果A.fn能提供给我们一个获取元素的方法init就好了.
let A = function (selector) {
return A.fn.init(selector);
};
A.fn = A.prototype = {
init: function (selector) {
return document.getElementById(selector);
},
length: 0,
size: function () {
return this.length;
}
};
A('container'); // <div id="container"></div>
A('container').size(); // Error ---> Uncaught TypeError: A(...).size is not a function
两个问题
此时是无法访问size
方法的,因为init
返回的是DOM
元素,故可以使用this
解决
let A = function (selector) {
return A.fn.init(selector);
};
A.fn = A.prototype = {
init: function (selector) {
this.selector = selector;
this[0] = document.getElementById(selector);
this.length = 1; // 重置length
return this;
},
length: 0,
size: function () {
return this.length;
}
};
A('container'); // {0: div#container, length: 1, selector: 'container', init: ƒ, size: ƒ}
A('container').size(); // 1
覆盖问题: box2覆盖了box1
let box1 = A('container1');
let box2 = A('container2');
console.log(box1); // {0: div#container2, length: 1, selector: 'container2', init: ƒ, size: ƒ}
console.log(box2); // {0: div#container2, length: 1, selector: 'container2', init: ƒ, size: ƒ}
覆盖获取
后获取id的DOM元素会将先获取id的DOM元素进行覆盖,是因为每次在A的构造函数中返回的A.fn.init(selector);对象指向同一个对象造成的
解决方法
: 使用new关键字进行处理,让被赋值的变量都是独立的对象
let A = function (selector) {
return new A.fn.init(selector);
};
A.fn = A.prototype = {
init: function (selector) {
this.selector = selector;
this[0] = document.getElementById(selector);
this.length = 1; // 重置length
return this;
},
length: 0,
size: function () {
return this.length;
}
};
let box1 = A('container1');
let box2 = A('container2');
console.log(box1); // {0: div#container1, length: 1, selector: 'container1', init: ƒ, size: ƒ}
console.log(box2); // {0: div#container2, length: 1, selector: 'container2', init: ƒ, size: ƒ}
box1.length; // 1
box1.size(); // Error ---> Uncaught TypeError: box1.size is not a function
方法丢失
因为我们使用new关键字,导致无法在init原型上找到size方法,导致方法丢失,所以访问size方法报错
let A = function (selector) {
return new A.fn.init(selector);
};
A.fn = A.prototype = {
init: function (selector) {
console.log(this === A.fn);
console.log(this === A.prototype);
this.selector = selector;
this[0] = document.getElementById(selector);
this.length = 1; // 重置length
return this;
},
length: 0,
size: function () {
return this.length;
}
};
// 未使用new的
A.fn.init('container'); // true true
// 使用了new的,所以找不到size方法,解决方案请向下看
A('container'); // false false
对比jQuery
对比返回对象的构造器constructor
let A = function (selector) {
return new A.fn.init(selector);
};
A.fn = A.prototype = {
init: function (selector) {
this.selector = selector;
this[0] = document.getElementById(selector);
this.length = 1; // 重置length
return this;
},
length: 0,
size: function () {
return this.length;
}
};
console.log($('#container').constructor.name); // jQuery
console.log(A('container').constructor.name); // init
解决方法: 故需要我们去强化构造器
let A = function (selector) {
return new A.fn.init(selector);
};
A.fn = A.prototype = {
constructor: A, // 强化构造函数,使构造器constructor返回A对象
init: function (selector) {
this.selector = selector;
this[0] = document.getElementById(selector);
this.length = 1; // 重置length
return this;
},
length: 0,
size: function () {
return this.length;
}
};
// 要访问size方法需要添加这一行
A.fn.init.prototype = A.fn; // 单独添加这一行会导致constructor返回的是Object
console.log($('#container').constructor.name); // jQuery
console.log(A('container').constructor.name); // A
A('container').size(); // 1
丰富元素获取
丰富功能,可以获取各种选择器返回的DOM元素
<div id="box0">
<ul id="box1">
<li class="item"></li>
<li></li>
<li class="item"></li>
</ul>
<div class="item"></div>
</div>
let A = function (selector, context) {
return new A.fn.init(selector, context);
};
A.fn = A.prototype = {
constructor: A,
/**
* 初始化
* @param selector 选择器
* @param context 上下文
* @returns {A} 返回A对象
*/
init: function (selector, context) {
this.selector = selector;
this.context = context || document;
let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
this.length = doms.length;
doms.forEach((dom, index) => this[index] = dom);
return this;
},
length: 0,
size: function () {
return this.length;
}
};
A.fn.init.prototype = A.fn;
/**
* init {
* 0: div#box0,
* selector: '#box0',
* context: document,
* length: 1
* }
*/
console.log(A('#box0'));
/**
* init {
* 0: li.item,
* 1: li.item,
* 2: div.item,
* selector: '.item',
* context: document,
* length: 3
* }
*/
console.log(A('.item'));
/**
* init {
* 0: li.item,
* 1: li,
* 2: li.item,
* selector: 'li',
* context: document,
* length: 3
* }
*/
console.log(A('li'));
/**
* init {
* 0: li.item,
* 1: li.item,
* selector: '.item',
* context: ul#box1,
* length: 2
* }
*/
console.log(A('.item', document.getElementById('box1')));
/**
* init {
* selector: 'xxx',
* context: document,
* length: 0
* }
*/
console.log(A('xxx'));
数组与对象
由于JavaScript的弱类型语言,并且数组、对象、函数都被看成是对象的实例,所以JavaScript中并没有一个纯粹的数组类型,而且JavaScript引擎的实现也没有做严格的校验,也是基于对象实现的.
一些浏览器解析引擎在判断对象是否是数组的时候不仅仅判断其有没有length属性,可否通过
[索引值]
方式访问元素,还会判断其是否具有数组方法来确定是否要用数组的形式展现,所以我们只需要在A.fn中添加几个数组常用的方法来增强数组特性就可以解决问题了
let A = function (selector, context) {
return new A.fn.init(selector, context);
};
A.fn = A.prototype = {
constructor: A,
// 增强数组(参见访问者模式)
push: [].push,
sort: [].sort,
splice: [].splice,
/**
* 初始化
* @param selector 选择器
* @param context 上下文
* @returns {A} 返回A对象
*/
init: function (selector, context) {
this.selector = selector;
this.context = context || document;
let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
this.length = doms.length;
doms.forEach((dom, index) => this[index] = dom);
return this;
},
length: 0,
size: function () {
return this.length;
}
};
A.fn.init.prototype = A.fn;
console.log(A('#text')); // init [p#text.user-name, selector: '#text', context: document] ---> length: 1
方法拓展
对内部对象的拓展和对外部对象的拓展
let A = function (selector, context) {
return new A.fn.init(selector, context);
};
A.fn = A.prototype = {
constructor: A,
// 增强数组
push: [].push,
sort: [].sort,
splice: [].splice,
/**
* 初始化
* @param selector 选择器
* @param context 上下文
* @returns {A} 返回A对象
*/
init: function (selector, context) {
this.selector = selector;
this.context = context || document;
let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
this.length = doms.length;
doms.forEach((dom, index) => this[index] = dom);
return this;
},
length: 0,
size: function () {
return this.length;
}
};
A.fn.init.prototype = A.fn;
/**
* 对象拓展 - 对内部对象的拓展和对外部对象的拓展
* @type {A.extend}
*/
A.extend = A.fn.extend = function (...args) {
// 如果只存在一个参数,那么是对内部对象进行拓展;
// 如果是多个参数,那么第一个参数作为需要拓展的对象,为外部对象的拓展;
let target = args.length <= 1 ? this : args[0];
args.forEach(arg => {
for (const k in arg) {
target[k] = arg[k];
}
});
return target;
};
A.extend({
name: 'Tom'}, {
age: 18}); // {name: 'Tom', age: 18}
A.extend(A.fn, {
version: '1.0.0',
getVersion: function () {
return this.version;
}
});
console.log(A('container').getVersion()); // 1.0.0
A.extend({
version: 'v1.0.0',
getVersion: function () {
return this.version;
}
});
console.log(A.getVersion()); // v1.0.0
添加方法并使用
<button class="btn">按钮01</button>
<button class="btn">按钮02</button>
<button class="btn">按钮03</button>
let A = function (selector, context) {
return new A.fn.init(selector, context);
};
A.fn = A.prototype = {
constructor: A,
// 增强数组
push: [].push,
sort: [].sort,
splice: [].splice,
/**
* 初始化
* @param selector 选择器
* @param context 上下文
* @returns {A} 返回A对象
*/
init: function (selector, context) {
this.selector = selector;
this.context = context || document;
let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
this.length = doms.length;
doms.forEach((dom, index) => this[index] = dom);
return this;
},
length: 0,
size: function () {
return this.length;
}
};
A.fn.init.prototype = A.fn;
/**
* 对象拓展 - 对内部对象的拓展和对外部对象的拓展
* @type {A.extend}
*/
A.extend = A.fn.extend = function (...args) {
// 如果只存在一个参数,那么是对内部对象进行拓展;
// 如果是多个参数,那么第一个参数作为需要拓展的对象,为外部对象的拓展;
let target = args.length <= 1 ? this : args[0];
args.forEach(arg => {
for (const k in arg) {
target[k] = arg[k];
}
});
return target;
};
A.extend({
// 将'-'分割线转化为驼峰式,如:'border-color' -> 'borderColor'
camelCase: function (str) {
return str.replace(/-(\w)/g, function (all, letter) {
return letter.toUpperCase();
});
},
});
A.fn.extend({
css: function (obj) {
const len = this.length;
for (let i = 0; i < len; i++) {
for (const k in obj) {
this[i].style[A.camelCase(k)] = obj[k];
}
}
return this;
},
attr: function (key, value) {
const len = this.length;
for (let i = 0; i < len; i++) {
this[i].setAttribute(key, value);
}
return this;
},
html: function (htmlStr) {
const len = this.length;
for (let i = 0; i < len; i++) {
this[i].innerHTML = htmlStr;
}
return this;
},
on: function (type, fn) {
const len = this.length;
for (let i = 0; i < len; i++) {
this[i].addEventListener(type, function (e) {
fn.call(null, e);
}, false);
}
return this;
},
});
let btns = A('.btn');
btns
.css({
padding: '3px 10px',
borderRadius: '3px',
borderStyle: 'solid',
'border-width': '1px',
'border-color': 'red',
color: 'red'
})
.attr('class', 'btn btn-item')
.html('按钮喽~~~')
.on('click', function (e) {
console.log(e.target);
});
/**
* init(3) [
* button.btn.btn-item,
* button.btn.btn-item,
* button.btn.btn-item,
* selector: '.btn',
* context: document,
* length: 3
* ]
*/
console.log(btns);
函数、函数简写、箭头函数
区别
let obj = {
fnA: function () {
// console.log(fnA); // Error ---> Uncaught ReferenceError: fnA is not defined
console.log(this); // obj
},
fnB() {
// console.log(fnB); // Error ---> Uncaught ReferenceError: fnB is not defined
console.log(this); // obj
},
fnC: function fnD() {
// console.log(fnC); // Error ---> Uncaught ReferenceError: fnC is not defined
console.log(fnD); // ƒ fnD() { ... }
console.log(this); // obj
},
fnE: () => {
// console.log(fnE); // Error ---> Uncaught ReferenceError: fnE is not defined
console.log(this); // window
}
};
obj.fnA();
obj.fnB();
obj.fnC();
obj.fnE();
未来预言家-委托模式
委托模式(Entrust)
多个对象接收并处理同一请求,他们将请求委托给另一个对象统一处理请求。
点击事件绑定
<ul id="btn-box">
<li>
<button class="btn">按钮1</button>
</li>
<li>
<button class="btn">按钮2</button>
</li>
<li>
<button class="btn">按钮3</button>
</li>
<li>
<button class="btn">按钮4</button>
</li>
<li>
<button class="btn">按钮5</button>
</li>
<li>
<button class="btn">按钮6</button>
</li>
</ul>
循环绑定
let btns = document.querySelectorAll('.btn');
let len = btns.length;
for (let i = 0; i < len; i++) {
btns[i].onclick = function (e) {
this.style.backgroundColor = 'red';
};
}
委托给父元素,进行事件绑定
let btnBox = document.querySelector('#btn-box');
btnBox.onclick = function (e) {
if (e.target.nodeName === 'BUTTON') {
e.target.style.backgroundColor = 'red';
}
};
数据分发
原来我们处理数据的方法
// 比如原来我们处理数据是这样的,我们需要请求5此才能完善所有模块
$.get("./deal.php?q=banner", function (res) {
// 处理banner模块逻辑
})
$.get("./deal.php?q=aside", function (res) {
// 处理aside模块逻辑
})
$.get("./deal.php?q=article", function (res) {
// 处理article模块数据
})
$.get("./deal.php?q=member", function (res) {
// 处理member模块数据
})
$.get("./deal.php?q=message", function (res) {
// 处理message模块数据
})
使用了委托模式处理数据的方法
// 使用委托后处理数据是这样的,我们只需要请求一次,就能用不同的方法完善所有模块
var Deal = {
banner: function () {
// 处理banner模块逻辑
},
aside: function (res) {
// 处理aside模块逻辑
},
article: function (res) {
// 处理article模块数据
},
member: function (res) {
// 处理member模块数据
},
message: function (res) {
// 处理message模块数据
}
}
$.get('./deal.php?', function (res) {
//数据拆包分发
for (var k in res) {
Deal[k] && Deal[k](res[k]);
}
});
特点
委托模式是通过委托者将请求委托给被委托者去处理实现的。因此委托模式解决了请求与委托者之间的耦合。通过被委托者对接收到的请求的处理后,分发给相应的委托者去处理。
数据管理器-数据访问对象模式
数据访问对象模式(Data access object-DAO)
抽象和封装对数据源的访问与存储,DAO通过对数据源链接的管理方便对数据的访问与存储
封装本地存储
/**
* 定义本地存储类
* @param preId 存储库前缀
*/
const BaseLocalStorage = function (preId) {
this.preId = preId;
};
// 本地存储类原型方法
BaseLocalStorage.prototype = {
// 状态
state: {
FAILURE: 0, // 失败
SUCCESS: 1, // 成功
OVERFLOW: 2, // 溢出
},
// 存储工具
storage: localStorage || window.localStorage,
// 获取数据库的真是字段
getKey(key) {
return this.preId + key;
},
/**
* 新增/修改 数据到数据库
* @param key 不带前缀的属性
* @param value 值
* @param callback 回调(传入 `状态,key,值`)
*/
set(key, value, callback) {
let state = this.state.SUCCESS;
let realKey = this.getKey(key); // 真实key
try {
this.storage.setItem(realKey, value);
} catch (e) {
// 存不进去了,返回溢出窗台
state = this.state.OVERFLOW;
}
callback && callback.call(this, {
state, key, value});
},
/**
* 查询数据到数据库
* @param key 不带前缀的属性
* @param callback 回调(传入 `状态,key,值`)
*/
get(key, callback) {
let state = this.state.SUCCESS;
let realKey = this.getKey(key); // 真实key
let value = null;
try {
value = this.storage.getItem(realKey);
} catch (e) {
// 获取失败
state = this.state.FAILURE;
}
callback && callback.call(this, {
state, key, value});
},
/**
* 删除数据
* @param key 不带前缀的属性
* @param callback 回调(传入 `状态,key`)
*/
remove(key, callback) {
let state = this.state.SUCCESS;
let realKey = this.getKey(key); // 真实key
try {
this.storage.removeItem(realKey);
} catch (e) {
// 获取失败
state = this.state.FAILURE;
}
callback && callback.call(this, {
state, key});
}
};
const s = new BaseLocalStorage('Lee-');
s.set('name', 'Lee', function ({
state, key, value}) {
console.log(state, key, value); // 1 'name' 'Lee'
});
s.get('name', function ({
state, key, value}) {
console.log(state, key, value); // 1 'name' 'Lee'
});
s.remove('name', function ({
state, key}) {
console.log(state, key); // 1 'name'
});
s.get('name', function ({
state, key, value}) {
console.log(state, key, value); // 1 'name' null
});
MongoDB
config.js
module.exports = { // 数据库相关配置数据 DB: { db: 'demo', // 数据库名称 host: '192.168.3.91', // 主机名 port: 27017, // 端口号 } };
db.js
const { MongoClient } = require('mongodb'); const config = require('./config').DB; // 创建数据库对象 const url = `mongodb://${ config.host}:${ config.port}`; const mongoClient = new MongoClient(url); /** * 连接数据库 * @param {string} col 表名 * @param {Function} fn 回调 */ function connect(col, fn) { mongoClient.connect() .then(client => { const db = client.db(config.db); const collection = db.collection(col); fn && fn(collection, client); }) .catch(err => { throw err; }); } // 输出数据访问对象 exports.DB = function (col) { return { /** * 插入一条数据 * @param {data, success, fail} param * data 插入数据 * success 成功回调 * fail 失败回调 */ insertOne({ data, success = () => { }, fail = () => { } }) { connect(col, (collection, client) => { collection.insertOne(data) .then(res => success(res)) .catch(err => fail(err)) .finally(() => client.close()); // 关闭数据库 }); }, /** * 删除一条数据 * @param {data, success, fail} param * q 查询数据 * success 成功回调 * fail 失败回调 */ deleteMany({ q, success = () => { }, fail = () => { } }) { connect(col, (collection, client) => { collection.deleteMany(q) .then(res => success(res)) .catch(err => fail(err)) .finally(() => client.close()); }); }, /** * 修改一条数据 * @param {data, success, fail} param * q 查询数据 * data 修改数据 * success 成功回调 * fail 失败回调 */ updateOne({ q, data, success = () => { }, fail = () => { } }) { connect(col, (collection, client) => { collection.updateOne(q, { $set: data }) .then(res => success(res)) .catch(err => fail(err)) .finally(() => client.close()); }); }, /** * 查询数据 * @param {data, success, fail} param * q 查询数据 * success 成功回调 * fail 失败回调 */ find({ q, success = () => { }, fail = () => { } }) { connect(col, (collection, client) => { collection.find(q).toArray((err, res) => { if (err) { fail(err); } else { success(res); } client.close() }); }); }, }; };
app.js
const DB = require('./db').DB; // 操作user表 const user = DB('user'); // 增 user.insertOne({ data: { name: 'Lee', age: 18, desc: '简介啦~~~' }, success(res) { console.log(res); }, fail(err) { console.log(err.message); } }); // 删 user.deleteMany({ q: { name: 'Lee' }, success(res) { console.log(res); }, fail(err) { console.log(err.message); } }); // 改 user.updateOne({ q: { name: 'Lee' }, data: { age: 20 }, success(res) { console.log(res); }, fail(err) { console.log(err.message); } }); // 查 user.find({ q: { }, success(res) { console.log(res); }, fail(err) { console.log(err.message); } }); const book = DB('book'); // 增 book.insertOne({ data: { name: '《Tom》', desc: '简介啦~~~' }, success(res) { console.log(res); }, fail(err) { console.log(err.message); } });
执行控制-节流模式
节流模式(Throttler)
对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能。
简单的节流器示例
// 节流器
const throttle = (function () {
let timer = null;
return function ({
handle = () => {
}, time = 300 }) {
// 清除计时器句柄
clearTimeout(timer);
// 创建计时器句柄,延迟函数的执行
timer = setTimeout(() => {
handle();
}, time);
};
}());
let n = 0;
window.onresize = function () {
throttle({
handle() {
console.log(n++);
},
time: 500
});
};
鼠标移入移出示例
鼠标移入/移出标题上时,显示/隐藏简介
<style>
#desc {
display: none;
background-color: #eee;
border: 1px solid #ccc;
padding: 10px 20px;
}
#content {
text-align: justify;
text-indent: 2em;
}
</style>
<h3 id="title">You瞧谁不起 - 道不尽世间的沧桑,诉不完人生的悲凉</h3>
<p id="desc">简介:人海茫茫,你我依旧孤独~~~</p>
<div id="content">
就我个人来说,中午吃什么对我的意义,不能不说非常重大。 叔本华说过一句富有哲理的话,普通人只想到如何度过时间,有才能的人设法利用时间。这句话语虽然很短,但令我浮想联翩。 贝多芬说过一句富有哲理的话,卓越的人一大优点是:在不利与艰难的遭遇里百折不饶。这似乎解答了我的疑惑。 我认为, 了解清楚中午吃什么到底是一种怎么样的存在,是解决一切问题的关键。 一般来讲,我们都必须务必慎重的考虑考虑。 吕凯特曾经说过,生命不可能有两次,但许多人连一次也不善于度过。这似乎解答了我的疑惑。 一般来说, 一般来讲,我们都必须务必慎重的考虑考虑。 中午吃什么因何而发生。
</div>
function $(id) {
return document.getElementById(id);
}
$('title').addEventListener('mouseenter', function (e) {
$('desc').style.display = 'block';
}, false);
$('title').addEventListener('mouseleave', function (e) {
$('desc').style.display = 'none';
}, false);
- 缺点:
- 当我们阅读下边内容时,鼠标不小心移入移除到标题区域时,简介区域会立即的显示隐藏,很影响我们后续的布局和阅读体验
节流模式
解决上述问题
function $(id) {
return document.getElementById(id);
}
// 节流器
const throttle = (function () {
let timer = null;
return function ({
handle = () => {
}, time = 300 }) {
// 清除计时器句柄
clearTimeout(timer);
// 创建计时器句柄,延迟函数的执行
timer = setTimeout(() => {
handle();
}, time);
};
}());
$('title').addEventListener('mouseenter', function (e) {
throttle({
handle() {
$('desc').style.display = 'block';
},
time: 500
});
}, false);
$('title').addEventListener('mouseleave', function (e) {
throttle({
handle() {
$('desc').style.display = 'none';
},
time: 500
});
}, false);
输入框输入请求接口示例
避免每次输入都立即去调用接口
function $(id) {
return document.getElementById(id);
}
// 节流器
const throttle = (function () {
let timer = null;
return function ({
handle = () => {
}, time = 300 }) {
// 清除计时器句柄
clearTimeout(timer);
// 创建计时器句柄,延迟函数的执行
timer = setTimeout(() => {
handle();
}, time);
};
}());
// 搜索接口
function search(q) {
console.log(q.value);
}
$('search').addEventListener('input', function (e) {
throttle({
handle() {
search(e.target);
},
time: 500
})
});
特点
痛点
:- 对于
DOM的操作
,常常会占用大量的内存资源
和cpu处理时间
。甚至大量的DOM操作
在一些浏览器中也很可能导致浏览器的崩溃
。由于JavaScript
的单线程处理机制
,导致DOM操作占用大量资源
时会严重堵塞后面重要程序的执行
。
- 对于
节流模式的核心思想
:- 核心思想是
创建计时器
,延迟程序的执行
。这也使得计时器中回调函数
的操作异步执行
- 这里的
异步执行
并不是
说JavaScript是多线程语言
,JavaScript
从设计之初就是单线程语言
,异步
只是说脱离原来程序执行的顺序
,看上去,异步程序
像是在同时执行
。但是某一时刻
,当前执行的程序
一定是所有异步程序(包括原程序)中的某一个
- 这里的
- 核心思想是
- 由此可看出节流模式主要有
两点优势
:程序能否执行是可控的
- 执行前的某一时刻是否清除计时器来决定程序是否可以继续执行;
程序是异步的
- 由于计时器机制,使得程序脱离原程序而异步执行(当然随着worker技术的兴起,也可开启多线程模式实现),因此不会影响后面的程序的正常执行。在其他方面,比如对异步请求(ajax)应用节流,此时可以优化请求次数来节省资源;
卡片拼图-简单模板模式
简单模板模式(Simple template)
通过格式化字符串拼凑出视图避免创建视图时大量节点操作。优化内存开销。
<div id="container"></div>
创建文字列表视图
// 命名空间
const A = {
// 主体展示区容器
root: document.getElementById('container'),
// 创建视图方法集合
strategy: {
// 文字列表展示
listPart({
data: d}) {
let part = document.createElement("div"); // 模块容器
let titleBox = document.createElement("h2"); // 标题容器
let title = document.createTextNode(d.title); // 标题内容
let descBox = document.createElement("p"); // 描述容器
let desc = document.createTextNode(d.desc); // 描述内容
let listBox = document.createElement("ul"); // 列表容器
titleBox.appendChild(title); // 将标题内容放入标题容器中
descBox.appendChild(desc); // 将描述内容放入描述容器中
part.className = "part"; // 设置模块类名
part.appendChild(titleBox); // 将标题容器插入模块容器中
part.appendChild(descBox); // 将描述容器插入模板容器中
let list = d.list; // 列表数据
let rowBox, labelBox, label, valueBox, value;
// 遍历列表数据
list.forEach(item => {
rowBox = document.createElement("li"); // 创建列表项容器
labelBox = document.createElement("strong"); // 创建列表项标题容器
label = document.createTextNode(item.label); // 创建列表项标题内容
valueBox = document.createElement("span"); // 创建列表项解释容器
value = document.createTextNode(item.value); // 创建列表项解释内容
labelBox.appendChild(label);
valueBox.appendChild(value);
rowBox.appendChild(labelBox);
rowBox.appendChild(valueBox);
listBox.appendChild(rowBox);
});
part.appendChild(listBox); // 向模块中插入列表
A.root.appendChild(part); // 展现模块
},
codePart(data) {
/* ···CODE··· */
},
onlyTitle(data) {
/* ···CODE··· */
},
guide(data) {
/* ···CODE··· */
},
/* ···CODE··· */
},
// 创建视图入口
init: function (data) {
// 根据传输的视图类型创建视图
this.strategy[data.type](data);
}
}
A.init({
type: 'listPart',
data: {
title: 'You瞧谁不起 - 道不尽世间的沧桑,诉不完人生的悲凉',
desc: '人海茫茫,你我依旧孤独~~~',
list: [
{
label: '姓名:', value: 'Lee'},
{
label: '年龄:', value: 18},
{
label: '性别:', value: '男'}
]
}
});
⚠ 缺点
:
创建一个简单的视图就要操作这么多大节点,如果需求创建一个比较复杂的页面,那么很难想象会操作每个节点多少次。当然这过程中还可能会遇到像添加类、绑定事件什么的,这样你的开销就大了。
新方案(简单模板模式
)
// 命名空间
const A = {
// 模板渲染方法
formatString(str, data) {
return str.replace(/\{#(\w+)#\}/g, function (match, key) {
return typeof data[key] === undefined ? '' : data[key]
});
},
// 视图模板
view(data) {
const tpl = {
listPart: {
row: `<li><strong>{#label#}</strong><span>{#value#}</span></li>`,
theme: `
<div class="{#class#}">
<h2>{#title#}</h2>
<p>{#desc#}</p>
<ul>{#content#}</ul>
</div>
`
},
codePart: {
},
onlyTitle: {
},
guide: {
},
/* ···CODE··· */
};
let rowStr = data.list.map(d => this.formatString(tpl.listPart.row, d)).join('');
return this.formatString(tpl.listPart.theme, {
...data, content: rowStr});
},
// 主体展示区容器
root: document.getElementById('container'),
// 创建视图方法集合
strategy: {
// 文字列表展示
listPart({
data: d}) {
A.root.innerHTML = A.view({
...d, class: 'part'});
},
codePart(data) {
/* ···CODE··· */
},
onlyTitle(data) {
/* ···CODE··· */
},
guide(data) {
/* ···CODE··· */
},
/* ···CODE··· */
},
// 创建视图入口
init: function (data) {
// 根据传输的视图类型创建视图
this.strategy[data.type](data);
}
}
A.init({
type: 'listPart',
data: {
title: 'You瞧谁不起 - 道不尽世间的沧桑,诉不完人生的悲凉',
desc: '人海茫茫,你我依旧孤独~~~',
list: [
{
label: '姓名:', value: 'Lee'},
{
label: '年龄:', value: 18},
{
label: '性别:', value: '男'}
]
}
});
特点
简单模板模式主要包含三部分,字符串模板库,格式化方法,字符串拼接操作。
简单模板模式意在解决运用DOM操作创建视图时造成资源消耗大、性能低下、操作复杂等问题。
用正则匹配方式去格式化字符串的执行的性能要远高于DOM操作拼接视图的执行性能,因此这种方式常备用于大型框架(如MVC等)创建视图操作中。
机器学习-惰性模式
惰性模式(layier)
减少每次代码执行时的重复性的分支判断,通过对对象重定义来屏蔽原对象中的分支判断。
<button id="a1">按钮01</button>
<button id="b1">按钮02</button>
<button id="c1">按钮03</button>
function $(id) {
return document.getElementById(id);
}
一般分支判断调用
每次调用都走一遍检测,以后每次调用都消耗一次性能
let A = {
// 添加绑定事件方法on
on(dom, type, fn) {
if (dom.addEventListener) {
dom.addEventListener(type, fn, false);
} else if (dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else {
dom['on' + type] = fn;
}
},
};
A.on($('a1'), 'click', function (e) {
console.log(e.target, e);
});
利用闭包进行分支判断
加载即执行:首次加载js就执行了闭包功能,意味着首次加载js消耗性能较大,以后每次调用消耗性能小
let B = {
on: (function () {
// 如果支持addEventListener方法
if (document.addEventListener) {
// 返回新定义方法
return function (dom, type, fn) {
dom.addEventListener(type, fn, false);
}
// 如果支持attachEvent方法(IE)
} else if (document.attachEvent) {
// 返回新定义方法
return function (dom, type, fn) {
dom.attachEvent('on' + type, fn);
}
// 定义on方法
} else {
// 返回新定义方法
return function (dom, type, fn) {
dom['on' + type] = fn;
}
}
}()),
};
B.on($('b1'), 'click', function (e) {
console.log(e.target, e);
});
惰性绑定
利用方法的重写覆盖实现,只有第一次调用的时候消耗性能,以后每次调用消耗性能较小
let C = {
on(dom, type, fn) {
// 如果支持addEventListener方法
if (dom.addEventListener) {
// 显示重定义on方法
A.on = function (dom, type, fn) {
dom.addEventListener(type, fn, false);
};
// 如果支持attachEvent方法(IE)
} else if (dom.attachEvent) {
// 显示重定义on方法
A.on = function (dom, type, fn) {
dom.attachEvent('on' + type, fn);
};
// 如果支持DOM0级事件绑定
} else {
// 显示重定义on方法
A.on = function (dom, type, fn) {
dom['on' + type] = fn;
};
}
// 执行重定义on方法
A.on(dom, type, fn);
}
};
C.on($('c1'), 'click', function (e) {
console.log(e.target, e);
});
特点:
惰性模式是一种拖延模式,由于对象的创建或者数据的计算会花费高昂的代价(如页面刚加载时无法辨别是该浏览器支持某个功能,此时创建对象是不够安全的),因此页面之处会延迟对这一类对象的创建。
- 惰性模式又分为两种:
- 文件加载后立即执行对象方法来重定义对象,由于文件加载时执行,因此会占用一些资源;
- 当第一次使用方法对象时重定义对象,由于在第一次使用时重定义对象,以致第一次执行时间增加
- 有时候两种方式对资源的开销都是可接受的,因此到底使用哪种方式,要看具体需求而定
异国战场-参与者模式
参与者(participator)
在特定的作用域中执行给定的函数,并将参数原封不动地传递
需求:实现系统的bind功能
传递参数
存在的
缺点
:添加的事件回调函数不能移除(removeEventListener)
function $(id) {
return document.getElementById(id);
}
const A = {
on(dom, type, fn, ...args) {
dom.addEventListener(type, function (e) {
fn && fn(dom, e, args);
}, false);
}
};
A.on($('btn1'), 'click', function (dom, e, args) {
console.log(dom, e, args); // DOM PointerEvent{} [{ name: 'Lee' }, { age: 18 }]
}, {
name: 'Lee' }, {
age: 18 });
函数绑定
实现建简易的
bind
功能
function bind(context, fn) {
return function (...args) {
return fn.apply(context, args);
}
}
let bindFn = bind({
name: 'Lee', age: 18 }, function (msg) {
console.log(`${
msg} ${
this.name}`);
});
bindFn('Hello'); // Hello Lee
将bind
应用于事件
这种方式传参还是存在一定的局限性,我们必须事先明确参数是什么然后在传递下去(
通过柯里化解决此问题
)
function $(id) {
return document.getElementById(id);
}
function bind(context, fn) {
return function (...args) {
return fn.apply(context, args);
}
}
const bindFn = bind({
dom: $('btn1'), args: {
name: 'Lee' } }, function (e) {
console.log(this, e); // {dom: button#btn1, args: { name: 'Lee' }} PointerEvent{}
this.dom.removeEventListener('click', bindFn);
});
$('btn1').addEventListener('click', bindFn, false);
原生bind
function sayHi(name) {
console.log(`${
this.msg} ${
name}`);
}
let bindFn = sayHi.bind({
msg: 'Hello' }, 'Lee');
bindFn(); // Hello Lee
函数柯里化
函数柯里化的思想是
对函数的参数分割
,类似面向语言中的类的多态,根据传递的参数不同,可以让一个函数存在多种状态实现函数的柯里化是以函数为基础的,借助柯里化器伪造其他函数,让这些伪造的函数在执行时调用这个基函数完成不同的功能
通过函数柯里化器对add方法实现的多态拓展且不需要像以前那样明确声明函数了,因为函数的创建过程已经在函数柯里化器中完成了
// 创建柯里化器
function curry(fn, ...args1) {
return function (...args2) {
// 所有参数
let args = [...args1, ...args2];
return fn.apply(null, args);
}
}
curry((...args) => {
console.log(args); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
}, 1, 2, 3, 4, 5, 6)(7, 8, 9);
// 加法器
function add(a, b) {
return a + b;
}
let sum1 = curry(add, 1)(2);
console.log(sum1); // 3
let sum2 = curry(add, 1, 2)();
console.log(sum2); // 3
利用柯里化重构bind
优化传参
function $(id) {
return document.getElementById(id);
}
function bind(context, fn, ...args1) {
return function (...args2) {
// 所有参数
let args = [...args1, ...args2];
return fn.apply(context, args);
}
}
const bindFn1 = bind(null, (...args) => {
console.log(args); // [1, 2, 3, 4, 5, 6, 7, 8]
}, 1, 2, 3, 4, 5)
bindFn1(6, 7, 8);
const bindFn2 = bind($('btn1'), function (a, b, ...args) {
console.log(this, a, b, args); // DOM [1, 2, 3, 4, 5, 6, PointerEvent]
this.removeEventListener('click', bindFn2);
}, 1, 2, 3, 4, 5, 6);
$('btn1').addEventListener('click', bindFn2, false);
兼容bind
方法
// 兼容各个浏览器
if (Function.prototype.bind === undefined) {
Function.prototype.bind = function (context, ...args1) {
const that = this;
return function (...args2) {
// 所有参数
let args = [...args1, ...args2];
return that.apply(context, args);
}
}
}
const logNum = function (...args) {
console.log(args); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
};
logNum.bind(null, 1, 2, 3, 4, 5)(6, 7, 8, 9);
特点
参与者模式实质
上是两种技术
的结晶函数绑定
函数柯里化
反柯里化
方便对方法的调用
// 反柯里化
Function.prototype.uncurry = function () {
var that = this;
return function (...args) {
return Function.prototype.call.apply(that, args);
}
};
// 以前的方法
{
Object.prototype.toString.call(function () {
}); // '[object Function]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call(123); // '[object Number]'
}
// 反柯里化后的方法
{
const toString = Object.prototype.toString.uncurry();
toString(function () {
}); // '[object Function]'
toString([]); // '[object Array]'
toString(123); // '[object Number]'
}
var push = [].push.uncurry(); // 保存数组push方法
var obj = {
};
push(obj, 'Lee', 18); // 通过push方法为对象添加数据成员
console.log(obj); // {0: 'Lee', 1: 18, length: 2}
入场仪式-等待者模式
等待者模式(waiter)
通过对多个异步进程监听,来触发未来发生的动作。(类似于
Promise
Promise.all(...)
)
什么是等待者模式
等待者模式
或者说等待者对象
是用来解决那些不确定先后完成的异步逻辑的
比如:运动会的入场仪式,你不确定请哪只队伍先入场,但有一点你很确定,就是会议开始必须等到所有的队伍入场完毕。而这里的会议开始就相当于等待这模式的逻辑执行
Promise.all(...)
function fnA() {
let code = [0, 1][Math.floor(Math.random() * 2)];
return new Promise((resolve, reject) => {
setTimeout(function () {
if (code) resolve({
code, msg: 'success!', name: 'fnA' })
else reject({
code, msg: 'fail!', name: 'fnA' })
}, 1500);
});
}
function fnB() {
let code = 1;
return new Promise((resolve, reject) => {
setTimeout(function () {
if (code) resolve({
code, msg: 'success!', name: 'fnB' })
else reject({
code, msg: 'fail!', name: 'fnB' })
}, 1000);
});
}
Promise.all([fnA(), fnB()])
.then(res => {
/**
* [{ code: 1, msg: 'success!', name: 'fnA' },
* { code: 1, msg: 'success!', name: 'fnB' }]
*/
console.log(res);
})
.catch(err => {
// { code: 0, msg: 'fail!', name: 'fnA' }
console.log(err);
});
函数形参嵌套函数形参
/**
* 函数形参fn 嵌套 函数形参resolve、reject
* @param {Function} fn demo的函数行参
* @param {Function} resolve fn的函数形参1
* @param {Function} reject fn的函数形参2
*/
function demo(fn = (resolve, reject) => {
resolve(null); reject(null); }) {
fn(resolve, reject);
function resolve(...args) {
console.log('resolve', args); }
function reject(...args) {
console.log('reject', args); }
}
demo(); // resolve->[null] reject->[null]
demo((resolve, reject) => {
resolve(1, 2, 3);
reject(4, 5, 6);
}); // resolve->[1, 2, 3] reject->[4, 5, 6]
以上代码的变式1
等待者模式完成 40%
/**
* 函数形参fn 嵌套 函数形参resolve、reject
* @param {Function} fn Demo的函数行参
* @param {Function} resolve fn的函数形参1
* @param {Function} reject fn的函数形参2
*/
function Demo(fn = (resolve, reject) => {
resolve(null); reject(null); }) {
let value = [];
fn(resolve, reject);
function resolve(...args) {
value = [...value, ...args];
}
function reject(...args) {
value = [...value, ...args];
}
this.then = (callback) => {
callback(value);
}
}
new Demo((resolve, reject) => {
resolve(1, 2, 3);
reject(4, 5, 6);
}).then(res => console.log(res)); // (6) [1, 2, 3, 4, 5, 6]
new Demo().then(res => console.log(res)); // [null, null]
以上代码的变式2
等待者模式完成 80%
/**
* 函数形参fn 嵌套 函数形参resolve、reject
* @param {Function} fn Demo的函数行参
* @param {Function} resolve fn的函数形参1
* @param {Function} reject fn的函数形参2
*/
function Demo(fn) {
let state = null; // null->赋值操作 true->resolve、onFulfilled false->reject、onRejected
let value = [];
let deferred = {
};
fn(resolve, reject);
function resolve(...args) {
state = true;
value = args;
deferred.onFulfilled && deferred.onFulfilled(value);
}
function reject(...args) {
state = false;
value = args;
deferred.onRejected && deferred.onRejected(value);
}
this.then = (onFulfilled, onRejected) => {
if (state === null) {
deferred = {
onFulfilled, onRejected }
} else {
(state ? onFulfilled : onRejected)(value);
}
return this;
}
}
new Demo((resolve, reject) => {
setTimeout(function () {
resolve(1, 2, 3);
reject(4, 5, 6);
}, 2000);
}).then(res => {
console.log('res', res); // res->[1, 2, 3]
}, err => {
console.log('err', err); // err->[4, 5, 6]
});
自定义实现等待者模式(仿Promise.all
)
等待者模式完成 100%
/**
* 等待者模式
* @param {Function} fn (resolve, reject) => { resolve('ok'); reject('fail'); }
* @returns 实例化
*/
function Waiter(fn) {
// 兼容不使用new关键字
if (!(this instanceof Waiter)) return new Waiter(fn);
let state = null; // true 成功回调; false 失败回调; null 中立
let value = null; // 暂存`成功或失败回调的结果`,用于`后续调用.then`的`使用`
let deferred = {
}; // 存储.then(...)传入的参数 ---> { onFulfilled, onRejected }
fn(resolve, reject);
/**
* 接收成功失败回调
* @param {Function} onFulfilled 成功回调
* @param {Function} onRejected 失败回调
* @returns 当前实例
*/
this.then = function (onFulfilled, onRejected) {
if (state === null) {
deferred = {
onFulfilled, onRejected };
} else {
(state ? onFulfilled : onRejected)(value);
}
return this;
};
/**
* 成功回调
* @param {any} val 结果
*/
function resolve(val) {
if (state !== null) return;
state = true;
value = val;
deferred.onFulfilled && deferred.onFulfilled(value);
deferred = null;
}
/**
* 失败回调
* @param {any} val 结果
*/
function reject(val) {
if (state !== null) return;
state = false;
value = val;
deferred.onRejected && deferred.onRejected(value);
deferred = null;
}
}
/**
* 等待所传函数全部完成
* @param {...any} args 任意参数
* @returns Waiter实例
*/
Waiter.all = (...args) => {
// 如果参数只有一个并且是数组,那么使用这个数组;如果参数为多个,那么使用参数组合成的数组;
let arr = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
return new Waiter(function (resolve, reject) {
// 如果未传参,则返回空数组
if (arr.length === 0) return resolve([]);
// 还需处理的对象数量计数器
let remaining = arr.length;
// 处理传参保存结果函数
function result(w, i) {
if (w instanceof Waiter) {
w.then(function (res) {
result(res, i); }, reject);
return;
}
// 完成一个替换一个
arr[i] = w;
if (--remaining === 0) {
resolve(arr);
}
}
arr.forEach((w, i) => result(w, i));
});
};
const w1 = new Waiter((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve(1) : reject(0);
}, 2000);
}).then(res => console.log('res', res), err => console.log('err', err));
// 返回对象,成功或失败回调
function fnA() {
return new Waiter((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve({
name: 'A', msg: 'success' }) : reject({
name: 'A', msg: 'fail' });
}, 100);
});
}
// 返回对象,成功回调
function fnB() {
return new Waiter((resolve, reject) => {
setTimeout(() => {
resolve({
name: 'B', msg: 'success' });
}, 3000);
});
}
// 返回数字,成功回调
function fnC() {
return new Waiter((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
}
const w2 = Waiter.all([fnB(), fnA(), fnC(), 'abc', 456])
.then(res => console.log(JSON.stringify(res)), err => console.log(err));
等待者模式(ES6写法)
class Waiter {
#state = null; // true 成功回调; false 失败回调; null 中立
#value = null; // 暂存`成功或失败回调的结果`,用于`后续调用.then`的`使用`
#deferred = {
}; // 存储.then(...)传入的参数 ---> { onFulfilled, onRejected }
constructor(fn) {
// 兼容不使用new关键字
if (!(this instanceof Waiter)) return new Waiter(fn);
fn(this.#resolve, this.#reject);
}
/**
* 接收成功失败回调
* @param {Function} onFulfilled 成功回调
* @param {Function} onRejected 失败回调
* @returns 当前实例
*/
then = function (onFulfilled, onRejected) {
if (this.#state === null) {
this.#deferred = {
onFulfilled, onRejected };
} else {
(this.#state ? onFulfilled : onRejected)(this.#value);
}
return this;
};
/**
* 成功回调
* @param {any} val 结果
*/
#resolve = (val) => {
if (this.#state !== null) return;
this.#state = true;
this.#value = val;
this.#deferred.onFulfilled && this.#deferred.onFulfilled(this.#value);
this.#deferred = null;
};
/**
* 失败回调
* @param {any} val 结果
*/
#reject = (val) => {
if (this.#state !== null) return;
this.#state = false;
this.#value = val;
this.#deferred.onRejected && this.#deferred.onRejected(this.#value);
this.#deferred = null;
};
/**
* 等待所传函数全部完成
* @param {...any} args 任意参数
* @returns Waiter实例
*/
static all = (...args) => {
// 如果参数只有一个并且是数组,那么使用这个数组;如果参数为多个,那么使用参数组合成的数组;
let arr = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
return new Waiter(function (resolve, reject) {
// 如果未传参,则返回空数组
if (arr.length === 0) return resolve([]);
// 还需处理的对象数量计数器
let remaining = arr.length;
// 处理传参保存结果函数
function result(w, i) {
if (w instanceof Waiter) {
w.then(function (res) {
result(res, i); }, reject);
return;
}
// 完成一个替换一个
arr[i] = w;
if (--remaining === 0) {
resolve(arr);
}
}
arr.forEach((w, i) => result(w, i));
});
};
}
const w1 = new Waiter((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve({
msg: 'success' }) : reject({
msg: 'fail' });
}, 5000);
}).then(res => console.log('res', res), err => console.log('err', err));
// 返回对象,成功或失败回调
function fnA() {
return new Waiter((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve({
name: 'A', msg: 'success' }) : reject({
name: 'A', msg: 'fail' });
}, 100);
});
}
// 返回对象,成功回调
function fnB() {
return new Waiter((resolve, reject) => {
setTimeout(() => {
resolve({
name: 'B', msg: 'success' });
}, 3000);
});
}
// 返回数字,成功回调
function fnC() {
return new Waiter((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
}
const w2 = Waiter.all([fnB(), fnA(), fnC(), 'abc'])
.then(res => console.log(JSON.stringify(res)), err => console.log(err));