迫于生存压力开始补充(系统)学习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就稍微容易理解了。