react学习记录-类组件化理解

迫于生存压力开始补充(系统)学习React,参考了React.js小书的学习进程进行探索,记录一下我的学习思考,争取端午啃完它

React.js 简单认识


React.js似乎更加强调界面的组件化,比如说JSX的书写方式,类组件等等,都带着组件化的思维。在使用vue的时候,虽然有使用组件化的思想去构建界面,但无非就是模块化组件的代码,然后分别引入而已。尤其在做组件通信的时候,是会感觉到vue的一个组件化其实很不明显,通信机制也是比较麻烦的(在父子通信、兄弟通信和生命周期函数面前反复狗带)。相信React.js作为一种生态更加丰富的MVVM框架,在数据变化导致的组件更新上有着更好的解决方案。
那么,说到前端组件化,它到底解决了什么问题?

前端组件化解决了什么样的问题(一)组件复用


一个是组件的复用,也就是前端代码的复用。

以点赞按钮为例子:

<body>
    <div class='wrapper'>
        <button class='like-btn'>
            <span class='like-text'>点赞</span>
            <span>👍</span>
        </button>
    </div>
</body>
const button = document.querySelector('.like-btn')
const buttonText = button.querySelector('.like-text')
let isLiked = false
button.addEventListener('click', () => {
    isLiked = !isLiked
    if (isLiked) {
        buttonText.innerHTML = '取消'
    } else {
        buttonText.innerHTML = '点赞'
    }
}, false)

我们在复用一个组件时,并不仅仅是希望复用它的一个样式结构,可能还希望得到它的一些事件绑定函数,让开发的队友直接拷贝到项目中,往往是不太方便的。你不可避免地要将某个组件的结构都cv过去,还有它在JavaScript中绑定的一些方法。就像上面的点赞按钮例子一样,没有体现出可复用性。

可复用性我们可以依靠类来实现,在react中编写类组件,我们会给类提供一个render方法,并让render方法返回一串HTML:

class LikeButton {
    render () {
        return `
            <button id='like-btn'>
                <span class='like-text'>赞</span>
                <span>👍</span>
            </button>
        `
    }
}

问题又来了,现在按钮没有绑定事件方法,要怎么在字符串中为button添加事件。我们知道addEventListener是DOM的API,所以想要使得这个HTML字符串变成DOM结构。我们假设有一个函数叫做createDOMFromString(),我们往里面传入HTML字符串,就可以返回相应的DOM元素,就能实现事件的添加了。类似下面:

createDOMFromString():

// ::String => ::Document
const createDOMFromString = (domString) => {
  const div = document.createElement('div')
  div.innerHTML = domString
  return div
}

对LikeButton类进行丰富:

class LikeButton {
    render () {
        this.el = createDOMFromString(`
            <button class='like-button'>
                <span class='like-text'>点赞</span>
                <span>👍</span>
            </button>
        `)
        this.el.addEventListener('click', () => console.log('click'), false)
        return this.el
    }
}

现在我们会把HTML先转为DOM结构,然后对它添加事件再返回。

下面是把两个新建的DOM元素插入到HTML之中,因为render方法现在返回了DOM,所以要用DOM的API去插入元素,比如说appendChild():

const wrapper = document.querySelector('.wrapper')

const likeButton1 = new LikeButton()
wrapper.appendChild(likeButton1.render())

const likeButton2 = new LikeButton()
wrapper.appendChild(likeButton2.render())

现在两个按钮已经绑定上了事件,点击即可在控制台上打印出“click”,假如我们想修改按钮文本,还可以继续封装LikeButton类:

class LikeButton {
    constructor () {
        this.state = { isLiked: false }
    }

    changeLikeText () {
        const likeText = this.el.querySelector('.like-text')
        this.state.isLiked = !this.state.isLiked
        likeText.innerHTML = this.state.isLiked ? '取消' : '点赞'
    }

    render () {
        this.el = createDOMFromString(`
            <button class='like-button'>
                <span class='like-text'>点赞</span>
                <span>👍</span>
            </button>
        `)
        this.el.addEventListener('click', this.changeLikeText.bind(this), false)
        return this.el
    }
}

上面的组件现在有了构造函数,构造函数为每个实例都注册了state对象,来保存按钮的状态。我们可以根据按钮状态,在事件触发时修改按钮的文本(innerHTML),并将事件方法绑定至DOM对象上。

这下组件的可复用性已经提高了很多,别人只要复制过去实例化使用,就能插入到DOM当中了。

前端组件化解决了什么样的问题(二)


上一例的changeLikeText函数,包含了对DOM的操作,它依赖了isLiked这个状态去变化,看起来似乎没什么影响。但是当你的组件依赖了很多的状态数据,那么一次事件触发就会发生组件上很多的DOM操作。代码中混杂着对DOM的操作其实是一种不好的实践,我们要尽量去减少手动的DOM操作。

优化(减少)DOM操作的解决方案(1)改变即更新

我们可以想一种解决方案:状态改变就重新调用render方法,构建一个新的DOM元素。这样做的好处是每次render都会用到最新的状态数据,去构造HTML字符串,进而产生不同的DOM元素,造成页面更新。

class LikeButton {
    constructor () {
        this.state = { isLiked: false }
    }

    setState (state) {
        this.state = state
        this.el = this.render()
    }

    changeLikeText () {
        this.setState({
            isLiked: !this.state.isLiked
        })
    }

    render () {
        this.el = createDOMFromString(`
            <button class='like-btn'>
                <span class='like-text'>${this.state.isLiked ? '取消' : '点赞'}</span>
                <span>👍</span>
            </button>
        `)
        this.el.addEventListener('click', this.changeLikeText.bind(this), false)
        return this.el
    }
}

与之前的类组件,不同之处在于:

1、render中的HTML字符串中使用到了模板字符串,会根据this.state去变化;

2、用户点击按钮的时候,changeLikeText会构建新的state对象,这个新的state会传入到setState函数中;

3、新增setState函数,接受一个新对象,并覆盖改变实例的state,进而重新调用一次render方法。

这样就导致事件触发,就会通过setState去传参并调用render,render根据state的改变而重新构造不同的DOM元素。这样就解除了手动的DOM操作,不用调用innerHTML之类的DOM API。

优化(减少)DOM操作的解决方案(2)重新插入新的DOM元素

然而方案一这种改进其实没有什么效果,重新渲染的DOM元素其实没有插入到页面当中。我们需要在组件外面得知组件发生了改变,再更新页面中的DOM元素。

现在修改一下setState方法:

setState (state) {
    const oldEl = this.el
    this.state = state
    this.el = this.render()
    if (this.onStateChange) this.onStateChange(oldEl, this.el)
}

实例化并使用组件:

const likeButton = new LikeButton()
wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素
likeButton.onStateChange = (oldEl, newEl) => {
  wrapper.insertBefore(newEl, oldEl) // 插入新的元素
  wrapper.removeChild(oldEl) // 删除旧的元素
}

每次调用setState时,我们会调用一个onStateChange方法,它用于告知外部,可以插入新的DOM元素到组件中进行替换,更新页面。

前端组件化(三)抽象出公共组件类


为了让代码更加灵活,我们可以抽象出一种模式出来,放到一个Component类之中:

class Component {
    setState (state) {
        const oldEl = this.el
        this.state = state
        this._renderDOM()
        if (this.onStateChange) this.onStateChange(oldEl, this.el)
    }

    _renderDOM () {
        this.el = createDOMFromString(this.render())
        if (this.onClick) {
            this.el.addEventListener('click', this.onClick.bind(this), false)
        }
        return this.el
    }
}

我们所有的组件都可以从Component继承而来,它具备了两个方法,一个是setState,另一个是私有方法**_renderDOM**。_renderDOM方法会调用this.render获得字符串,并构建DOM元素,监听onClick事件。我们的子类只需要实现一个render方法,返回HTML字符串给它即可。

还有一个额外的mount方法,顾名思义,就是把组件加载进去,也就是把组件的DOM元素插入到页面当中,并且在setState的时候更新页面:

const mount = (component, wrapper) => {
    wrapper.appendChild(component._renderDOM())
    component.onStateChange = (oldEl, newEl) => {
        wrapper.insertBefore(newEl, oldEl)
        wrapper.removeChild(oldEl)
    }
}

这样一来,点赞组件就可以写成React中比较常见的类组件写法:

class LikeButton extends Component {
    constructor () {
        super()
        this.state = { isLiked: false }
    }

    onClick () {
        this.setState({
            isLiked: !this.state.isLiked
        })
    }

    render () {
        return `
        <button class='like-btn'>
            <span class='like-text'>${this.state.isLiked ? '取消' : '点赞'}</span>
            <span>👍</span>
        </button>
        `
    }
}

mount(new LikeButton(), wrapper)

当我们希望给组件加一点自定义的配置时,比如说按钮的背景颜色,就可以传参进去定制。即修改一下render,使用模板字符串为标签增加相应的样式,值则由构造函数的参数决定。

class LikeButton extends Component {
    constructor (props) {
        super(props)
        this.state = { isLiked: false }
    }

    onClick () {
        this.setState({
            isLiked: !this.state.isLiked
        })
    }

    render () {
        return `
        <button class='like-btn' style="background-color: ${this.props.bgColor}">
            <span class='like-text'>
            ${this.state.isLiked ? '取消' : '点赞'}
            </span>
            <span>👍</span>
        </button>
        `
    }
}

mount(new LikeButton({ bgColor: 'red' }), wrapper)

至此就完成了一种灵活进行组件化的方案,即通过Component类进行继承并复用,配合mount方法插入到DOM节点当中。React.js就是用相似的方法去实现组件化界面的(非hooks写法),铺垫了这个之后,学React就稍微容易理解了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值