文章名称来自豆瓣高分电影列表, 电影名为《这个杀手不太冷》

前言

我们假设你有一定的前端知识,简单使用过React但不了解他的原理以及为什么要用它。

通过这篇文章你会了解到这些问题的答案:

  1. 为什么你自己写的组件要首字母大写?

  2. 为什么能在js文件里写html的语法?

  3. 为什么jsx的方式要比模板强?

  4. 为什么jsx必须要有一个顶层节点?

  5. 为什么jsx中不能用class要用className?

  6. 为什么react-dom要单独作为一个库?

  7. 为什么我即使没在这个js中用到React也需要引入React这个库?

看文章之前请记住:
在js中的所有东西都是js。


什么是jsx?

首先上一段我们熟悉的jsx代码, 这种在js中写类似html代码的方式就是jsx。

  1. <MyButton color="blue" shadowSize={2}>
  2. Click Me
  3. </MyButton>

其实jsx是React.createElement(component, props, …children) 函数的语法糖。这段代码编译之后就成了:

  1. React.createElement(
  2. MyButton,
  3. {color: 'blue', shadowSize: 2},
  4. 'Click Me'
  5. )

那么为什么是这三个参数呢?

仔细观察一下熟悉的DOM代码

  1. <div class='box' id='content'>
  2. <div class='title'>Hello</div>
  3. <button>Click</button>
  4. </div>

想一下如何用 JavaScript 对象来表现一个 DOM 元素的结构

  1. {
  2. tag: 'div',
  3. attrs: { className: 'box', id: 'content'},
  4. children: [
  5. {
  6. tag: 'div',
  7. arrts: { className: 'title' },
  8. children: ['Hello']
  9. },
  10. {
  11. tag: 'button',
  12. attrs: null,
  13. children: ['Click']
  14. }
  15. ]
  16. }

你会发现每个DOM结构都可以通过DOM名称, DOM属性,DOM子元素来表示,DOM一层套一层形成了DOM树。


那么为什么不直接在js中写UI呢?

你比较下html方式的UI表示和js方式的UI表示的代码行数你就知道了:

  1. 太长

  2. 不清晰

所以React用jsx语法让我们能在js中可以用HTML的方式描述UI。但这坨代码肯定不是标准的js是吧,直接运行肯定是不行的,所以需要编译。其实就是树的递归调用啊…想想你写斐波那契数列程序的时候画的图。

  1. <div>
  2. <h1 className='title'>React 小书</h1>
  3. </div>
  4. =>
  5. React.createElement(
  6. "div",
  7. null,
  8. React.createElement(
  9. "h1",
  10. { className: 'title' },
  11. "React 小书"
  12. )
  13. )
  14. )

那么生成的代码怎么变成真的DOM?

光是生成了一堆函数调用并不是真正的DOM,它只是包含了生成一个DOM树所需要的所有信息,我们称它为VDOM(虚拟DOM)。那么用什么东西把它变成真正的DOM呢? 想一下你每次是怎么引入React的?

  1. import React, { Component } from 'react'
  2. import ReactDOM from 'react-dom'
  3. class Header extends Component {
  4. ...
  5. }
  6. ReactDOM.render(
  7. <Header />,
  8. document.getElementById('root')
  9. )

除了基本的React库,用于扩展的Component组件基类, 这个react-dom是什么? ReactDOM用于渲染组件并构造DOM树,然后插入到页面上的某个挂载点上。


那么为什么不把这玩意和react库封装在一起?

因为这个UI信息并不一定要渲染到网页上,比如渲染到Canvas上,渲染到手机上。

总结下过程:

  1. // JSX -> VDOM: 将jsx编译成vdom
  2. let vdom = <div id="foo">Hello!</div>;
  3. // VDOM -> DOM: 将vdom渲染成dom
  4. let dom = render(vdom);
  5. // add the tree to <body>: 将dom插入到挂载点
  6. document.body.appendChild(dom);

那么为什么非要一层VDOM呢?

因为原生的DOM操作非常费时, 和 DOM 操作比起来,js 计算是极其便宜,有了Vdom之后我们可以直接在这个js对象上操作,而不用直接与DOM打交道。这样的话可以减少浏览器的重排,极大的优化性能。React会在每次数据变化之后更新DOM,但不是更新真实的DOM,而是存在内存中的JS对象,不管你数据怎么变化,React都可以以最小的代价来更新 DOM。

虚拟DOM是React的一个非常重要的概念,不同的类React框架中对虚拟DOM的实现有差异,造成了其性能的千差万别。VDOM比较复杂,这期不介绍。


等等,我还有几个个小问题

  • 为什么我即使没在这个js中用到React也需要引入React这个库?
    答: 还记得在文章开头说过的在js中的所有东西都是js吗? 只要你在这个js文件中用到了jsx语法,那么这个jsx会被翻译成React.createElement()的形式,你看如果你不引入React库,这段代码能执行吗?

  • 不对啊,有时候我不需要引入这两个库也能用React,怎么解释?
    答: 那是因为你把react和react-dom放在html文件的标签中了, React成为全局变量

  • 为什么React的组件名一定要大写?
    答: 因为普通的html标签都是小写的, div, a, p,那么React如何区分是已有的HTML标签还是用户自定义的组件呢?就是首字母大小写, 如果你小写你的组件名称,react会把它当原生html标签,然后报错因为找不到

  • 为什么组件必须要有一个顶层节点?
    答:React15以下组件需要包一个顶层节点,否则会报Adjacent XJS elements must be wrapped in an enclosing tag 的错,为什么呢? 再复习一遍在js当中所有东西都是js ,并列的两个tag 会渲染成什么样子?React.createElement(...) React.createElement(...) 并不符合语法,但如果做成数组形式返回其实是可以的,因此React16中终于支持了返回数组的写法。这个问题的issue14年就已提出来了,有兴趣的同学可以研究一下。

  1. render() {
  2. return [
  3. <li key="A">First item</li>,
  4. <li key="B">Second item</li>,
  5. <li key="C">Third item</li>,
  6. ];
  7. }

为什么要用jsx?

说了那么多,我还是觉得jquery还有vm模板好用,那么我为什么要迁移到React + jsx中来呢?
=> 因为一方面用jsx+React我们可以使用js的所有语法和能力,而使用模板引擎通常只能使用其提供的有限的模板语法。

举个栗子🌰,循环列表,在vm中我们只能用这样的语法写:

  1. <ul>
  2. #foreach ( $product in $allProducts )
  3. <li> $product </li>
  4. #end
  5. </ul>

而在jsx中, 我们可以:

  1. // 用map写
  2. let list = items => items.map( p => <li> {p} </li> );
  3. // 用循环写
  4. let list = [];
  5. for (let i = 0 ; i < items.length: i++) {
  6. list.push(<li>{items[i]}</li>);
  7. }
  8. // 用forEach写
  9. let list = [];
  10. items.forEach(item => list.push(<li>{item}</li>))
  11. // 用while写
  12. // 用for...of写
  13. // ...

总之你爱怎么写就怎么写。这极大地拓展了前端写界面的能力,前端同学心里美滋滋。

另一方面jsx结合React能发挥它前端组件化的优势,提高代码复用率,避免手动操作DOM等,这里不赘述。


等下你这些代码里的{}是什么? 为什么不直接在{}里写循环套jsx

这个是jsx提供给你插入表达式的,包括变量,表达式计算,函数调用都可以放在里面。如何判断是否是表达式? 看他可不可以放在等于号右边。

所以if, for这样的就不是表达式了,所以我们也不能在{}中写for循环和if判断,但我们可以把结果暂存到变量中,然后插入到{}中,或者用?表达式啊。

另外这个表达式不仅可以用在标签内部,还能用在标签的属性上,属性props是jsx的一个重要概念。


props?属性? 为什么需要属性

首先我们聊聊为什么要组件化,组件化是为了代码的复用和分治。比如你的同事写了一个红色的按钮组件,有了React,我只要引入一下就可以用到我的工程里了,太棒了。但是等等,我的工程里需要的是一个蓝色的按钮,难道我要去改组件的代码?我希望我可以传一个颜色参数进去改变组件的样式,而这个需求的实现方式就是props。你可以理解为函数的参数,传入不同的参数,输出不同的值,没有参数的函数功能太过限定了。

所以 props的作用是让外部能对组件自己进行配置。

  1. <div className="sidebar" color="red"/>

注意props一旦传入到组件中,它就是只读的,不可以再赋值。如果 props 渲染过程中可以被修改,那么就会导致这个组件显示形态和行为变得不可预测,这样会可能会给组件使用者带来困惑


咦,这个className是什么鬼,为什么不用class?

让我们再来复习一下开篇的话: 在js中的所有东西都是js。当然不止class,同样的还有htmlFor替代for。

  1. React.createElement(
  2. MyButton,
  3. {class: 'blue', shadowSize: 2},
  4. 'Click Me'
  5. )

通常的说辞是class是js的保留字。不过翻译成js好像没什么问题? 即使class是js的保留词依然可以用在属性语法中才对,只是不能做变量标识。是的,就有类react框架preact直接用的class,详见issue,甚至还有自动用babel帮我们做转换的jsx-html-class

React中用className的原因参考quora,总结一下原因有两点:

  1. 我们在操作html属性的时候一般用的是el.className=...,而不是el.setAttribute('class', ...),Attributes一般赋值字符串,而属性名可以赋值对象,更灵活。所以jsx中的className和HMTL的className属性表现一致,没毛病

  2. 未来react可能会用…来解构this.props,这时候class和for作为保留字不能作为变量标识,就不能适用这种情况了。


求带啊,有没有其他React技巧

来来来,新鲜的React技巧便宜卖,10元一条,请扫我的付款码…好吧,其实都是jsx-in-depth里的。

  • 属性可以用字符串字面量, 会进行解码
  1. <MyComponent message="&lt;3" />
  2. ===
  3. <MyComponent message={'<3'} />
  • 如果你不给属性赋值,那么默认值为true。这和html中的语义是一样的,但不建议使用,还是下面的直观
  1. <MyTextBox autocomplete />
  2. ===
  3. <MyTextBox autocomplete={true} />
  • 善用…,如果属性在对象中,解构对象轻松搞定
  1. function App2() {
  2. const props = {firstName: 'Ben', lastName: 'Hector'};
  3. return <Greeting {...props} />;
  4. }
  • 布尔值,null,undefined作为子组件不会渲染,如果你想渲染这些值可以先转成string,如{String(myVariable)}。这个可以用来做条件渲染,注意showHeader得是一个boolean值,不能是0这样的伪false值。
  1. <div>
  2. {showHeader && <Header />}
  3. <Content />
  4. </div>

jsx深度剖析

to be continued 😃

参考文章