40行代码内实现一个React.js

开发 开发工具
本文会教你如何在40行代码内,不依赖任何第三方的库,用纯JavaScript实现一个React.js。

[[186904]]

一、前言

本文会教你如何在 40 行代码内,不依赖任何第三方的库,用纯 JavaScript 实现一个 React.js 。

本文的目的是:揭开对初学者看起来很很难理解的 React.js 的组件化形式的外衣。如果你刚开始学习 React.js 并且感觉很迷茫,那么看完这篇文章以后就能够解除一些疑惑。

另外注意,本文所实现的代码只用于说明教学展示,并不适用于生产环境。代码托管这个 仓库。心急如焚的同学可以先去看代码,但本文会从最基础的内容开始解释。

二、一切从点赞说起

接下来所有的代码都会从一个基本的点赞功能开始演化,你会逐渐看到,文章代码慢慢地越来越像 React.js 的组件代码。而在这个过程里面,大家需要只需要跟着文章的思路,就可以在代码的演化当中体会到组件化形式。

假设现在我们需要实现一个点赞、取消点赞的功能。

如果你对前端稍微有一点了解,你就顺手拈来:

HTML:

<body> 
    <div class='wrapper'> 
      <button class='like-btn'> 
        <span class='like-text'>点赞</span> 
        <span>👍</span> 
      </button> 
    </div> 
  </body> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

 

 

 

为了现实当中的实际情况,所以这里特意把这个 button 的 HTML 结构搞得稍微复杂一些。有了这个 HTML 结构,现在就给它加入一些 JavaScript 的行为:

JavaScript:

const button = document.querySelector('.like-btn') 
  const buttonbuttonText = button.querySelector('.like-text') 
  let isLiked = false 
  button.addEventListener('click', function () { 
    isLiked = !isLiked 
    if (isLiked) { 
      buttonText.innerHTML = '取消' 
    } else { 
      buttonText.innerHTML = '点赞' 
    } 
  }, false) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

功能和实现都很简单,按钮已经可以提供点赞和取消点赞的功能。这时候你的同事跑过来了,说他很喜欢你的按钮,他也想用你写的这个点赞功能。你就会发现这种实现方式很致命:你的同事要把整个 button 和里面的结构复制过去,还有整段 JavaScript 代码也要复制过去。这样的实现方式没有任何可复用性。

三、实现可复用性

所以现在我们来想办法解决这个问题,让这个点赞功能具有较好的可复用的效果,那么你的同事们就可以轻松自在地使用这个点赞功能。

1. 结构复用

现在我们来重新编写这个点赞功能。这次我们先写一个类,这个类有 render 方法,这个方法里面直接返回一个表示 HTML 结构的字符串:

class LikeButton { 
   render () { 
     return ` 
       <button class='like-btn'> 
         <span class='like-text'></span> 
         <span>👍</span> 
       </button> 
     ` 
   } 
 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

然后可以用这个类来构建不同的点赞功能的实例,然后把它们插到页面中。

const wrapper = document.querySelector('.wrapper') 
  const likeButton1 = new LikeButton() 
  wrapper.innerHTML = likeButton1.render() 
 
  const likeButton2 = new LikeButton() 
  wrapper.innerHTML += likeButton2.render() 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

这里非常暴力地使用了 innerHTML ,把两个按钮粗鲁地插入了 wrapper 当中。虽然你可能会对这种实现方式非常不满意,但我们还是勉强了实现了结构的复用。我们后面再来优化它。

2. 生成 DOM 元素并且添加事件

你一定会发现,现在的按钮是死的,你点击它它根本不会有什么反应。因为根本没有往上面添加事件。但是问题来了,LikeButton 类里面是虽然说有一个 button,但是这玩意根本就是在字符串里面的。你怎么能往一个字符串里面添加事件呢?DOM 事件的 API 只有 DOM 结构才能用。

我们需要 DOM 结构,准确地来说:我们需要这个点赞功能的 HTML 字符串代表的 DOM 结构。假设我们现在有一个函数 createDOMFromString ,你往这个函数传入 HTML 字符串,但是它会把相应的 DOM 元素返回给你。这个问题就可以额解决了。

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

  • 1.
  • 2.
  • 3.
  • 4.

先不用管这个函数应该怎么实现,先知道它是干嘛的。拿来用就好,这时候用它来改写一下 LikeButton 类:

class LikeButton { 
    render () { 
      this.el = createDOMFromString(` 
        <button class='like-btn'> 
          <span class='like-text'>点赞</span> 
          <span>👍</span> 
        </button> 
      `) 
      this.el.addEventListener('click', () => console.log('click'), false) 
      return this.el 
    } 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

现在 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()) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

现在你点击这两个按钮,每个按钮都会在控制台打印 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-btn'> 
          <span class='like-text'>点赞</span> 
          <span>👍</span> 
        </button> 
      `) 
      this.el.addEventListener('click', this.changeLikeText.bind(this), false) 
      return this.el 
    } 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

这里的代码稍微长了一些,但是还是很好理解。只不过是在给 LikeButton 类添加了构造函数,这个构造函数会给每一个 LikeButton 的实例添加一个对象 state,state 里面保存了每个按钮自己是否点赞的状态。还改写了原来的事件绑定函数:原来只打印 click,现在点击的按钮的时候会调用 changeLikeText 方法,这个方法会根据 this.state 的状态改变点赞按钮的文本。

如果你现在还能跟得上文章的思路,那么你留意下,现在的代码已经和 React.js 的组件代码有点类似了。但其实我们根本没有讲 React.js 的任何内容,我们一心一意只想怎么做好“组件化”。

现在这个组件的可复用性已经很不错了,你的同事们只要实例化一下然后插入到 DOM 里面去就好了。

四、为什么不暴力一点?

仔细留意一下 changeLikeText 函数,这个函数包含了 DOM 操作,现在看起来比较简单,那是因为现在只有 isLiked 一个状态。但想一下,因为你的数据状态改变了你就需要去更新页面的内容,所以如果你的组件包含了很多状态,那么你的组件基本全部都是 DOM 操作。一个组件包含很多状态的情况非常常见,所以这里还有优化的空间:如何尽量减少这种手动 DOM 操作?

1. 状态改变 -> 构建新的 DOM 元素

这里要提出的一种解决方案:一旦状态发生改变,就重新调用 render 方法,构建一个新的 DOM 元素。这样做的好处是什么呢?好处就是你可以在 render 方法里面使用***的 this.state 来构造不同 HTML 结构的字符串,并且通过这个字符串构造不同的 DOM 元素。页面就更新了!听起来有点绕,看看代码怎么写:

class LikeButton { 
    constructor () { 
      this.state = { isLiked: false } 
    } 
 
    setState (state) { 
      this.state = state 
      thisthis.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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

其实只是改了几个小地方:

  • render 函数里面的 HTML 字符串会根据 this.state 不同而不同(这里是用了 ES6 的字符串特性,做这种事情很方便)。
  • 新增一个 setState 函数,这个函数接受一个对象作为参数;它会设置实例的 state,然后重新调用一下 render 方法。
  • 当用户点击按钮的时候, changeLikeText 会构建新的 state 对象,这个新的 state ,传入 setState 函数当中。

这样的结果就是,用户每次点击,changeLikeText 都会调用改变组件状态然后调用 setState;setState 会调用 render 方法重新构建新的 DOM 元素;render 方法会根据 state 的不同构建不同的 DOM 元素。

也就是说,你只要调用 setState,组件就会重新渲染。我们顺利地消除了没必要的 DOM 操作。

2. 重新插入新的 DOM 元素

上面的改进不会有什么效果,因为你仔细看一下就会发现,其实重新渲染的 DOM 元素并没有插入到页面当中。所以这个组件之外,你需要知道这个组件发生了改变,并且把新的 DOM 元素更新到页面当中。

重新修改一下 setState 方法:

... 
    setState (state) { 
      const oldEl = this.el 
      this.state = state 
      thisthis.el = this.render() 
      if (this.onStateChange) this.onStateChange(oldEl, this.el) 
    } 
... 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

使用这个组件的时候:

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

这里每次 setState 都会调用 onStateChange 方法,而这个方法是实例化以后时候被设置的,所以你可以自定义 onStateChange 的行为。这里做的事是,每当 setState 的时候,就会把插入新的 DOM 元素,然后删除旧的元素,页面就更新了。这里已经做到了进一步的优化了:现在不需要再手动更新页面了。

非一般的暴力。不过没有关系,这种暴力行为可以被 Virtual-DOM 的 diff 策略规避掉,但这不是本文章所讨论的范围。

这个版本的点赞功能很不错,我可以继续往上面加功能,而且还不需要手动操作DOM。但是有一个不好的地方,如果我要重新另外做一个新组件,譬如说评论组件,那么里面的这些 setState 方法要重新写一遍,其实这些东西都可以抽出来。

五、抽象出 Component 类

为了让代码更灵活,可以写更多的组件,我把这种模式抽象出来,放到一个 Component 类当中:

class Component { 
    constructor (props = {}) { 
      this.props = props 
    } 
 
    setState (state) { 
      const oldEl = this.el 
      this.state = state 
      thisthis.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 
    } 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

还有一个额外的 mount 的方法,其实就是把组件的 DOM 元素插入页面,并且在 setState 的时候更新页面:

const mount = (wrapper, component) => { 
    wrapper.appendChild(component.renderDOM()) 
    component.onStateChange = (oldEl, newEl) => { 
      wrapper.insertBefore(newEl, oldEl) 
      wrapper.removeChild(oldEl) 
    } 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样的话我们重新写点赞组件就会变成:

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' })) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

有没有发现你写的代码已经和 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> 
      ` 
    } 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

简单好用,完整的代码可以在这里找到: React.js in 40

噢,忘了,还有一个神秘的 createDOMFromString,其实它更简单:

const createDOMFromString = (domString) => { 
    const div = document.createElement('div') 
    div.innerHTML = domString 
    return div 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

六、总结

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

好吧,我承认我标题党了,这个 40 行不到的代码其实是一个残废而且智障版的 React.js,没有 JSX ,没有组件嵌套等等。它只是 React.js 组件化表现形式的一种实现而已。

React 的 setState 、props 等等都只不过是一种形式,而很多初学者会被它这种形式作迷惑。本篇文章其实就是揭露了这种组件化形式的实现原理。如果你正在学习或者学习 React.js 过程很迷茫,那么看完这篇文章以后就能够解除一些疑惑。

点击《40 行代码内实现一个 React.js》阅读原文。

【本文是51CTO专栏作者“胡子大哈”的原创文章,转载请联系作者本人获取授权】

戳这里,看该作者更多好文

责任编辑:赵宁宁 来源: 51CTO专栏
相关推荐

2022-04-15 08:07:21

ReactDiff算法

2020-11-30 06:18:21

React

2024-03-20 09:31:00

图片懒加载性能优化React

2025-01-13 00:00:00

2020-04-27 14:54:45

React开发

2025-01-17 09:29:42

2022-06-08 08:03:51

React.jsReactJS 库

2022-06-29 09:02:31

go脚本解释器

2022-01-26 16:30:47

代码虚拟机Linux

2017-02-09 15:19:14

2018-06-21 16:03:25

Vue.jsReact.js框架

2015-12-31 10:14:54

React.js开发Web应用

2019-11-15 15:50:41

JS代码React前端

2022-06-28 08:17:10

JSON性能反射

2022-02-08 12:30:30

React事件系统React事件系统

2021-12-16 06:21:16

React组件前端

2016-11-14 15:51:42

JavaScriptAngular.jsReact.js

2023-02-07 13:42:44

代码实现并发

2022-06-06 09:28:36

ReactHook

2023-07-03 07:51:47

点赞
收藏

51CTO技术栈公众号