这是我参与「第四届青训营 」笔记创作活动的第31天
javascript书写原则
1. 各司其职
能用css的部分就用css,能引用html就使用html,尽量使用各自的语言做各自的事情。 例如我们经常使用的直接获取dom去修改样式就不太建议使用。
2. 组件封装
默认思路:完成样式以及功能
<div id='my-slider'>
<ul>
<li class='slider-list__item--selected'>
<img />
</li>
<li class='slider-list__item'>
<img />
</li>
<li class='slider-list__item'>
<img />
</li>
<li class='slider-list__item'>
<img />
</li>
</ul>
<a class='slider-list__prev'>
<a class='slider-list__next'>
</div>
class Slider{
constructor(id){
this.container = document.querySelector(id)
this.items = document.querySelector(id)
.querySelectorAll('slider-list__item, slider-list__item--selected')
}
//获取当前选中的页面
getSelectedItem(){
return document.querySelector('.slider-list__item--selected')
}
//获取当前选中的页面索引
getSelectedItemIndex(){
return [...this.items].indexOf(this.getSelectedItem())
}
//跳转到某个
selectTo(index){
let selected = this.getSelectedItem()
if(selected){
selected.className = 'slider-list__item'
}
let selectToEl = this.items[index]
if(selectToEl){
selected.className = 'slider-list__item--selected'
}
//this.items.forEach(item=>item.className=='slider-list__item')
//this.items[index].className = 'slider-list__item--selected'
}
//跳转到下一个
selectNext(){
let index = this.getSelectedItemIndex()
let nextIndex = (index + 1) % this.items.length
this.selectTo(nextIndex)
}
//跳转到上一个
selectPrevious(){
let index = this.getSelectedItemIndex()
let prevIndex = (this.items.length + index - 1) % this.items.length
this.selectTo(prevIndex)
}
} //此轮播图组件只封装了一些基本的方法,如果此时把操作dom的行为加上去,则耦合性很强,并且不适合扩展以及改造
组件设计的原则: 封装性,正确性,扩展性,复用性
实现组件的步骤: 结构设计,展现效果,行为设计
三次重构:
1. 插件化
2. 模板化
3. 抽象化
插件化改造
- 为什么要用插件化改造?
- 直接在原组件中写我们想要的业务功能也可以,但是每个业务场景不同,甚至即使相同的业务场景我们的诉求也是不太一样的,(拿此文章中举例子,我需要给此轮播图组件添加两个按钮,分别是点击下一页和点击上一页,这个可能满足大部分需求,但是可能你的需求不需要这一对按钮,或者即使需要按钮,但是点击效果是前进两页。)所以说组件尽量只提供单纯的基础方法,而业务就交给插件来扩展。
class Slider{
constructor(id){
this.container = document.querySelector(id)
this.items = document.querySelector(id)
.querySelectorAll('slider-list__item, slider-list__item--selected')
}
registerPlugins(...plugins){
//将所有的插件使用一次
plugins.forEach(plugin=>plugin(this))
}
getSelectedItem(){
return document.querySelector('.slider-list__item--selected')
}
getSelectedItemIndex(){
return [...this.items].indexOf(this.getSelectedItem())
}
selectTo(index){
let selected = this.getSelectedItem()
if(selected){
selected.className = 'slider-list__item'
}
let selectToEl = this.items[index]
if(selectToEl){
selected.className = 'slider-list__item--selected'
}
//this.items.forEach(item=>item.className=='slider-list__item')
//this.items[index].className = 'slider-list__item--selected'
}
selectNext(){
let index = this.getSelectedItemIndex()
let nextIndex = (index + 1) % this.items.length
this.selectTo(nextIndex)
}
selectPrevious(){
let index = this.getSelectedItemIndex()
let prevIndex = (this.items.length + index - 1) % this.items.length
this.selectTo(prevIndex)
}
}
// 此时 具体的业务就是点击下一页和点击上一页,我们通过Slider原生提供的基础方法来实现。
当然你也可以是点击下两页,点击上两页。扩展的好处就在于此。
function prev(slider){
let prev = slider.querySelector('slider-list__prev')
prev.addEvenetListener('click', e => this.selectPrevious())
}
function next(slider){
let next = slider.querySelector('slider-list__next')
next.addEvenetListener('click', e => this.selectNext())
}
const slider = new Slider('#my-slider')
//调用提供的注册插件的api,传进去prev,next业务方法, 在方法调用时 通过参数拿到slider的实例
slider.registerPlugins(prev, next)
//此时想要扩展组件就会很方便
模板化改造
- 为什么需要模板化改造
- 因为上面的例子中,我们组件还是需要提前写好对应的html,那此时如果我的业务需求是轮播图的下方还有四个点,点击每个点的时候跳转到对应的页面,那么此时还需要去原组件内部去修改html。 我们无法也不能每次都去修改组件的内部结构,那此时我们能像vue或者react一样用数据驱使组件的视图,是不是不用再每次去修改原代码。
class Slider{
constructor(id, opts = {images:[], cycle:3000}){
//此时组件只需要一个盒子
this.container = document.querySelector(id)
this.options = opts
//数据创建模板, 更加灵活
this.container.innerHTML = this.render()//用传进的数据去生成对应的轮播图
this.items = document.querySelector(id)
.querySelectorAll('slider-list__item, slider-list__item--selected')
}
// 此时组件新增了一个render方法,用于给组件生成自己的html
render(){
let images = this.options.images.map(img=>`
<li>
<image src=${img} />
</li>
`)
return `<ul>${images.join('')}</ul>`
}
registerPlugins(...plugins){
//此时我们的注册插件也进行扩展,插件也将可以生成自己的html并插入到你想放的盒子里面
//将所有的插件使用一次
plugins.forEach(plugin=>{
//插件定制化使用模板, 统一使用actions初始化
//plugin.render(this.options.images)
plugin.actions(this)
})
}
getSelectedItem(){
return document.querySelector('.slider-list__item--selected')
}
getSelectedItemIndex(){
return [...this.items].indexOf(this.getSelectedItem())
}
selectTo(index){
let selected = this.getSelectedItem()
if(selected){
selected.className = 'slider-list__item'
}
let selectToEl = this.items[index]
if(selectToEl){
selected.className = 'slider-list__item--selected'
}
//this.items.forEach(item=>item.className=='slider-list__item')
//this.items[index].className = 'slider-list__item--selected'
}
selectNext(){
let index = this.getSelectedItemIndex()
let nextIndex = (index + 1) % this.items.length
this.selectTo(nextIndex)
}
selectPrevious(){
let index = this.getSelectedItemIndex()
let prevIndex = (this.items.length + index - 1) % this.items.length
this.selectTo(prevIndex)
}
}
const dots = {
//每个插件可以定制自己的模板
render(opts){
let dots= opts.map(opt,index=> `
<span index=${index}></span>
`)
return `<div calss='dot' >${dots.join('')}</div>`
}
//初始化函数 拿到slider实例
action(slider){
//拿到slider实例中的images传给render生成自己的html
let el = this.render(slider.options.images)
//然后将html放到盒子中对应的位置并 实现对应的业务逻辑
let container = slider.container
container.appendChild(el)
let spans = container.querySelector('.dot').querySelectAll('span')
[...spans].forEach(span=>{
span.addEventListener('mouseon', ()=>{
slider.selectTo(span.index)
})
})
}
}
function prev(slider){
let prev = slider.querySelector('slider-list__prev')
prev.addEvenetListener('click', e => this.selectPrevious())
}
function next(slider){
let next = slider.querySelector('slider-list__next')
next.addEvenetListener('click', e => this.selectNext())
}
//使用组件时 根据数据生成对应模板
const slider = new Slider('#my-slider', {images:[1.jpg, 2.jpg, 3.jpg, 4.jpg]})
//注册插件也有添加html的能力, 方法的能力也还得有保留
slider.registerPlugins(dots)
抽象化改造
- 为什么要进行抽象化改造?
- 因为我们在编写类似组件时,其实有大量类似的逻辑,此时如果想偷懒的话,我们可以抽象出一套模板组件(例如下面), 在实际编写组件的时候可以用js提供的继承,来直接继承这些模板的方法。避免重复书写。此处就不展开说了。。。
class Component{
constructor(id, opts = {data:[], cycle:3000}){
this.container = document.querySelector(id)
this.options = opts
this.container.innerHTML = this.render(opts.data)
}
registerPlugin(...plugins){
plugins.forEach(plugin=>{
//插件定制化使用模板, 统一使用actions初始化
plugin.render(this.options.images)
plugin.actions(this)
})
}
render(){
return ''
}
}
//抽象出组件通用的模型, 适用于所有组件使用
3. 过程抽象
函数本身就是一个封装好的过程,某一种类型的函数会遇见同一个类型的副作用,例如某一种类型的函数都需要防抖,某一种类型的函数都默认都只能点击一次。 那么这一种类型的副作用可以抽象出同一种方法来解决。这就是过程抽象。
//过程抽象就是一个高阶函数, 它接受一个函数并返回一个函数,它并不修改传入函数的任何内容,但却在原函数的上一层,增加了其他的抽象化后的过程。
//这个过程可以是限制原函数的触发(一次函数,防抖,节流等),可以是提前为原函数数据格式化,也可以是对函数是否处理进行鉴权等等操作。
//过程抽象是一个非常有意思的过程,就好比你在吃饭前必须要洗手,那么洗手这个动作就可以抽象化写成一个函数,在以后遇到吃饭的业务场景,无论是吃晚饭还是吃早饭,还是躺在女朋友怀里吃饭饭,你都可以把吃饭的函数传给洗手的函数,想吃饭就必须得洗手。
//一次函数
function once(fn){
return function(...arg){
if(fn){
let fnRet = fn.apply(this, arg)
fn = null
return fnRet
}
}
}
function debuconce(fn, delay){
let timer = null
return function(...args){
clearTimeout(timer)
timer = setTimeout(()=>{
return fn && fn.apply(this, args)
}, delay)
}
}
function throttle(fn, time=5000){
let timer
return function(...args){
if(timer == null){
fn && fn.apply(this. args)
setTimeout(()=>{
timer = null
}, time)
}
}
}
//手写一个每隔一段时间执行一次的抽象方法
//
function consumer(fn, delay){
let tasks = [],
timer
return function(..args){ //先把函数都push到任务列表
tasks.push(fn.bind(this, ...args))
if(timer==null){
timer = setInterval(()=>{
tasks.shift().call(this)
if(tasks.length <=0){
clearInterval(timer)
timer = null
}
}, delay)
}
}
}
//
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, [obj, ...rest])
}
const setColor = interative((el,color) => {
el.style.color = color
//操作过程 将每个el都执行 这个过程
})
const els = document.querySelectorAll('li:nth-child(2n+1)')
setColor(els, 'red')