这样理解react岂不是很好!

219 阅读5分钟

用掘金已经很久了,一直都有写点什么的想法,无奈技术实在太菜,无从下手,潜意识里还总想等到技术成熟一些再开始,然而大家知道,这个世界遇到问题解决问题才是真正的王道,做好准备也是在有大量经验基础上的,所以,和我有一样顾虑的小伙伴,现在不开始更待何时呢?

开始之前想吐槽一下行业内一些现象,就叫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用的还不熟练,感谢掘金,感谢老铁,晚安!