这是我参与「第四届青训营 」笔记创作活动的第二天
写好js的三个原则
各司其责:让HTML、CSS和js职能分离
组件封装:好的IU组件具备正确性、扩展性、复用性
过程抽象:应用函数式编程思想(不仅是对数据抽象)
关于函数式编程:非常重要的编程范式
各司其责
控制样式的代码可以用纯css实现。
改进一
版本二操控body.className,一般用class定义html元素的状态。
而版本一的body-style直接操控css,用js控制css,违背各司其责。
改进二
控制样式的代码可以用纯css实现。
css伪类选择器:可以修改、匹配元素状态。
首先,通过伪类选择器,实现点击CheckBox修改content状态。
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
这里通过给CheckBox设置一个checked状态,通过checked状态修改content样式。(修改CheckBox状态,后面用兄弟节点选择器匹配到下面的内容。)
#modeCheckBox {
display: none;
}
上面这个地方,把CheckBox隐藏起来。
然后,把CheckBox与label关联起来,实现点击图标也有相同效果。
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
这里,给label设置for属性值,与CheckBox的id相同。
于是点击label也会改变CheckBox状态,根据CheckBox兄弟节点改变content状态。
总结
- HTML/CSS/JS 各司其责(结构/表现/行为)
- 应当避免不必要的由 JS 直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 JS 方案
组件封装
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性(组件设计的原则)。
例:轮播图
CSS
? 轮播图切换的状态使用修饰符(modifier)——这是哪里体现的?--selected吗?了解BEM的CSS命名规范(搜索 使用修饰符(modifier))
。 “连接修饰符则使用‘-’,.block - modifier代表.block的不同状态或不同版本。
JS
JS里是在Slider类里,创建一个构造函数并实现五个api功能
下面代码用定时调用的函数,实现两秒切换下一张图片
setInterval(() => {
slider.slideNext();
}, 2000);
通过控制流添加下方可以控制的点。
对应第几个图片那么第几个小圆点呈红色,从这可以看出小圆点有状态,且与图片状态绑定。通过自定义事件进行状态绑定。
轮播图-2增添的代码(除了html的):
在构造器中,定义controller为那四个小圆点,在controller里监听事件mouseover和mouseout;监听slide事件(这是自定义事件),在slide事件里拿到当前图片index,设置对应小圆点为红色状态。定义previous和next,左滑右滑标签因为是无状态的,所以不需要注册、监听slide事件。
? evt.preventDefault();这里为什么要加上阻止浏览器默认事件?
。 通知浏览器不执行与事件(比如click)关联的默认动作(例如,当点击提交按钮时阻止对表单的提交)
补充:preventDefault是用来阻止浏览器的默认事件,stopPropagation是用来阻止冒泡事件(父子元素之间)。
const变let
? const与let的区别?
在slideTo函数中,增加开发自定义事件的过程,在items设置好后new一个customEvent,为slide事件,把事件派发出去,这样可以在controller里监听slide事件。
增加start和stop事件,在start放入之前写的setInterval定时调用函数,this.cycle调用的是构造器传入的cycle参数(this.cycle = cycle;)。
最后slider.start();运行。
小结:实现组件的基本步骤
-
结构设计html
-
展现效果css
-
行为设计js
- API(功能)设计一些接口来操作
- Event(控制流)通常情况下用自定义事件解耦
这样做可以实现一个基础的组件
但是如何重构这个轮播图组件呢?(目的:解耦)
重构-插件化
-
将控制元素抽取成插件
-
插件与组件之间通过 **依赖注入 **方式建立联系
(轮播图-3)
前面的:控制点和组件绑定在一起,要改则html、css、js都要改,牵一发则动全身。所以这里把组件的插件抽象出来。
重构前,构造函数(constructor)有四十几行代码,不是很好的实现。
重构后,构造函数只剩三行代码,后面增加插件的api:
注册插件registerPlugins、增加事件监听addEventListener
构造函数内其他的没变化, slideTo依旧派发事件。
插件抽象出pluginController、pluginPrevious、pluginNext三个函数,把注册事件的代码放到里面(这三个函数)去,通过依赖注入的方式把slider组件传到plugin构造方法里,即在构造函数api:registerPlugins内,plugins.forEach(plugin => plugin(this));在这个this里注入入进来 。这样,插件可以控制slider,而slider不关心有哪些插件,这些插件都是通过slider.registerPlugins注册进来的。
总结:这样做两个优点——1.构造函数精简 2.某个插件功能不用时,只需要把注册插件那里注释掉(slider.registerPlugins(/*pluginController,*/ pluginPrevious, pluginNext);这样插件功能取消但模块还在),然后把html对应代码删掉就好。 3.新增插件也方便
<button id="randomGet">手气不错</button>
function pluginRandom(Slider) {
randomGet.addEventListener('click', () => {
const idx = Math.floor(slider.items.length * Math.random());
// items长度×随机数,然后向下取整
slider.stop();
slider.slideTo(idx);
slider.start();
})
}
重构-模板化
- 将HTML模板化,更易于扩展
js的ui组件首先要做到数据驱动,根据数据生成html模板,不需要把图片写死到模板中去。
(轮播图-4)
要将组件和插件模板化。通过Slider的render生成代码,图片通过最后创建Slider对象时传入,代码生成通过this.container.innerHTML = this.render();调用render方法返回innerHTML并插入到容器中去。
在注册插件这里也是用的插件pluginController的render方法,传入images图像并根据传入图像决定对应的模板,把插件的模板插入到插件容器中,然后插入组件容器中。然后调用插件的action去完成插件自己的初始化。
pluginController里有两个方法:render和action。render的渲染下方小圆点。action完成初始化。
重构-抽象化
- 将组件通用模型抽象出来
将Slider这个特定的组件模型抽象成通用的组件。通用组件里有两个抽象的方法,注册插件和渲染(抽象方法)。然后Slider就可以继承Component, SliderPlugin也可以完成渲染(模板化的内容)。
形成一个抽象通用得到模型(组件框架),支持定义一个组件,在组件里注册若干控制插件。
new Slider这里传入id,css名字前缀,data。
这种方法不好的地方:
1.没有考虑组件嵌套,因为component和plugin分开。更通用的办法是component和plugin组合起来,可以有子组件,子组件也可以作为父组件来使用。
总结
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构 \1. 插件化 \1. 模板化 \1. 抽象化(组件框架)
过程抽象
-
用来处理局部细节控制的一些方法
-
函数式编程思想的基础应用
无副作用的输入和输出:纯函数(不改变外界环境的函数)才有。
组件框架和MVVM框架普遍应用过程抽象,比如:react hooks (典型抽象应用,过程抽象之后,可以通过hooks去存储状态,这个状态在函数调用的scope之外,我们存储和同步的一些状态 )
例子:学习列表-Once-
让事件只能触发一次
法一:加入once()
buttons.forEach((button) => {
button.addEventListener('click', once((evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}));
});
法二: {once: true}
buttons.forEach((button) => {
button.addEventListener('click', (evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}, {once: true});
});
这个是DOM事件里提供的。
向服务器get请求数据,这个数据希望只是被请求一次,要是发送多个请求,只会返回第一次请求,其他请求block掉。类似这种请求(多次调用只被允许执行一次),可以把整个过程封装成一个once的过程抽象(高阶函数)(法一),保证里面的函数只被执行一次。
高阶函数
一个函数返回另一个函数
function once(fn) {
// outer scope closure
return function(...args) {
// inner scope
if(fn) {
const ret = fn.apply(this, args);
fn = null; // 把函数值设为null,防二次调用
return ret;
}
}
}
调用foo()实际上调用的是里面的函数,因为第一次fn设为null了,就永远不会第二次调用到里面的方法。
经典的过程抽象:抽象了一个once函数,它的参数是一个函数,能把里面的任意函数变成只调用一次。
过程抽象对应数据抽象
数据抽象针对实体,过程抽象针对动作(复用某个动作函数即过程抽象,可以用高阶函数实现)。
HOF
HOF0为等价高阶函数,HOF0(fn)与fn函数完全等价。一般的高阶函数都是在HOF0的基础上做一些事情,比如改变某个函数的参数、返回值等。
function HOF0(fn) {
return function(...args) {
return fn.apply(this, args);
}
}
常用的高阶函数:
包括前面Once
Throttle节流函数(类库Underscore、Lodash)
防止事件触发频率过高(mousemove、scoll)。
timer = setTimeout(() => {
timer = null;
}, time)
在500ms后再把timer设为null
Debounce防抖函数
延伸:自动保存功能
Consumer
把同步调用的函数变成异步的。
2-延时调用的实现,800ms调用一次
Iterative可迭代方法
jQuery框架设计原则:批量操作(调用一个api,同时操作多个元素)
同样这里,实现批量操作。
判断subject是否是可迭代(比如list),是的话就用subject里面的每一个元素去调用fn方法,不是的话就只调用一次。
为什么用高阶函数
减少使用非纯函数的可能性
纯函数:没有副作用,结果可预期。纯函数的好处:不需要构建context就可以知道它结果是否符合预期
纯函数例子:
function add(x, y){ return x + y; } // test console.assert(add(4, 5) === 9, 'failed');
非纯函数:有副作用 function with side effect,会改变scope外面的值(外部状态),需要构建特定的环境。维护难度大。
let idx = 0; function count() { return ++idx; } // test const result = count(); // setup() 或 init() 前面环境的初始化 console.assert(result === 3, failed ${result}); // teardown() 把环境销毁掉
function setColor(el, color){ el.style.color = color; }
function setColors(els, color){ els.forEach((el) => { setColor(el, color); }) } setColors([...document.querySelecorAll('li:nth-child(2n+1)')], 'green');
测试时需要给两个非纯函数写testcase,所以用Iterative高阶函数的好处是把非纯函数变为纯函数(记得前面加上是否可迭代的判断)(setColors是一个表达式,不是函数,它调用高阶函数)
function setColor(el, color){
el.style.color = color;
}
const isIterable = obj => obj != null
&& typeof obj[Symbol.iterator] === 'function';
function iterative(fn) {
return function(subject, ...rest) {
if(isIterable(subject)) {
const ret = [];
for(let obj of subject) {
ret.push(fn.apply(this, [obj, ...rest]));
}
return ret;
}
return fn.apply(this, [subject, ...rest]);
}
}
const setColors = iterative(setColor)
setColors([...document.querySelecorAll('li:nth-child(2n+1)')], 'green');
测试:测试iterative函数(可迭代方法,批量操作)是否符合预期
const addMany = iterative((a, b) => a + b);
addMany([1, 2, 3, 4], 5) // 每个数加5,得6,7,8,9
为什么Iterative是纯函数:输入输出是确定的,运行逻辑只由传给它的函数决定。
编程范式
上面高阶函数的使用是一种模式。
编程范式分为两类:命令式(面向过程 / 面向对象),声明式(逻辑式 / 函数式)
声明式的代码更整洁
js数组迭代方法有map、forEach,通过这个实现声明式的代码。
命令式趋向于怎么做,声明式趋向于做什么
例子:1-命令式(和深夜食堂例子是一样的)
2-声明式(首先做一个过程抽象,定义一个toggle的高阶函数,里面接收一系列的actions,把第一个action取出来,push到列表最后一个,然后调用当前的action),方便添加逻辑分支(更强的可拓展性->3-三态)
总结:
-
过程抽象 / HOF / 函数装饰器
-
命令式 / 声明式
写代码最应该关注什么?
风格?效率?约定?使用场景?设计?
案例:当年的Leftpad事件
(npm模块,作用:补齐字符串长度,比如将所有数字变为5位,前面补0;参数:str字符串,len长度,ch用什么字符补齐)
优化后用repeat()代替while (),二次幂快速方法而不用循环线性地加。O(n)变O(log(n))
repeat() 类似原理:把n转为二进制数,从n的末位 依次判断n每一位的值。n>1是判断前面是否还有一位,n>>=1;是把n右移一位。(前面的依赖是最终变成string prototype方法)
module.exports = function repeat(string, count) {
var n = count;
// Account for out-of-bounds indices
if (n < 0 || n == Infinity) {
throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
}
var result = '';
while (n) {
if (n % 2 == 1) {
result += string; // * // - // *****
}
if (n > 1) {
string += string; // ** // **** // -
}
n >>= 1;
}
return result;
};
console.log(repeat('*', 5)) // 只需要循环3次
优化空间:字符串加法
改进二 是先计算出总共需要循环的次数,再循环,最后通过substring取出
注意性能
优化意义不大,考虑可读性更佳。
可读性、可拓展性、封装....
案例一:交通灯状态切换
普通
setTimeout由于异步,所以嵌套5层
数据抽象(封装)
把交通灯状态和时间等数据抽象出来,定义一个状态列表。
定义一个状态切换方法(传入html元素、状态列表),里面递归调用applyState方法。
过程抽象
定义了几个函数
wait等待
poll轮询(抽象出来),使代码更有灵活性和可拓展性
......
然后异步执行。
但是过度抽象。
异步+函数式
最佳
案例二:洗牌
错误
用Math.random决定这两位是否交换位置。
把每个位置的值相加会发现并不均等
这是因为sort随机交换不是两两位置均衡交换
正确
随机抽一张牌放最后一位,然后剩余牌随机抽放到倒数第二位...
使用生成器
抽奖,没有必要把所有牌洗完的情况下用。
yield c[i - 1];让每抽一次就有一次返回结果
console.log([...result]);全部打印
console.log(result.next().value);打印一次
console.log(result.next().value,result.next().value);
案例三:分红包
切西瓜法
细节:每人能分到最少数额0.01,否则提示钱不够分
每次切最大的那块。
O(mn)
抽牌法
把红包看成一个数列(0-9999),随机插入分隔符。
抽10份则随机抽出9个值,排序...插入值相减
O(n)
但空间复杂度比较高
最后
打好数学与算法基础
\