用掘金已经很久了,一直都有写点什么的想法,无奈技术实在太菜,无从下手,潜意识里还总想等到技术成熟一些再开始,然而大家知道,这个世界遇到问题解决问题才是真正的王道,做好准备也是在有大量经验基础上的,所以,和我有一样顾虑的小伙伴,现在不开始更待何时呢?
开始之前想吐槽一下行业内一些现象,就叫IT鄙视链吧。后端鄙视前端,前端鄙视测试。特别是有很多前后端不分离项目的公司里,后端会写点前端或者前端会写点后端就觉得牛逼了,只想说这是一种无知的表现。前后端分离就像NBA中的小球时代的到来一样,随着时代发展这是必然趋势,是库里开启了小球时代吗?不,应该是加速了小球时代的到来。随着4g、5g时代的到来,硬件设备性能大幅提升,前端页面越来越复杂,大量交互的场景,前后端不分离的方式显然无法满足。因为编程的世界里,是以ms为单位的,几百ms已经算是很长时间了,你总不会希望每次请求响应过来一个带着数据的页面吧,那比单纯的响应数据肯定是要费时的。而且前后端分离也符合分层架构、低耦合的原则(其实我也不知道自己在说啥)。总之,一个技术的产生就像洋葱一样,是在以前的基础上进行的一次又一次的封装,也许你知道的只是洋葱表面,想扒开洋葱圈看到洋葱心简直太难,主要tmd辣眼睛😂。话不多逼逼,开始进入正题。 假如开发这样一个button组件:

html代码
<button class="btn">
<span class="comeonbaby">来啦老弟!</span>
<span>^_^</span>
</button>
js代码
let btn = document.querySelector('.btn')
let flag = true
btn.addEventListener('click', () => {
flag = !flag
btn.querySelector('.comeonbaby').innerHTML = flag? '来啦老弟!':'老弟走了!'
})
过段时间另一个项目也用到了这个button组件,如果只是复制js代码还好,但是如果想要这样的功能还要复制dom结构,非常麻烦。能不能只提供一个方法呢?那好,继续改进:
let btn = document.querySelector('.wrapper')
btn.innerHTML = getButton()
let flag = true
function change() {
flag = !flag
document.querySelector('.comeonbaby').innerHTML = flag? '来啦老弟!':'老弟走了!'
}
function getButton() {
return `
<button class="btn" onclick="change()">
<span class="comeonbaby">来啦老弟!</span>
<span>^_^</span>
</button>
`
}
- 上面提供一个获取dom字符串的方法,用innerHTML暴力的插入到class为wrapper的元素中,似乎也能解决问题,但大量的dom操作严重影响性能
- 我们知道,这个button组件文字的变化是因为点击之后状态和行为发生了变化,引起了视图的改变,可以说是MVVM设计模式。那好,我们继续改进:
class ButtonComponent {
constructor() {
this.state = {
isComing: true
}
}
render() {
return `
<button class="btn">
<span class="comeonbaby">${this.state.isComing?'来啦老弟!':'老弟走了!'}</span>
<span>^_^</span>
</button>
`
}
}
let btn = document.querySelector('.wrapper')
btn.innerHTML = new ButtonComponent().render()
这种方法是可以初始化一个button的,但是我们发现在字符串的基础上是没法绑定事件的,那么,继续优化,做一些小的改动
class ButtonComponent {
constructor() {
this.state = {
isComing: true
}
}
setState(state) {
this.state = state
this.render()
}
getState() {
this.setState({
isComing: !this.state.isComing
})
}
render() {
this.el = createDomFromString(
`
<button class="btn">
<span class="comeonbaby">${this.state.isComing?'来啦老弟!':'老弟走了!'}</span>
<span>^_^</span>
</button>
`
)
this.el.addEventListener('click', this.getState.bind(this))
return this.el
}
}
function createDomFromString(str) {
let wrapper = document.createElement('div')
wrapper.innerHTML = str
return wrapper
}
let box = document.querySelector('.wrapper')
let com = new ButtonComponent()
box.appendChild(com.render())
上面的代码增加了一个createDomFromString函数,用来将字符串格式化成dom节点,这样就可以绑定事件了,触发事件运行getState方法获取改变后的状态,在调用setState方法更新state然后重新执行render函数,然而我们发现,视图并没有更新,原因是虽然render函数重新执行返回了需要的dom,但是并没有通知到外部进行更新操作。好吧,继续优化:
class ButtonComponent {
//.....
setState(state) {
let oldElement = this.el
this.state = state
this.el = this.render()
if(this.onStateChange)this.onStateChange(oldElement, this.el)
}
//....
}
function createDomFromString(str) {
let wrapper = document.createElement('div')
wrapper.innerHTML = str
return wrapper
}
let box = document.querySelector('.wrapper')
let com = new ButtonComponent()
box.appendChild(com.render())
com.onStateChange = (oldEl, newEl) => {
box.insertBefore(newEl,oldEl)
box.removeChild(oldEl)
}
这样就实现了状态改变,通知消费者进行组件的更新。问题又来了,如果想写另外一个组件呢,是不是还要复制一遍上面的class类,能不能封装一个父类,父类负责dom的编译渲染和事件绑定,也就是视图的更新,子类负责提供特定的dom结构以及具体的业务逻辑,so?继续优化:
//先封装一个父类,父类负责视图更新
class Components {
setState(state) {
let oldElement = this.el
this.state = state
this.el = this.fooRender()
if(this.onStateChange) this.onStateChange(oldElement, this.el)
}
fooRender() {
this.el = createDomFromString(this.render())
this.el.addEventListener('click', this.getState.bind(this))
return this.el
}
}
//在修改一下ButtonComponent类,子类负责提供特定的dom结构以及具体的业务逻辑
class ButtonComponent extends Components {
constructor() {
super()//必须写,否则报错
this.state = {
isComing: true
}
}
getState() {
this.setState({
isComing: !this.state.isComing
})
}
render() {
return `
<button class="btn">
<span class="comeonbaby">${this.state.isComing?'来啦老弟!':'老弟走了!'}</span>
<span>^_^</span>
</button>
`
}
}
let box = document.querySelector('.wrapper')
let com = new ButtonComponent()
box.appendChild(com.fooRender())//执行fooRender方法
com.onStateChange = (oldEl, newEl) => {
box.insertBefore(newEl,oldEl)
box.removeChild(oldEl)
}
这样子就差不多了,你会发现写法基本和react的class组件一样,所有组件都要继承自components组件,状态改变必须调用setState方法,没有用到的就是jsx语法,react是将render函数中的jsx部分先转换成js对象来描述dom结构,也可以说是虚拟dom,再渲染成真是dom,当然这里的class组件也可以写成function组件。
- 到这里其实能做得事情已经很多了,比如现在的文字是固定的,我们可以传入一些参数来改变现实的文字,继续:
class Components {
constructor(props={}) {
this.props = props
}
//父类加上构造函数,接收传入的参数
//.......
}
class ButtonComponent extends Components {
constructor(props) {
super(props)//子类也要加上
this.state = {
isComing: true
}
}
render() {
const { comeon,letusgo } = this.props
const word1 = comeon || '来啦老弟!'
const word2 = letusgo || '老弟走了!'
return `
<button class="btn">
<span class="comeonbaby">${this.state.isComing?word1:word2}</span>
<span>^_^</span>
</button>
`
}
//.......
}
//只需要在实例化的时候传入参数即可
let com = new ButtonComponent({ comeon:"不去了",letusgo:"886" })
上面只是改变了显示文字,想改变各种样式都可以传入相应参数
还有一些功能可以完善,比如改变父组件传过来的props从而更新视图的变化,这就要在构造函数里利用Object.defineProperties方法劫持变量,当变量改变的时候重新触发fooRender方法更新视图,事实上,react每次更新视图就重新render。
终于写完了,已经凌晨两点多了,写了五个小时了🤣,第一次写文章,markdown用的还不熟练,感谢掘金,感谢老铁,晚安!