《如何寫‘好’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是多麼的優雅😆~~