代码要写成别人看不懂的样子(十八)

本文参考《JavaScript设计模式》,探讨节流模式。作者因早期项目需求接触节流,它可对重复业务逻辑节流控制以提升性能。常见应用如在scroll事件添加动画,还介绍了图片延迟加载,包括相关原型方法、获取图片、加载图片、判断可视范围等逻辑及事件绑定。

本篇文章参考书籍《JavaScript设计模式》–张容铭

前言

  今年3月份的时候,写过几篇文章,,介绍了一下浏览器的回流,重绘,以及对应的解决办法分别是节流和防抖。当时刚开始写博客,还很青涩,各位要是不嫌弃,可以简单扫一眼,链接在下面。

  今天我们一块研究的设计模式,就跟节流有关,这个东西平时不常用,但是一旦遇到相关问题后,不用还不行。大家也看到了,关于节流的文章,算上正在写的这篇,一共有4篇博客了,为啥我会对这个出现频率并不高的词这么关注?这说起来还有点历史。

  想当年我还是个小萌新的时候,遇到一个需求,点击一个区域,调用一个接口,然后把查询的数据更新到页面上。这个普通不能再普通的需求,开启了我新世界的大门。

  正常来讲上面的需求没什么特别的,但是嘞,转测试之后,测试同事在疯狂的点击那个请求区域,导致请求快速送,虽然是同一个请求,但是返回时间有快有慢,这就有可能导致,先发送的请求后返回过来,那数据显示就存在问题了。

  对于当时的我,虽然有问题,但是还在可控范围之内,我给它加个蒙层不就行了,上一个接口不返回信息,就不能调用下一次。

  这样改问题是没了,但是交互体验不太好,而且后面增加了一个双击功能,这个方案就彻底歇菜了。有没有什么方法既能满足测试,又能满足需求呢?研究了好久,终于让我找到了节流。

节流模式

  对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能

  平时业务中比较常见的节流情况,就是在 scroll 事件里面添加动画了,比如我们浏览一面时旁边会有一个回滚到顶部的按钮,由于 scroll 事件会触发多次,那添加的动画也就会被执行多次。为了解决这一个问题,我们先要实现一个节流器。

//节流器
var throttle = function() {
	//获取第一个参数
	var isClear = arguments[0], fn;
	//如果第一个参数是 boolean 类型,那么第一个参数表示是否清除计时器
	if(typeof isClear === 'boolean') {
		//第二个参数则为函数
		fn = arguments[1];
		//函数的计时器句柄存在,清除计时器
		fn.__throttleID && clearTimeout(fn.__throttleID);
	//通过计时器延迟函数的执行
	} else {
		//第一个参数为函数
		fn = isClear;
		//第二个参数为函数执行时的参数
		param = arguments[1];
		//对执行时的参数适配默认值,这里用以前学习的 extend 方法
		var p = extend({
			context: null,  //执行函数执行时的作用域
			args: [],       //执行函数执行时的相关参数
			time: 300       //执行函数延迟执行的时间
		}, param);
		//清除执行函数计时器句柄
		arguments.callee(true, fn);
		//为函数绑定计时器句柄,延迟执行函数
		fn.__throttleID = setTimeout(function() {
			//执行函数
			fn.apply(p.context, p.args)
		}, p.time)
	}
}

  对于返回顶部按钮,我们可以进行如下优化:

//首先引入jquery.js 与 easing.js 方便返回顶部动画实现
//返回顶部按钮动画
function moveScroll() {
	var top = $(document).scrollTop();
	$('#back').animate({top: top + 300}, 400, 'easeOutCubic')
}
//监听页面滚动事件
$(window).on('scroll', function() {
	//节流执行返回顶部按钮动画
	throttle(moveScroll);
})

图片延迟加载

  节流模式还有一个常见的重要应用,就是图片的延迟加载,我们在网上浏览图片的时候,都是加载当前页的图片,当往下滚动的时候,再加载新的图片,还有就是当用户把滑动条直接拉到底部的时候,由于上面的图片会优先加载,造成底部图片加载推迟,这种体验也不是很好,此时我们就可以使用节流模式来处理这些逻辑,使可视范围内的图片优先加载。

/**
* 截留延迟加载图片类
* param id 延迟加载图片的容器 id
* 注:图片格式如下<img src="img/loading.gif" alt="" data-src="img/i.jpg">
*/
function LazyLoad(id) {
	//获取需要延迟加载图片的容器
	this.container = document.getElementById(id);
	//缓存图片
	this.imgs = this.getImgs();
	//执行逻辑
	this.init();
}
//节流延迟加载图片类原型方法
LazyLoad.prototype = {
	//起始执行逻辑
	init: function() {},
	//获取延迟加载图片
	getImgs: function() {},
	//加载图片
	update: function() {},
	//判断图片是否在可视范围内
	shouldShow: function() {},
	//获取元素在页面中的纵坐标位置
	pageY: function(element) {},
	//绑定事件(简化版)
	on: function(element, type, fn) {},
	//为窗口绑定 resize 事件与 scroll 事件
	bindEvent: function() {}
}

  对于节流延迟加载图片类的原型方法 init 应该做两件事,初始化图片加载(即执行 update 方法)和为窗口绑定事件。

//起始执行逻辑
init: function() {
	//加载当前视图图片
	this.update();
	//绑定事件
	this.bindEvent();
}

  对于获取容器内图片的 getImages 方法需要注意,为了方便操作获取的图片元素集合(类数组),需要将其转化成数组,由于在 IE中对获取到的元素集合直接执行数组方法 slice 会报错,故通过遍历每一个元素,并将其加入新数组中返回,来显性创建数组。

//获取延迟加载图片
getImage: function() {
	//新数组容器
	var arr = [];
	//获取图片
	var imgs = this.container.getElementsByTagName('img');
	//将获取的图片转化为数组(IE下通过 Array.prototype.slice会报错)
	for(var i = 0, len = imgs.length; i < len; i++) {
		arr.push(imgs[i])
	}
	return arr;
}

  对于加载图片方法 update,需要遍历每一个图片元素,如果处在可视区域内则加载并将其在图片缓存中清除。

//加载图片
update: function() {
	//如果图片都加载完成 返回
	if(!this.imgs.length) {
		return;
	}
	//获取图片长度
	var i = this.imgs.length;
	//遍历图片
	for(--i; i >= 0; i--) {
		//如果图片在可视范围内
		if(this.shouldShow(i)) {
			//加载图片
			this.imgs[i].src = this.imgs[i].getAttribute('data-src');
			//清除缓存中的此图片
			this.imgs.splice(i, 1);
		}
	}
}

  对于判断图片是否在可视范围内的方法,是判断图片的上下边位置是否符合下列条件(由于页面左边关系,对于 y 坐标,由上至下依次增大,对于 x 坐标,由左至右依次增大):图片底部高度大于可视视图顶部高度并且图片底部高度小于可视视图底部高度。

//判断图片是否在可视范围内
shouldShow: function(i) {
	//获取当前图片
	var img = this.imgs[i],
		//可视范围内顶部高度(页面滚动条 top 值)
		scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
		//可视范围内底部高度
		scrollBottom = scrollTop + document.documentElement.clientHeight,
		//图片的顶部位置
		imgTop = this.pageY(img),
		//图片的底部位置
		imgBottom = imgTop + img.offsetHeight;
	//判断图片是否在可视范围内,图片底部高度大于可视视图顶部高度并且图片底部高度
	//小于可视视图底部高度,或者图片顶部高度大于可视视图顶部高度并且图片顶部高度
	//小于可视视图底部高度
	if(imgBottom > scrollTop && imgBottom < scrollBottom || (imgTop > scrollTop && imgTop < scrollBottom)) return true;
	//不满足上面条件则返回 false
	return false;
}

  对于获取图片元素纵坐标方法 pageY 这是通过元素一级一级遍历其父元素,并累加每一级元素 offsetTop 值获取的。

//获取元素页面中的纵坐标位置
pageY: function() {
	//如果元素有父元素
	if(element.offsetParent) {
		//返回元素 + 父元素高度
		return element.offsetTop + this.pageY(element.offsetParent);
	} else {
		//否则返回元素高度
		return element.offsetTop;
	}
}

  为了简化代码,我们简单实现一下 on 绑定事件方法。

//绑定事件
on: function(element, type, fn) {
	if(element.addEventListener) {
		addEventListener(type, fn, false);
	} else {
		element.attachEvent('on', type, fn, false);
	}
}

  最后一个方法,绑定事件则是对页面的 scroll resize 事件的监听,为检测每一次交互中事件的最后一次执行,故需要对事件的回调函数做节流处理。

//为窗口绑定 resize 事件与 scroll 事件
bindEvent: function() {
	var that = this;
	this.on(window, 'resize', function() {
		//节流处理更新图片逻辑
		throttle(that.update, {context: that});
	});
	this.on(window, 'scroll', function() {
		//节流处理更新图片逻辑
		throttle(that.update, {context: that});
	})
}

  大功告成,接下来我们只需要实例化类就可以了。

//延迟加载 container 容器内的图片
new LazyLoad('container');

  




完全没关系!你说“**我全都看不懂**”,那我们就从最最基础的开始,像教一个刚学编程的人一样,**一句一句、一步一步地解释这段代码到底在做什么**。 我们不跳步、不假设你知道任何图形学或数学知识。准备好,我们现在开始: --- ## 🌟 目标:画一个“圆柱体”(比如像一根水管、一个罐头) 我们要用 OpenGL(一种图形库)在屏幕上画出这个圆柱体。 但计算机不能直接理解“圆柱”这种东西,它只能画: - 点 - 线 - 三角形 所以我们必须把“圆柱”拆成很多小三角形拼起来 —— 就像乐高积木搭模型。 --- ## 🔧 第一步:准备材料(定义参数) ```cpp void Cylinder::initData(DataParam *param) { dataParam = *param; ``` ### ✅ 这句什么意思? - `DataParam *param` 是一个“装数据的盒子”的指针。 - 它里面可能有这些信息: ```cpp struct DataParam { float radius; // 圆柱半径(粗细) GLuint longSegments; // 把圆周切成几段(越多样子越圆) float height; // 高度(多长) }; ``` - `dataParam = *param;` 表示:“把这个盒子的内容复制到我自己的成员变量里保存下来。” 👉 所以现在我们知道要画一个多粗、多高、多精细的圆柱了。 --- ### 继续往下: ```cpp GLfloat radius = dataParam.radius; GLuint longSegments = dataParam.longSegments; GLfloat height = dataParam.height; ``` ✅ 把刚才复制过来的数据取出来,起个短名字方便后面用。 比如: - `radius` 就是半径(比如 1.0) - `height` 是高度(比如 2.0) - `longSegments` 是“把圆切几块”(比如 8 块 → 八边形) --- ## 📐 第二步:计算角度(怎么画一个圆?) ```cpp float fTheta = (glm::pi<float>() * 2.0f) / longSegments; ``` ### ❓这是什么? 我们来慢慢讲。 ### 💡 想象你在一个操场上,想绕着中心走一圈,走出一个“圆形”。 你可以: 1. 每次走一小步 2. 走完后转一个小角度 3. 再走下一步…… 这样走很多次,就画出了一个近似的圆。 而每次转的角度是多少呢? > 一圈总共是 **360 度**,也就是 **2π 弧度**(程序员喜欢用弧度) 如果你把圆分成 `longSegments = 8` 段,那每段就是: $$ \frac{2\pi}{8} = \frac{\pi}{4} $$ 👉 所以这行代码的意思是: > “我要把一个完整的圆($2\pi$)平均分成 `longSegments` 份,每一份的角度差是 `fTheta`” 🎯 `fTheta` 就是你每画一个点时增加的角度。 --- ## 🧮 第三步:需要多少个顶点?(提前申请内存) ```cpp int numVertices = 2 * (longSegments + 1) + (longSegments + 2) + (longSegments + 2); ``` 这句话看起来复杂,其实就是在数:**一共要用多少个“点”来组成这个圆柱?** 我们把它拆开来看! ### 🔹 第一部分:侧面(筒身)→ `2 * (longSegments + 1)` - 我们要在上下两个圈上各放一堆点: - 下面一圈:`longSegments + 1` 个点(因为首尾重合,所以比段数多1) - 上面一圈:同样 `longSegments + 1` 个点 - 所以侧面一共需要:`2 * (L+1)` 个点 📌 举个例子:如果 `longSegments = 8`,那么每圈9个点,共 `2×9=18` 个点 --- ### 🔹 第二部分:上底盖(顶上的圆)→ `longSegments + 2` - 画一个圆盖,要用 `GL_TRIANGLE_FAN`(扇形模式),需要: - 1 个中心点 - `longSegments + 1` 个边缘点(闭合) - 所以总共:`1 + (L+1) = L+2` 个点 --- ### 🔹 第三部分:下底盖(底下的圆)→ 又一个 `longSegments + 2` - 同理,下面也要画一个圆盖,也需要 `L+2` 个点 --- ### ✅ 加起来总数: ``` 侧面: 2*(L+1) 上盖: L+2 下盖: L+2 总点数 = 2L+2 + L+2 + L+2 = 4L + 6 ``` 代入 `L=8` → `4×8 + 6 = 38` 个点 所以我们要准备一个能装 **38 个点** 的数组。 --- ## 🗃️ 第四步:创建一个“点”的数组 ```cpp if (vertices) { delete[] vertices; } vertices = new TextureColorVertex[numVertices]; ``` ### ✅ 解释: - `vertices` 是一个数组,用来存所有的“顶点” - 每个顶点包含: - 坐标(x, y, z) - 颜色(r, g, b) - 纹理坐标(s, t) 👉 就像 Excel 表格的一行,记录一个点的所有信息。 #### 为什么先 `delete[]`? - 如果之前已经画过一次圆柱,`vertices` 已经分配过内存了 - 再次初始化前必须先释放旧内存,否则会**内存泄漏** #### `new TextureColorVertex[38]` 是什么? - 在电脑内存中开辟一块空间,可以放 38 个这样的“点” - 就像租了一个有 38 个格子的柜子,每个格子放一个点的信息 --- ## 🌀 第五步:生成侧面的点(最关键的一步) ```cpp for (int i = 0; i < (longSegments + 1); i++) { ``` 👉 循环 `L+1` 次(比如 9 次),每次生成一对点:一个在底下,一个在顶上。 --- ### 🟢 第一个点:底部的点 ```cpp vertices[2 * i].coordinate.x = radius * cosf(i * fTheta); vertices[2 * i].coordinate.y = -(height / 2.0f); vertices[2 * i].coordinate.z = radius * sinf(i * fTheta); ``` 我们来理解这三行。 #### 数学知识:如何用角度算出 x 和 z? 想象你在画一个圆: - 角度为 θ - 半径为 r - 那么: - $ x = r \cdot \cos(\theta) $ - $ z = r \cdot \sin(\theta) $ 这里的 `i * fTheta` 就是当前的角度。 例如: - i=0 → 角度=0 → x=r, z=0 → 正右方 - i=1 → 角度=π/4 → 斜上方 - ... - i=8 → 角度=2π → 回到起点 🎯 所以这一系列点就在 XY 平面上绕了一圈(其实是 XZ 平面,Y 是上下) Y 固定为 `-height / 2.0f` → 所有底边点都在“最下面” --- ### 🔵 第二个点:顶部的点 ```cpp vertices[2 * i + 1].coordinate.x = radius * cosf(i * fTheta); vertices[2 * i + 1].coordinate.y = (height / 2.0f); vertices[2 * i + 1].coordinate.z = radius * sinf(i * fTheta); ``` 和上面几乎一样,只是 Y 是 `+height/2` → 所有点都在“最上面” --- ### 📦 存储位置是怎么安排的? | i | 底部点索引 | 顶部点索引 | |---|------------|------------| | 0 | 0 | 1 | | 1 | 2 | 3 | | 2 | 4 | 5 | | ... | ... | ... | 所以整个侧面变成了这样一排点: ``` [底0][顶0][底1][顶1][底2][顶2]... ``` OpenGL 用 `GL_TRIANGLE_STRIP` 把它们连成带状三角形,形成筒壁。 --- ### 🎨 设置纹理坐标(简单理解:贴图位置) ```cpp vertices[2 * i].texture.s = (longSegments - i) * (1.0f / longSegments); vertices[2 * i].texture.t = 0; ``` - `s` 和 `t` 类似于图片上的横纵坐标(0~1之间) - 这里 `s` 是 `(L - i)/L` → 随着 i 增大而减小 → **从右往左贴图** - `t=0` → 贴图最下面 - 顶点设 `t=1` → 贴图最上面 ⚠️ 缺点:纹理是倒着贴的!正常应该用 `i * (1.0/L)` --- ### 🖍️ 设置颜色 ```cpp vertices[2 * i].color.r = ... = 1.0f; ``` - RGB 都设为 1 → 白色 --- ## 🛑 到这里为止:完成了侧面 18 个点 接下来要“上底盖”和“下底盖” 所以我们需要知道:**下一个该哪个位置?** --- ## 📍 关键行来了! ```cpp int start = 2 * (longSegments + 1); ``` 👉 这句话的意思是: > “我已经了 `2*(L+1)` 个点(侧面),所以下一个可用的位置是第 `2*(L+1)` 号。” 比如 `L=8` → `2*9 = 18` → 所以下一个点从 `vertices[18]` 开始 🎯 完全就是一个“记账”操作:前面用了多少格子?现在该从哪开始? --- ## ✅ 总结:你现在明白了吗? | 代码 | 实际含义 | |------|----------| | `fTheta = 2π / L` | 每一步转多少角度 | | `numVertices = ...` | 总共要多少个点 | | `new TextureColorVertex[N]` | 开辟内存存点 | | `for(...)` | 一个个生成点 | | `x = r*cosθ`, `z = r*sinθ` | 用三角函数画圆 | | `start = 2*(L+1)` | 记录“接下来从哪个位置开始” | --- ## 🧩 最后一句话类比: 想象你要做一盘饺子: 1. 先看要做几个(`longSegments`) 2. 准备面粉和馅(`new` 内存) 3. 擀皮:一个个切好(生成侧面点) 4. 包完一圈后,开始做盖子(上底、下底) 5. `start` 就像是你对老婆说:“前面18个饺子包完了,现在该包盖子了,从第19个位置摆起。” ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值