《如何写‘好’javascript》这门课是由360技术专家月影老师讲的。javascript
这堂课的pptcss
说实话,我一直在纠结要不要写关于js的文章,由于对于js来讲,个人实际经验不足,更不要说面向对象编程与函数式编程了,对于过程抽象与行为抽象也没有深刻的理解,但想一想仍是以为应该分享出来,而且我尽可能原汁原味的阐述这门课的内容,尽可能不加入本身主观理解,由于对于没有实际经验的我来讲,若是添加本身主观的理解只能误导读者,好了,不费话了~html
需求:java
light.onclick = function(evt) { if(light.style.backgroundColor !== 'green'){ document.body.style.backgroundColor = '#000'; document.body.style.color = '#fff'; light.style.backgroundColor = 'green'; }else{ document.body.style.backgroundColor = ''; document.body.style.color = ''; light.style.backgroundColor = ''; } }
对于我来讲,要是让我完成这个需求,大概应该就写成这样吧^_^,面试
答案确定是很差的。算法
这样写的问题:编程
lightButton.onclick = function(evt) { if(main.className === 'light-on'){ main.className = 'light-off'; }else{ main.className = 'light-on'; } }
这回代码语义化就比较强了,经过js去修改className而不是用js来直接修改style,这样写会比较好一点。api
<input id="light" type="checkbox"></input> <div id="main"> <div class="pic"> <img src="https://p4.ssl.qhimg.com/t01e932bf06236f564f.jpg"> </div> <div class="content"> <pre> 今天回到家, 煮了点面吃, 一边吃面一边哭, 泪水滴落在碗里, 没有开灯。 </pre> </div> <label for="light"> <span id="lightButton"> </span> <label> </div> <style> html,body { margin: 0; padding: 0; width: 100%; height: 100%; } #light { display: none; } #main { position: relative; padding: 20px; width: 100%; height: 100%; background-color: #fff; color: #000; transition: all .5s; } #light:checked + #main { background-color: #000; color: #fff; } .pic { float: left; margin-right: 20px; } .content { font-weight: bold; font-size: 1.5em; } #lightButton { border: none; width: 25px; height: 25px; border-radius: 50%; position: absolute; left: 30px; top: 30px; cursor: pointer; background: red; } #light:checked+#main #lightButton { background: green; } </style>
这么写的思路就是不使用js,而是经过input和label关联来切换状态。app
这是你们最熟悉不过的轮播图组件了,若是用面向过程的写法,可能会出现不少bug,那么如何实现才是最好的呢?框架
总体思路
<ul>
和<li>
具体实现:
class Slider{ constructor(id){ this.container = document.getElementById(id); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); } // 得到当前元素 getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected } // 得到当前元素的索引 getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } // 切换到第index张图片 slideTo(idx){ const selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } const item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; } } // 切换到下一张图片 slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } // 切换到上一张图片 slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } } // 经过new来实例化 const slider = new Slider('my-slider'); setInterval(() => { slider.slideNext() }, 3000)
控制结构
<a class="slide-list__next"></a> <a class="slide-list__previous"></a> <div class="slide-list__control"> <span class="slide-list__control-buttons--selected"></span> <span class="slide-list__control-buttons"></span> <span class="slide-list__control-buttons"></span> <span class="slide-list__control-buttons"></span> </div>
自定义事件
const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event)
由于下方原点与图片自动切换的下标(index)是一致的,因此能够经过事件机制,在图片slide时候直接给container派发一个事件,这样的话呢,经过container去监听这个事件,去更新控制结构上小圆点的状态。
具体实现:
class Slider{ constructor(id, cycle = 3000){ this.container = document.getElementById(id); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = cycle; const controller = this.container.querySelector('.slide-list__control'); if(controller){ const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected'); controller.addEventListener('mouseover', evt=>{ const idx = Array.from(buttons).indexOf(evt.target); if(idx >= 0){ this.slideTo(idx); this.stop(); } }); controller.addEventListener('mouseout', evt=>{ this.start(); }); // 监听slide事件 this.container.addEventListener('slide', evt => { // 拿到slide事件传来的index const idx = evt.detail.index const selected = controller.querySelector('.slide-list__control-buttons--selected'); if(selected) selected.className = 'slide-list__control-buttons'; buttons[idx].className = 'slide-list__control-buttons--selected'; }) } const previous = this.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { this.stop(); this.slidePrevious(); this.start(); evt.preventDefault(); }); } const next = this.container.querySelector('.slide-list__next'); if(next){ next.addEventListener('click', evt => { this.stop(); this.slideNext(); this.start(); evt.preventDefault(); }); } } getSelectedItem(){ let selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx){ let selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } let item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; } const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) } slideNext(){ let currentIdx = this.getSelectedItemIndex(); let nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ let currentIdx = this.getSelectedItemIndex(); let previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } const slider = new Slider('my-slider'); slider.start();
这个实现的构造函数会复杂一些,可是把timer定时器也封装进去了,会有轮播的时间默认为3秒钟,一样的也是得到container,items,cycle(时间)经过事件机制将控制流中的小圆点与图片联动起来。而且还判断了controler是否存在,假如之后咱们不须要小圆点这个功能了,咱们只须要把html中相关的结构去掉,js也不会报错,可是这里还有一个优化的点就是slider与controler之间有着比较强的耦合度。
为何要用到事件机制呢?由于要下降结构之间的耦合度,若是不这样作的话,咱们须要作双向的操控的。
好比咱们要添加一个需求:显示当前index。
只须要这样作:
<div id="other">第0张</div>
document.addEventListener('slider', (evt) => { other.innerHTML = `第${evt.detail.index}张` })
其实仍是有很大的改动空间的,好比上面的代码在构造函数的代码量特别多,slider与controler的耦合度比较大,如何下降它们之间的耦合度呢?
class Slider{ constructor(id, cycle = 3000){ this.container = document.getElementById(id); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = cycle; } registerPlugins(...plugins){ plugins.forEach(plugin => plugin(this)); } getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx){ const selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } const item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; } const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) } slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } addEventListener(type, handler){ this.container.addEventListener(type, handler) } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } function pluginController(slider){ const controller = slider.container.querySelector('.slide-list__control'); if(controller){ const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected'); controller.addEventListener('mouseover', evt=>{ const idx = Array.from(buttons).indexOf(evt.target); if(idx >= 0){ slider.slideTo(idx); slider.stop(); } }); controller.addEventListener('mouseout', evt=>{ slider.start(); }); slider.addEventListener('slide', evt => { const idx = evt.detail.index const selected = controller.querySelector('.slide-list__control-buttons--selected'); if(selected) selected.className = 'slide-list__control-buttons'; buttons[idx].className = 'slide-list__control-buttons--selected'; }); } } function pluginPrevious(slider){ const previous = slider.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); slider.slidePrevious(); slider.start(); evt.preventDefault(); }); } } function pluginNext(slider){ const next = slider.container.querySelector('.slide-list__next'); if(next){ next.addEventListener('click', evt => { slider.stop(); slider.slideNext(); slider.start(); evt.preventDefault(); }); } } const slider = new Slider('my-slider'); slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start();
这样作的好处:好比咱们不想要controler这个组件了,直接删掉插件与html对应结构,其余的功能仍是能够正常使用。
上面的代码还不是特别的优雅,当咱们不想要一个功能时,须要删除html结构与js代码,若是用模板化,只须要修改js便可。
render方法会传data数据,负责构造html结构
action方法会注入component对象,负责初始化这个对象,添加事件、行为。
这样咱们的html结构只有
<div id="my-slider" class="slider-list"></div>
class Slider{ constructor(id, opts = {images:[], cycle: 3000}){ this.container = document.getElementById(id); this.options = opts; this.container.innerHTML = this.render(); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = opts.cycle || 3000; this.slideTo(0); } 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 => { const pluginContainer = document.createElement('div'); pluginContainer.className = '.slider-list__plugin'; pluginContainer.innerHTML = plugin.render(this.options.images); this.container.appendChild(pluginContainer); plugin.action(this); }); } getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx){ const selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } let item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; } const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) } slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } addEventListener(type, handler){ this.container.addEventListener(type, handler); } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } const pluginController = { render(images){ return ` <div class="slide-list__control"> ${images.map((image, i) => ` <span class="slide-list__control-buttons${i===0?'--selected':''}"></span> `).join('')} </div> `.trim(); }, action(slider){ const controller = slider.container.querySelector('.slide-list__control'); if(controller){ const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected'); controller.addEventListener('mouseover', evt => { const idx = Array.from(buttons).indexOf(evt.target); if(idx >= 0){ slider.slideTo(idx); slider.stop(); } }); controller.addEventListener('mouseout', evt => { slider.start(); }); slider.addEventListener('slide', evt => { const idx = evt.detail.index const selected = controller.querySelector('.slide-list__control-buttons--selected'); if(selected) selected.className = 'slide-list__control-buttons'; buttons[idx].className = 'slide-list__control-buttons--selected'; }); } } }; const pluginPrevious = { render(){ return `<a class="slide-list__previous"></a>`; }, action(slider){ const previous = slider.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); slider.slidePrevious(); slider.start(); evt.preventDefault(); }); } } }; const pluginNext = { render(){ return `<a class="slide-list__next"></a>`; }, action(slider){ const previous = slider.container.querySelector('.slide-list__next'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); slider.slideNext(); slider.start(); evt.preventDefault(); }); } } }; const slider = new Slider('my-slider', {images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', 'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg', 'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg', 'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000}); slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start();
这样作的好处就是咱们能够随意修改这个组件的功能,若是不想要两边的按钮或者控制流的小圆点,只须要修改注册插件便可。
插件化/模板化这种作法还有一个缺点就是若是咱们修改插件时,咱们直接append到组件里,可能只修改了一点点代码,最后致使整个dom都刷新了,这就是为何如今一些主流框架采用虚拟dom的方式,经过diff算法来局部修改dom。
class Component{ constructor(id, opts = {data:[]}){ this.container = document.getElementById(id); this.options = opts; this.container.innerHTML = this.render(opts.data); } registerPlugins(...plugins){ plugins.forEach(plugin => { const pluginContainer = document.createElement('div'); pluginContainer.className = '.slider-list__plugin'; pluginContainer.innerHTML = plugin.render(this.options.data); this.container.appendChild(pluginContainer); plugin.action(this); }); } render(data) { /* abstract */ return '' } } class Slider extends Component{ constructor(id, opts = {data:[], cycle: 3000}){ super(id, opts); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = opts.cycle || 3000; this.slideTo(0); } render(data){ const content = data.map(image => ` <li class="slider-list__item"> <img src="${image}"/> </li> `.trim()); return `<ul>${content.join('')}</ul>`; } getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx){ const selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } const item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; } const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) } slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } addEventListener(type, handler){ this.container.addEventListener(type, handler); } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } const pluginController = { render(images){ return ` <div class="slide-list__control"> ${images.map((image, i) => ` <span class="slide-list__control-buttons${i===0?'--selected':''}"></span> `).join('')} </div> `.trim(); }, action(slider){ let controller = slider.container.querySelector('.slide-list__control'); if(controller){ let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected'); controller.addEventListener('mouseover', evt=>{ var idx = Array.from(buttons).indexOf(evt.target); if(idx >= 0){ slider.slideTo(idx); slider.stop(); } }); controller.addEventListener('mouseout', evt=>{ slider.start(); }); slider.addEventListener('slide', evt => { const idx = evt.detail.index; let selected = controller.querySelector('.slide-list__control-buttons--selected'); if(selected) selected.className = 'slide-list__control-buttons'; buttons[idx].className = 'slide-list__control-buttons--selected'; }); } } }; const pluginPrevious = { render(){ return `<a class="slide-list__previous"></a>`; }, action(slider){ let previous = slider.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); slider.slidePrevious(); slider.start(); evt.preventDefault(); }); } } }; const pluginNext = { render(){ return `<a class="slide-list__next"></a>`; }, action(slider){ let previous = slider.container.querySelector('.slide-list__next'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); slider.slideNext(); slider.start(); evt.preventDefault(); }); } } }; const slider = new Slider('my-slider', {data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', 'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg', 'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg', 'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000}); slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start();
咱们从最初的需求开始一步一步的获得最终组件抽象的这个模型,咱们理清楚了组件和插件的关系,以及他们之间应该怎样完成渲染,这里面很重要的是咱们一步步的在作抽象,一步步的抽象出来这些元素,而后一步步的拆解这些元素之间的依赖关系,尽可能把他们独立出来,无论组件也好仍是插件也好,咱们都但愿将来当咱们这个ui、交互有一小部分的变化的时候,咱们只要去修改、重建这部分变化所涉及到的插件或者组件就能够了,而不用动整个这个代码结构,这样让咱们代码的健壮性和可维护性就大大的加强了,咱们就能够把这个组件发布出来了。
这三个东西设计完以后,经过一些技巧,把这个组件这三个部分给封装好,而且把他们抽象出来,下降他们的耦合度。好比咱们用到了依赖注入技巧、自定义事件技巧、模板化的技巧,这些技巧均可以让咱们设计出低耦合度的ui组件。
我一直以为一篇文章过多的代码会让读者感到视觉疲劳,但实在是没有须要修改的地方,很是建议你们一步步的敲一遍,深入体会月影大大写的javascript是多么的优雅😆~~