ITPub博客

首页 > 应用开发 > Javascript > 深入浅出 React:探究 JSX 本质

深入浅出 React:探究 JSX 本质

原创 Javascript 作者:沃趣科技微课堂 时间:2021-09-16 14:52:20 0 删除 编辑

前言

JSX(JavaScript XML)是学习React框架必须要了解的一个概念, 虽然现在越来越多的人认可与支持 JSX 语法的框架(比如 Vue.js),但是当 React 首次带着 JSX 发布之时,还是引起了很多争议,甚至我的一个朋友大呼这是开历史倒车。随着 React 框架的不断完善,JSX 也被越来越多的开发者所认可,变得越来越流行,当得了一句“真香”!


本次分享会从下面三个问题出发:

  • JSX 是什么?

  • JSX 作用是什么?

  • JSX 背后做了哪些事情?

在分享的过程中,我会通过一些 Demo 代码讲解以及解读源码的方式尝试着去寻找答案,希望通过这样的方式帮助你认识 JSX,并从根本上理解 JSX,从而帮助我们写出更好的 React 代码。


JSX 的本质:JavaScript的语法扩展

我们先看一下 React 官网是如何定义 JSX 的:

■ JSX 是一个 JavaScript 的语法扩展,它看起来像是一种模板语言,但它具有 JavaScript 的全部功能。


刚开始接触 JSX 的同学一般会认为这是一种新的模板语言,因为它与一般的模板语言非常相像。下面我们通过三段作用相同的代码来直观的感受一下:

// 定义一些数据
const title = 'hello'
const list = ['a', 'b', c]
const visible = true
const footCssName = 'footer'


Vue 模板:

<div className="app">  
<h1 className="title">{{ title }}</h1>  
<section className="content">    
<p v-for="item in list">{{ item }}</p>  
</section>  
<footer v-if="visible" :class="footCssName">foot</footer>
</div>


EJS 模板:

<div class="app">  
<h1 class="title"><%= title %></h1>  
<section class="content">    
<% list.forEach(function(item) { %>      
<p><%= item %></p>    
<% }) %>  
</section>  
<% if (visible) { %>    
<footer class="<%= footCssName %>">foot</footer>  <% } %>
</div>


JSX语法:

<div className="app">  
<h1 className="title">{title}</h1>  
<section className="content">    
{list.map((item) => <p key={item}>{item}</p>)}  
</section>  
{visible && (    
<footer className={footCssName}>foot</footer>  
)}
</div>


实际上 JSX 并不是模板语言,而是对 JavaScript 语言的扩展。JSX 基于 JavaScript 语言,所以它具备 JS 的全部能力(JS 能做的 JSX 都可以做,比如表达式计算),但又新增了某些能力(JS 不能做的 JSX 也可以做,比如自定义组件)。


既然说 JSX 是 JavaScript 的扩展,那么我们的浏览器就没办法解析原始的 JSX 代码,因为不认识,那么 JSX 是如何生效的呢?其实 React 官网已经给出了答案(证明官网文档是值得反复研读的)


■ JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。



JSX 需要被编译,而编译这个动作就是由  Babel 来完成的。


Babel

■ Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

// 编译前
const firstName = 'Dan'
const lastName = 'Abramov'
conso.log(`hello ${firstName} ${lastName} !`)
// 编译后
var firstName = "Dan";
var lastName = "Abramov";
conso.log("hello ".concat(firstName, " ").concat(lastName, " !"));


更多的内容可以通过访问 Babel 官网进行学习了解。我们只需要知道,Babel 就是一个转译工具,它具备把 JSX 语法转换为 JavaScript 代码的能力。

// https://babeljs.io/repl/
<div className="app">  
<h1 className="title">{title}</h1>  
<p>content</p>
</div>


通过 Babel REPL 我们可以看到,JSX 中标签都被转换成了  React.createElement 函数调用,也就是说其实写 JSX 就是在写  React.createElement 函数,换句话说就是,JSX 就是  React.createElement 函数调用的语法糖。



JSX 的作用:更好的 React 使用体验

既然 JSX 只是  React.createElement 函数的语法糖,那么为什么不直接使用 React.createElement 来编写代码呢,这样还能省一个 Babel 编译的步骤。为了直观的理解使用 JSX 的好处,我们还是看下面的代码:

<div className="app"> 
 <div className="app__one"> 
    One text 
       <div className="app__two"> 
            Second text 
                 <div className="app__three"> 
                        <h2>Three text</h2> 
                             </div> 
                                  <div className="app__four"> 
                                         <p>Four text</p> 
                                              </div> 
                                                 </div> 
                                                  </div> 
                                                     <Hello name="woqutech" />
                                                     </div>
                                                     // ===>
                                                     // /*#__PURE__*/ 标记是 Babel 工具加上的
                                                     // 主要用处就是告诉后面的代码压缩插件,此处代码是无副作用的代码,可以放心优化(删除)
                                                     // 具体可以看:https://laysent.com/til/2019-12-24___pure__
                                                     /*#__PURE__*/
                                                     React.createElement( 
                                                      'div',
                                                        { 
                                                           className: 'app', 
                                                            }, 
                                                      /*#__PURE__*/ React.createElement( 
                                                         'div',
                                                             {
                                                                   className: 'app__one', 
                                                                      }, 
                                                                         'One text', 
                                                         /*#__PURE__*/ React.createElement( 
                                                              'div', 
                                                                   { 
                                                                          className: 'app__two', 
                                                                               }, 
                                                                                    'Second text', 
                                                              /*#__PURE__*/ React.createElement( 
                                                                     'div', 
                                                                            { 
                                                                                     className: 'app__three', 
                                                                                            }, 
                                                                                                   /*#__PURE__*/ React.createElement('h2', null, 'Three text') 
                                                                                                        ), 
                                                                                                             /*#__PURE__*/ React.createElement( 
                                                                                                                    'div',
                                                                                                                            {          className: 'app__four',        }, 
                                                                                                                                   /*#__PURE__*/ React.createElement('p', null, 'Four text') 
                                                                                                                                        )
                                                                                                                                            )
                                                                                                                                              ),
                                                                                                                                                /*#__PURE__*/ React.createElement(Hello, { name: "woqutech" }))


React 框架有一个著名的公式:UI=render(data),这个公式也是 React 理念的体现。用户看到的界面(UI),应该是一个函数(render)的执行结果,这个函数只接受数据(data)作为参数,且这个函数是一个 纯函数(无副作用,输出完全依赖于输入,相同的输入一定得到相同的输出)。


React 认为渲染逻辑本质上与其他 UI 逻辑是内在耦合的,比如说在 UI 中绑定处理时间,触发回调更新数据,以及在 UI 中展示更新后的数据等。如果仅仅通过纯 JavaScript 代码来描述 UI,那么在复杂的 UI 界面下,写出来的代码可读性非常差,只有使用 JSX 这种基于 JavaScript 语言并具备 标记语言(Markup Language)特性的特殊语法,让我们使用最为熟悉的 HTML(HyperText Markup Language,超文本标记语言) 标签语法来创建虚拟 DOM,才能在降低学习成本的同时,提高代码可读性(层次分明,嵌套关系清晰)以及编码效率(借助工具自动补全及错误提示)之间取得平衡,这也是 JSX 越来越流行的一个重要原因。



JSX 背后的处理:创建虚拟 DOM 节点

前文提到 JSX 是 React.createElement 函数的语法糖,那么 React.createElement 函数又是做什么的呢?让我们从 React 源码中一探究竟。

注意:本文摘取的 React 源码都来自于 16.8.6 版本。


·小技巧

在 github repo 页面,通过单击键盘上的 . 键,可以启动在线 IDE 模式,实现更好的代码阅读体验。

// packages/react/src/ReactElement.js
// createElement 有 3 个入参,这 3 个入参包含了 React 创建一个元素所需要知道的全部信息
// type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型
// config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中
// children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”
export function createElement(type, config, children) {  
// propName 变量用于储存后面需要用到的元素属性  
let propName  
// props 变量用于储存元素属性的键值对集合  
const props = {}  
// key、ref、self、source 均为 React 元素的属性  
let key = null  let ref = null  let self = null  let source = null  
// config 对象中存储的是元素的属性  
if (config != null) {    
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值    
if (hasValidRef(config)) {      
ref = config.ref      
// ...    
}    
if (hasValidKey(config)) {      
key = '' + config.key    
}    
self = config.__self === undefined ? null : config.__self    
source = config.__source === undefined ? null : config.__source    
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面    
for (propName in config) {      
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {        props[propName] = config[propName]      
}    
}  
}  
// childrenLength 指的是当前元素的子元素的个数  
// 减去的 2 是 type 和 config 两个参数占用的长度  
const childrenLength = arguments.length - 2  
// 如果只剩下一个参数,那么一般就是 文本节点  
if (childrenLength === 1) {    
// 直接把这个参数的值赋给 props.children    
props.children = children  } else if (childrenLength > 1) {    
// 声明一个子元素数组    
const childArray = Array(childrenLength)    
// 把子元素推进数组里    
for (let i = 0; i < childrenLength; i++) {      
childArray[i] = arguments[i + 2]    
}    
// ...    
// 最后把这个数组赋值给 props.children    
props.children = childArray  }  
// 处理 defaultProps  
if (type && type.defaultProps) {    
const defaultProps = type.defaultProps    
for (propName in defaultProps) {      
if (props[propName] === undefined) {        
props[propName] = defaultProps[propName]      
}    
}  
}  
if (__DEV__) {    
// ...  }  
// 最后返回一个调用 ReactElement 执行方法,并传入刚才处理过的参数  
// ReactCurrentOwner 是虚拟 DOM 中识别自定义组件的关键  
// 具体可以看:https://que01.top/2019/06/28/react-ReactCurrentOwner/  
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
}



createElement函数做的事情其实不复杂,简单来说就是 格式化数据,给ReactElement准备参数,算是一个数据处理层,真正“干活”的还是 ReactElement。

// packages/react/src/ReactElement.js
const ReactElement = function (type, key, ref, self, source, owner, props) {  
const element = {    
// REACT_ELEMENT_TYPE 是一个常量,用来标识该对象是一个 ReactElement    
$$typeof: REACT_ELEMENT_TYPE,    
// 内置属性赋值    
type: type,    
key: key,    
ref: ref,    
props: props,    
// 记录创造该元素的组件    
_owner: owner,  
}  
if (__DEV__) {    
// ...  
}  
return element
}


ReactElement 函数的代码比较简短,除去 __DEV__ 处理的逻辑代码基本就没有别的内容,从实际代码来看,这里做的事情就是 按照一定的规范组装 element 对象,并且返回这个组装好的对象。



这里的 ReactElement 对象又是什么呢?其实它本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是我们经常说的虚拟 DOM(准确说应该是虚拟 DOM 中的一个节点)。虚拟 DOM 和真实的 DOM 还是有一定的差别,而这个差别就是靠 ReactDOM.render 方法来填补。


// ReactDOM.render(<App />, document.getElementById('root'))
ReactDOM.render(  
// 需要渲染的元素(ReactElement)  
element,  
// 元素挂载的目标容器(一个真实 DOM 节点)  
container,  
// 回调函数,可选参数,可以用来处理渲染结束后的逻辑  
[callback]
)

至此,整个 JSX 到真实 DOM 的流程我们就算走完了,而通过解读源码我们也能看到这块内容其实比较简单,对于我们理解 JSX 没有障碍。




总结

通过阅读本文,我相信大家应该能轻松容易回答开篇提出的 3 个问题了,也欢迎大家把自己的心得或疑问写在评论区,方便我们进一步讨论。


后面我们会有一系列关于 React 框架的文章,请大家持续关注~




参考资料

https://babeljs.io/docs/en/babel-preset-react

https://laysent.com/til/2019-12-24___pure__

https://que01.top/2019/06/28/react-ReactCurrentOwner/


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31545952/viewspace-2792401/ ,如需转载,请注明出处,否则将追究法律责任。

上一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2018-07-23

  • 博文量
    2
  • 访问量
    587