这是我参与「第四届青训营 」笔记创作活动的的第3天
笔记有摘自月影老师课堂PPT上的内容,并附带一些自己的思考。
outline
- 职能分离
- 组件封装
- 插件化
- 模板化
- 抽象化
- 过程抽象与高阶函数
职能分离
- HTML、CSS、JS的职能分离——并非三者分开不同的文件,而是各自要做属于自己范畴内的事;
- 如可以用JS操控class呈现不同的样式,避免直接使用JS修改CSS
组件封装
- 好的组件———封装性、复用性、扩展性、正确性;
- 根据场景,设计场景下需要的功能支持;
- 实现步骤:
- 设计总体的结构,根据所需功能设计对应API和控制流;
- 进行三次重构:插件化、模板化、抽象化;
- 如月影老师课堂上讲解的轮播图例子,构造函数中确定了操纵的轮播图对象,播放时间间隔,控制按钮定义等;并暴露控制API。
class Slider{ constructor(id, cycle = 3000){ //获取要操纵的元素以及轮播间隔 //为轮播图下标按钮添加事件:当鼠标移入/移出/轮播时 //获取按钮并添加事件 } getSelectedItem(){} //当前选中页 getSelectedItemIndex(){} //当前选中页下标 slideTo(idx){} //去到固定页数 slideNext(){} //下一页 slidePrevious(){} //上一页 start(){} //开始轮播 stop(){} //暂停轮播 } const slider = new Slider('my-slider'); slider.start();
插件化
- 将控制元素抽取成插件;
- 插件与组件之间通过依赖注入方式建立联系;
- 比如轮播图中,将可以跳转轮播图页面的下标圆点、上下页按钮作为控制元素抽取出来封装成插件;插件在组件中注册后的this指向轮播图类,即可以使用该类中的API,如下为例:
class Slider(){ constructor(id, cycle = 3000){...} registerPlugins(...plugins){ plugins.forEach(plugin => plugin(this)); //注册插件,即可让插件调用类中API } ... } //上一页功能封装插件 function pluginPrevious(slider){ const previous = slider.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); //直接使用slide的方法 ... }); } } slider.registerPlugins(pluginController, pluginPrevious, pluginNext); //注入
模板化
-
把HTML部分抽离出来,即HTML模板化,下图摘自月影老师上课时所用图片。slide类中有专门的渲染函数,其把数据插入在含HTML文档的模板字面量中,最终返回包含元素标签的HTML文档。
-
在轮播图的例子中,轮播的图片数据作为参数传入构造函数中,在Slide类对象中调用render函数返回HTML模板。
-
包括插件中也配置了对应的render方法。
class Slider(){ constructor(id, opts = {images:[], cycle: 3000}){ ... this.options = opts; this.container.innerHTML = this.render(); //渲染HTML文档 } render(){ //渲染函数 const images = this.options.images; const content = images.map(image => ` <li class="slider-list__item"> <img src="${image}"/> </li> `.trim()); return `<ul>${content.join('')}</ul>`; } registerPlugins(...plugins){ plugins.forEach(plugin => { ... plugin.action(this);}); } } //在插件中也拥有自己的render方法 const pluginPrevious = { render(){ return `<a class="slide-list__previous"></a>`; }, action(slider){ ... } }; const slider = new Slider('my-slider', {images: ["图片URL列表"], cycle:3000}); //作为输入数据 slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
优点:
- 可以防止数据写死;
- 减少HTML代码量,模板化提高可复用率。
抽象化
- 把组件通用的模型抽象出来,即把一些公有、常用的属性或方法提取成一个父类;子类继承后可以使用父类的属性和方法,并添加自己的独有属性和方法让自己更具象化;
- 如下为轮播图例子中,Component中封装了常用的方法:
class Component{ constructor(id, opts = {name, data:[]}){ ... } registerPlugins(...plugins){ ... } render(data) { return '' } } - slide继承Component,注册插件时可以直接调用在Component上的方法:
class Slider extends Component{ constructor(id, opts = {name: 'slider-list', data:[], cycle: 3000}){ super(id, opts); //继承调用 ... } render(data){ ... } .... }
优点:
- 其他相似的类也可以继承父类属性和方法,可以提高组件可维护性、复用性和多样性,在大型的项目中可以减少冗余重复的代码量;
- 其次在修改代码时可以做局部修改而不影响全局代码。
过程抽象与高阶函数
- 在许多事件中,可能会用到相同的逻辑,从而把相同的逻辑给抽取出来形成一个函数,这样不同的事件都可以调用这个函数,故可以叫做过程抽象。
- 高阶函数参数是函数,返回值也为函数,可以想象函数中套了函数后形成闭包。
- 比如高阶函数
Once。 - 在第一次调用后,
Once的参数fn在返回的函数中变成了null,已经形成闭包,故第二次调用时不会执行return语句。function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } Once表示只被请求一次,可以应用在异步请求的场景中,比如防止用户多次请求时阻塞或元素添加、移除时报错,可以看出许多业务都会用到只需要执行一次的逻辑,故把该逻辑抽象出来形成Once函数。button.addEventListener('click', once((evt) => { setTimeout(() => {}, 2000); }));
总结
课程受益匪浅,难度对自己来说稍微有点大,在反复浏览代码的过程中逐渐理解了模块、封装等思想,在未来做项目时可以尝试应用。
下篇笔记会深入写写对高阶函数的理解。