目录
一.同样一段代码
1.1.震撼和感受
同样一段代码,同样是实现业务的函数片段,但是编写的方式不一样,差别很大吗?这一点,自己编写的时候,感触不会很清晰.但是对比通用性很强的库函数或者是框架源码中强调高精简性,高可复用性和维护性中,感触会很深刻.不亚于震撼的感觉.
1.2 参照物
单从编写业务代码的习惯来看,自己去编写,往往是按照自己最熟悉的逻辑编写的.自己编写的方式,往往是最直接的逻辑.那么怎样的代码是质量更高的代码,需要改变,就需要参照物.需要改变思考的逻辑方式和严谨度,就要改变相应的思考模式和方向.从业务的广度和复杂度来说,无疑这些被广泛使用的库函数和框架中的代码编写方式所代表的恰恰极其贴合代码艺术和编写方式的成果.
1.3 更好的看懂
(1) 这些库函数和框架的编写,因为高可用性和考虑的覆盖面的广泛.所以编写习惯和自己习惯的方式,很有大的不同.强行阅读相关的源码,会比较费劲.
(2)为了阅读过程更加流畅.先拆解函数式编程特性的场景实现,到几个公认的代码设计基本原则的实现方式和场景分析,最后到拆解熟悉的框架和库函数中如何运用这些代码设计的基本原则和设计模式的.
(3)在这个过程中,整理和编写属于自己的库函数.
1.4 为了代码编写风格的形成
(1)在库函数的形成后,将意味着有了相应的可复用的代码风格.等同于通用函数库和框架的雏形了.这意味着很大的一步跨越.意味着代码不再是用于编写业务逻辑,而是可以有着更大的价值了.重复审视对应的代码,也可以明显的感知到代码风格的变化.
(2)能够较为流程的阅读相应的库函数和框架代码片段,意味着绝大多数的开源项目,大部分都可以看懂. 看懂是加入的基础,意味着有加入到开源项目的初始条件.
(3)加入到开源项目的完善和编写,意味着可以进入到更加广阔的世界交流.五湖四海,无国界的用代码交流想法和思考.
二.代码模块的封装性,正确性,可行性,通用性思考
2.1 封装性
在深入思考Javascript的过程中,对封装的描述.分为了两个部分.分别是对象的封装和过程的封装.单从封装的角度来说,对象和function都包含了对对象的封装和过程的封装.但是同样的封装,差别很大.好的封装,可以不断的复用,叠加,且没有副作用.
2.2 场景模拟
2.2.1 需求描述
需求:在一个界面中,存在三种状态.三种状态对应着不同的样式.通过js编写,实现相应的需求.
const traffic = document.querySelector('.traffic');//获取相应节点
function loop() { //切换对应的样式
traffic.className = 'traffic pass';
setTimeout(() => {
traffic.className = 'traffic wait';
setTimeout(() => {
traffic.className = 'traffic stop';
setTimeout(loop, 3500);
}, 1500);
}, 5000);
}
loop();
就这样实现了相应的需求.但是节点样式再次发生变化,或者相应的状态循环的时间发生变化.都需要修改对应的代码
为了避免这种情况
const traffic = document.querySelector('.traffic');
function signalLoop(subject, signals = []) {
const signalCount = signals.length;
function updateState(i) {
const {signal, duration} = signals[i % signalCount];
subject.className = signal;
setTimeout(updateState.bind(null, i + 1), duration);
}
updateState(0);
}
// 数据抽象
const signals = [
{signal: 'traffic pass', duration: 5000},
{signal: 'traffic wait', duration: 3500},
{signal: 'traffic stop', duration: 1500},
];
signalLoop(traffic, signals);
该函数会修改影响到外部状态节点,即存在函数副作用.为了处理该副作用,将存在副作用的函数提取出来
const traffic = document.querySelector('.traffic');
function signalLoop(subject, signals = [], onSignal) {
const signalCount = signals.length;
function updateState(i) {
const {signal, duration} = signals[i % signalCount];
onSignal(subject, signal);
setTimeout(updateState.bind(null, i + 1), duration);
}
updateState(0);
}
const signals = [
{signal: 'pass', duration: 5000},
{signal: 'wait', duration: 3500},
{signal: 'stop', duration: 1500},
];
signalLoop(traffic, signals, (subject, signal) => {
subject.className = `traffic ${signal}`;
});
从可读性的角度来说,setTimeout,增加了代码的嵌套层数
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const traffic = document.querySelector('.traffic');
async signalLoop(subject, signals = [], onSignal) {
const signalCount = signals.length;
function updateState(i) {
const {signal, duration} = signals[i % signalCount];
onSignal(subject, signal);
await wait(duration)
updateState.bind(null, i + 1)
}
updateState(0);
}
const signals = [
{signal: 'pass', duration: 5000},
{signal: 'wait', duration: 3500},
{signal: 'stop', duration: 1500},
];
signalLoop(traffic, signals, (subject, signal) => {
subject.className = `traffic ${signal}`;
});
2.3 相关场景过程总结
(1)数据封装:将数据的结构变换为可供过程函数处理的结构.
(2)减少函数的副作用,真正的纯函数不存在副作用.而纯函数有利于函数通用性的实现
三.装饰器模式
3.1 场景分析与函数式编程
实现函数之间的相互组合调用,从而实现函数的复用.也是react高阶组件和js高级函数的关键,关于函数是一等公民的函数式编程思想,在http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html阮一峰的博客中描述的比较清晰.其中有几个感觉比较重要的概念和思考
(1)范畴论和函子: 函子可以理解为数据容器,而范畴则是容器转换成另一种数据容器的整体过程.可以理解为管道的一种,不直接处理数据,而是改变数据容器的结构.
(2)函数式编程的核心意义:函数的组合实现更为复杂的数据容器结构.但是拆分成函子,又是通用的函数.即,可以不改变原有函数的内部逻辑的同时,组合产生新的函数逻辑
3.3 常用的装饰器模式
3.3.1 只执行一次装饰器
function once(fn) {
return function (...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
};
}
传入once函数中的函数只会被运行一次.
其中fn是函数,once是装饰器.最后返回的函数是函数的修饰函数
常见的map方法和reduce方法,也是装饰器模式的运用
[1,2,3].map(function(a){
return a*2;
})
[1,2,3].map(function(a,b){
return a*b;
})
3.3.2 节流装饰器
function throttle(fn, ms = 100) {
let throttleTimer = null;
return function(...args) {
if(!throttleTimer) {
const ret = fn.apply(this, args);
throttleTimer = setTimeout(() => {
throttleTimer = null;
}, ms);
return ret;
}
}
}
function once(fn) {
return throttle(fn, Infinity);
}
创建相应的定时函数,判断定时函数是否存在,来决定方法是否执行. 从而实现短时间内同一个请求只响应一次.
3.3.3 防抖装饰器
function debounce(fn, ms) {
let debounceTimer = null;
return function (...args) {
if(debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fn.apply(this, args);
}, ms);
};
}
和节流装饰器很像,但是不同于节流装饰器.是实现将一段时间内的多个请求,合并为1次执行.
3.3.4 拦截器的实现
某个方法只有满足了某种条件后才能执行.这是通用拦截器的需求
function intercept(fn, {beforeCall = null, afterCall = null}) {
return function (...args) {
if(!beforeCall || beforeCall.call(this, args) !== false) {
// 如果beforeCall返回false,不执行后续函数
const ret = fn.apply(this, args);
if(afterCall) return afterCall.call(this, ret);
return ret;
}
};
}
权限列表也可以用拦截器做,原理大致相同.
3.3.5 Ract中的装饰器模式
React中的高阶组件HOC
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// 将 input 组件包装在容器中,而不对其进行修改。Good!
return <WrappedComponent {...this.props} />;
}
}
}
React中也极为强调纯函数的概念,同样是React通用组件实现的基础.传入组件,不改变原有的组件,组合返回新的组件.
3.3.6 管道装饰器
链式调用执行多个方法
function pipe(...fns) {
return function(input) {
return fns.reduce((a, b) => {
return b.call(this, a);
}, input);
}
}
pipe(fn1,fn2,fn3);可以实现按顺序执行对应方法.实现多个方法的组合调用
3.4 高阶函数+原子操作=函数库
如果基础库中只有高阶纯函数和一些基本的原子操作(就像(el, key, value)=> {el.style[key] = value;}
这种简单操作)。如果这样做,那么对应的库的可维护性很高.只需要组合对应的函数,即可实现变幻的需求,而无需进行重复逻辑代码的编写.
而且函数库是可以无视环境和框架,都可以使用的.理论上来说,为了实现框架源码的精简性.对应的也会使用相似的思考.
在完成其它设计模式在场景中的实现后,开始从框架代码中,分析和整理其构建结构的场景,来思考对应不同的模式更适合的场景.