40 行代码内完成一个 React.js

作者:胡子大哈
原文链接:http://huziketang.com/blog/posts/detail?postId=58aea515204d50674934c3ac

转载请申明出处,保留原文链接和作者信息。

目次

  • 1 媒介

  • 2 统统从点赞提及

  • 3 完成可复用性

    • 3.1 组织复用

    • 3.2 天生 DOM 元素而且增加事宜

  • 4 为何不暴力一点?

    • 4.1 状况转变 -> 构建新的 DOM 元素

    • 4.2 从新插进去新的 DOM 元素

  • 5 笼统出 Component 类

  • 6 总结

1 媒介

本文会教你如安在 50 行代码内,不依靠任何第三方的库,用纯 JavaScript 完成一个 React.js 。

本文的目标是:揭开对初学者看起来很很难明白的 React.js 的组件化情势的外套,让你有更多的精神和注重力去进修 React.js 精华的处所。假如你刚最先进修 React.js 而且觉得很渺茫,那末看完这篇文章今后就能够消除一些疑惑。

别的注重,本文所完成的代码只用于申明教授教养展现,并不适用于临盆环境。代码托管这个 堆栈 。心急如焚的同砚能够先去看代码,但本文会从最基础的内容最先诠释。

2 统统从点赞提及

接下来一切的代码都邑从一个基础的点赞功用最先演变,你会逐步看到,文章代码慢慢地愈来愈像 React.js 的组件代码。而在这个历程内里,人人须要只须要随着文章的思绪,就能够在代码的演变当中体会到组件化情势。

假定如今我们须要完成一个点赞、作废点赞的功用。

[image:B4B41FF2-519A-4A7C-8035-0D5CD4EE8FFA-86900-00013723B2CAE361/8D274601-162D-4B36-B1E0-9C65FB0C494F.png]

假如你对前端轻微有一点相识,你就随手拈来:

HTML:

  <body>
    <div class='wrapper'>
      <button class='like-btn'>
        <span class='like-text'>点赞</span>
        <span>?</span>
      </button>
    </div>
  </body>

为了实际当中的实际状况,所以这里特易把这个 button 的 HTML 组织搞得轻微庞杂一些。有了这个 HTML 组织,如今就给它到场一些 JavaScript 的行动:

JavaScript:

  const button = document.querySelector('.like-btn')
  const buttonText = button.querySelector('.like-text')
  let isLiked = false
  button.addEventListener('click', function () {
    isLiked = !isLiked
    if (isLiked) {
      buttonText.innerHTML = '作废'
    } else {
      buttonText.innerHTML = '点赞'
    }
  }, false)

功用和完成都很简朴,按钮已能够供应点赞和作废点赞的功用。这时刻你的同事跑过来了,说他很喜欢你的按钮,他也想用你写的这个点赞功用。你就会发明这类完成体式格局很致命:你的同事要把全部 button 和内里的组织复制过去,另有整段 JavaScript 代码也要复制过去。如许的完成体式格局没有任何可复用性。

3 完成可复用性

所以如今我们来想要领处置惩罚这个题目,让这个点赞功用具有较好的可复用的效果,那末你的同事们就能够轻松自在地运用这个点赞功用。

3.1 组织复用

如今我们来从新编写这个点赞功用。此次我们先写一个类,这个类有 render 要领,这个要领内里直接返回一个示意 HTML 组织的字符串:

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

然后能够用这个类来构建差别的点赞功用的实例,然后把它们插到页面中。

  const wrapper = document.querySelector('.wrapper')
  const likeButton1 = new LikeButton()
  wrapper.innerHTML = likeButton1.render()
  
  const likeButton2 = new LikeButton()
  wrapper.innerHTML += likeButton2.render()

[image:4AEFC6B6-F913-440E-9306-CCC454A7A30C-87312-00013B98FB6F8354/4555573C-8435-4079-9D64-C76913AB6E40.png]

这里异常暴力地运用了 innerHTML ,把两个按钮粗暴地插进去了 wrapper 当中。虽然你能够会对这类完成体式格局异常不满意,但我们照样勉强了完成了组织的复用。我们背面再来优化它。

3.2 天生 DOM 元素而且增加事宜

你一定会发明,如今的按钮是死的,你点击它它基础不会有什么回响反映。由于基础没有往上面增加事宜。然则题目来了,LikeButton 类内里是虽然说有一个 button,然则这玩意基础就是在字符串内里的。你怎样能往一个字符串内里增加事宜呢?DOM 事宜的 API 只需 DOM 组织才能用。

我们须要 DOM 组织,正确地来讲:我们须要这个点赞功用的 HTML 字符串代表的 DOM 组织。假定我们如今有一个函数 createDOMFromString ,你往这个函数传入 HTML 字符串,然则它会把响应的 DOM 元素返回给你。这个题目就能够额处置惩罚了。

// ::String => ::Document
const createDOMFromString = (domString) => {
  // TODO 
}

先不必管这个函数应当怎样完成,先晓得它是干吗的。拿来用就好,这时刻用它来改写一下 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
    }
  }

如今 render() 返回的不是一个 html 字符串了,而是一个由这个 html 字符串所天生的 DOM。在返回 DOM 元素之前会先给这个 DOM 元素上增加事宜在返回。

由于如今 render 返回的是 DOM 元素,所以不能用 innerHTML 暴力地插进去 wrapper。而是要用 DOM API 插进去。

    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
      if (this.state.isLiked) {
        likeText.innerHTML = '作废'
      } else {
        likeText.innerHTML = '点赞'
      }
    }

    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
    }
  }

这里的代码轻微长了一些,然则照样很好明白。只不过是在给 LikeButton 类增加了组织函数,这个组织函数会给每一个 LikeButton 的实例增加一个对象 statestate 内里保留了每一个按钮本身是不是点赞的状况。还改写了本来的事宜绑定函数:本来只打印 click,如今点击的按钮的时刻会挪用 changeLikeText 要领,这个要领会依据 this.state 的状况转变点赞按钮的文本。

假如你如今还能跟得上文章的思绪,那末你注意下,如今的代码已和 React.js 的组件代码有点类似了。但实在我们基础没有讲 React.js 的任何内容,我们专心致志只想怎样做好“组件化”。

如今这个组件的可复用性已很不错了,你的同事们只需实例化一下然后插进去到 DOM 内里去就好了。

4 为何不暴力一点?

细致注意一下 changeLikeText 函数,这个函数包括了 DOM 操纵,如今看起来比较简朴,那是由于如今只需 isLiked 一个状况。但想一下,由于你的数据状况转变了你就须要去更新页面的内容,所以假如你的组件包括了许多状况,那末你的组件基础全部都是 DOM 操纵。一个组件包括许多状况的状况异常罕见,所以这里另有优化的空间:怎样只管削减这类手动 DOM 操纵?

4.1 状况转变 -> 构建新的 DOM 元素

这里要提出的一种处置惩罚方案:一旦状况发作转变,就从新挪用 render 要领,构建一个新的 DOM 元素。如许做的优点是什么呢?优点就是你能够在 render 要领内里运用最新的 this.state 来组织差别 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 差别而差别(这里是用了 ES6 的字符串特征,做这类事变很轻易)。

  2. 新增一个 setState 函数,这个函数接收一个对象作为参数;它会设置实例的 state,然后从新挪用一下 render 要领。

  3. 当用户点击按钮的时刻, changeLikeText 会构建新的 state 对象,这个新的 state ,传入 setState 函数当中。

如许的效果就是,用户每次点击,changeLikeText 都邑挪用转变组件状况然后挪用 setStatesetState 会挪用 render 要领从新构建新的 DOM 元素;render 要领会依据 state 的差别构建差别的 DOM 元素。

也就是说,你只需挪用 setState,组件就会从新衬着。我们顺遂地消除了没必要的 DOM 操纵。

4.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 元素
component.onStateChange = (oldEl, newEl) => {
  wrapper.insertBefore(newEl, oldEl) // 插进去新的元素
  wrapper.removeChild(oldEl) // 删除旧的元素
}

这里每次 setState 都邑挪用 onStateChange 要领,而这个要领是实例化今后时刻被设置的,所以你能够自定义 onStateChange 的行动。这里做的事是,每当 setState 的时刻,就会把插进去新的 DOM 元素,然后删除旧的元素,页面就更新了。这里已做到了进一步的优化了:如今不须要再手动更新页面了。

非一般的暴力。不过没有关系,这类暴力行动能够被 Virtual-DOM 的 diff 战略躲避掉,但这不是本文章所议论的局限。

这个版本的点赞功用很不错,我能够继续往上面加功用,而且还不须要手动操纵DOM。然则有一个不好的处所,假如我要从新别的做一个新组件,譬如说批评组件,那末内里的这些 setState 要领要从新写一遍,实在这些东西都能够抽出来。

5 笼统出 Component 类

为了让代码更天真,能够写更多的组件,我把这类形式笼统出来,放到一个 Component 类当中:

  class Component {
    constructor (props = {}) {
      this.props = props
    }

    setState (state) {
      const oldEl = this.el
      this.state = state
      this.el = 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
    }
  }

另有一个分外的 mount 的要领,实在就是把组件的 DOM 元素插进去页面,而且在 setState 的时刻更新页面:

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

如许的话我们从新写点赞组件就会变成:

  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'>
          <span class='like-text'>${this.props.word || ''} ${this.state.isLiked ? '作废' : '点赞'}</span>
          <span>?</span>
        </button>
      `
    }
  }

  mount(wrapper, new LikeButton({ word: 'hello' }))

有无发明你写的代码已和 React.js 的组件写法很类似了?而且照样能够一般运作的代码,而且我们从头至尾都是用纯的 JavaScript,没有依靠任何第三方库。(注重这里到场了上面没有提到过点 props,能够给组件传入设置属性,跟 React.js 一样)。

只需有了上面谁人 Component 类和 mount 要领加起来不足40行代码就能够做到组件化。假如我们须要写别的一个组件,只须要像上面那样,简朴地继续一下 Component 类就好了:

  class RedBlueButton extends Component {
    constructor (props) {
      super(props)
      this.state = {
        color: 'red'
      }
    }

    onClick () {
      this.setState({
        color: 'blue'
      })
    }

    render () {
      return `
        <div style='color: ${this.state.color};'>${this.state.color}</div>
      `
    }
  }

简朴好用,完全的代码能够在这里找到: 堆栈

噢,忘了,另有一个神奇的 createDOMFromString,实在它更简朴:

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

6 总结

你究竟从文章能从文章中获取到什么?

好吧,我认可我题目党了,这个 40 行不到的代码实际上是一个残废而且智障版的 React.js,没有 JSX ,没有组件嵌套等等。它只是 React.js 组件化表现情势的一种完成罢了。它基础没有触遇到 React.js 的精华。

实在 React.js 的最最精华的处所能够就在于它的 Virtual DOM 算法,而它的 setStateprops 等等都只不过是一种情势,而许多初学者会被它这类情势作疑惑。本篇文章实在就是揭露了这类组件化情势的完成道理。假如你正在进修或许进修 React.js 历程很渺茫,那末看完这篇文章今后就能够消除一些疑惑。

本文并没有涉及到 Virtual DOM 的任何内容,有须要的同砚能够参考一下这篇博客 ,引见的很详实。有兴致的同砚能够把二者结合起来,把 Virtual DOM 替换本文暴力处置惩罚的 mount 中的完成,真正完成一个 React.js。

假如你对本文的内容有疑惑,能够关注我的知乎专栏而且批评或许给我知乎发私信。

我近来正在写一本《React.js 小书》,对 React.js 感兴致的童鞋,迎接指导

    原文作者:胡子大哈
    原文地址: https://segmentfault.com/a/1190000008472941
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞