JavaScript进阶 | 青训营笔记

95 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的第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')