重学React(一):JSX语法

501 阅读2分钟

前言:

笔者上一次系统学习React还要追溯到2020年下半年,当时还在读大四,学了基础部分写了个demo项目就没有后续了...实习的时候用的是Vue,毕业后大部分时间写的是Angular,但作为前端三大框架之一的React在国内外的影响力是有目共睹的,尤其是颇受国内一众大厂的青睐,故而掌握该框架对于一个前端工程师来说是相当有必要的。

1. JSX语法基础

JSX:javascript and xml(html),意即 js 和 html 混在一起,然后构建视图。

xml是可扩展的标记语言,html是xml的一种,叫超文本标记语言。html是按照W3C官方规范的文本标记来搭建的,xml是可以按照自己规定的标签来搭建一些结构。

下面是React18的语法:

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
       <div></div>
    </React.StrictMode>
);

ReactDOM.createRoot()的作用是指定哪个容器作为所有编译内容要渲染的那一个容器,和Vue中的$mounted一样的效果。它会返回一个实例,而这个实例会提供一个render方法,该方法可以将我们所写的所有的jsx语法经过虚拟DOM和dom diff最后变成真实DOM

React18以前的写法:

ReactDOM.render(视图,容器,回调函数)

ReactDOM里直接有render方法,有三个参数,对应分别是视图、容器和回调函数。当视图的内容全部编译完放到容器当中变成真实DOM,已经看到效果后会触发回调函数执行。而React18当中则是将指定容器和渲染分开了。

<React.StrictMode></React.StrictMode>让视图构建编译的时候,按照严格模式进行处理,会检测一些弃用/不建议的语法。

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <div>
      <div></div>
      <div></div>
    </div>
);

image.png

为了保证只能有一个根结点,要多一层div来包裹(👆),这样层级结构就多了一层,这种情况下是不利于seo优化的,也不利于视图的编译。

那怎样能做到鱼和熊掌兼得呢?我们可以用空标签来取代最外层包裹的div,这在React中叫做 Fragment。而在Vue3中,一个template视图当中可以出现多个根节点,其实现原理也是类似于React的 Fragment 在内部当中构建的。

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <>
      <div></div>
      <div></div>
    </>
);

或者写成👇:

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.Fragment>
      <div></div>
      <div></div>
    </React.Fragment>
);

通过这两种做法,在查看元素的时候就会发现没有多出的一层html结构了。

image.png

要将数据放在视图中渲染的话,vue中用的是大胡子语法,即 {{}}。而在React中用的是小胡子语法,即{}。{}里只能放js表达式(执行有返回结果),判断的话一般写三元表达式,循环的话一般用map。

{} 中可以渲染出来的值:

  • 基本数据类型:只渲染字符串和数字,其余类型的值渲染为空
  • 对象数据类型:
    • 数组对象:可以进行渲染,而且不是转换为字符串(每一项之间没有逗号分隔),它会逐一迭代数组每一项,把每一项都拿出来单独进行渲染
    • 函数对象:可以作为函数组件进行渲染,要写成 <Component/ > 这种格式
    • 其他对象:一般都是不可以直接进行渲染的
      • 可以是一个JSX对象
      • 如果设置的是style样式,则样式值必须写为对象格式
      • ...

eg:

let title = 'hello world!';
let total = 10;
let arr = [
  {
    id: 1,
    name: 'hebe'
  },
  {
    id: 2,
    name: 'Taylor Swift'
  }
];
let obj = {
    name: 'hebe',
    age: 18
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( 
   <>
      <div classname="box">{title}</div>
      <div style={{
        color: 'red',
        fontSize: '14px'
      }}>
        {total > 10 ? 'YES' : 'NO'}
      </div>
      { React.createElement('span') }
      <ul>
          {arr.map(item => {
              let { id, name } = item;
              return <li key={id}>
                  {name}
              </li>;
          })}
      </ul>
      {Reflect.ownKeys(obj).map((key, index) => {
          let value = obj[key];
          return <span key={index}>
              {key} : {value}
          </span>
      })}
   </>
);

和Vue一样,循环绑定的元素都要加唯一的key值,想绑定字符串类型的直接写字符串就行了,如果是一个变量or数字类型,就要用{}包裹起来。注意最好不要用索引作为唯一值。

那怎样去循环对象呢?这就要用到 Reflect 这个内置的对象,它是ES6中为了操作对象而提供的新 API。Reflect.ownKeys()中传入一个对象,可以获取该对象的所有私有属性并以数组形式返回。接下来在用map去循环这个数组就行了。

在JSX语法中,class这个属性是不被识别的,给元素设置类名要用 calssname。给元素设置样式,样式值对应的不是字符串,必须是一个对象,上面demo中关于style的两个大括号和Vue中的大胡子语法不一样,这里的最外层的大括号是胡子语法,用来渲染值的,里面的大括号是表示对象。

总结

JSX具有以下几个特点:

  • 最外层只能有一个根元素节点
  • <></> fragment空标记,即能作为容器把一堆内容包裹起来,还不占层级结构
  • 动态绑定数据使用{},大括号中存放的是JS表达式 => 可以直接放数组:把数组中的每一项都呈现出来 => 一般情况下不能直接渲染对象 => 但是如果是JSX的虚拟DOM对象,是直接可以渲染的
  • 设置行内样式,必须是 style={{color:'red'...}};设置样式类名需要使用的是className;
  • JSX中进行的判断一般都要基于三元运算符来完成
  • JSX中遍历数组中的每一项,动态绑定多个JSX元素,一般都是基于数组中的map来实现的=>和vue一样,循环绑定的元素要设置key值(作用:用于DOM-DIFF差异化对比)

另外,JSX语法具备过滤效果(过滤非法内容),有效防止XSS攻击。

2.JSX和template语法的对比

我们可以通过一个案例来比较下这两种语法的区别:

现有需求,需要通过一个数字类型的变量来创建标签,变量是几我们就创建h几标签。

这个需求在Vue的template语法中该怎么处理呢?

<template>
    <div>
        <h1 v-if="level === 1">标题1</h1>
        <h1 v-else-if="level === 2">标题2</h1>
        <h1 v-else-if="level === 3">标题3</h1>
        <h1 v-else-if="level === 4">标题4</h1>
        <h1 v-else-if="level === 5">标题5</h1>
        <h1 v-else>标题6</h1>
    </div>
</template>

<script>
export default {
  name: 'App',
  data() {
      return {
          level: 2
      }
  }
}
</script>

这样写的话得要事先将所有的视图构建好,通过当前状态值控制哪一块元素渲染哪一块元素不渲染,写起来比较麻烦。那如果用JSX语法怎么来写这个需求呢?

let level = 2;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <>
      { React.createElement(`h${level}`, null, `标题${level}`) }
    </>
);

这样写起来就比Vue的template语法更加简洁一些。

在构建更复杂的视图的时候,用JSX语法会更好一些

那面对这种场景,Vue就没有更好的方式去解决嘛?答案是有的!Vue 官方也知道 template 语法这种弱编程性的特点,所以在2.5版本之后Vue也融入了JSX语法。因此,现在在Vue项目中既可以使用template语法,也可以使用JSX语法来构建视图。

接下来,我们可以来尝试下在Vue中怎么去使用JSX语法:

首先,在 main.js 中注册一个全局组件,取名 TextDemo。这里构建js语法使用的是render这个方法,h 其实就是 createElement

Vue.component('TestDemo', {
  data() {
      return {
          level: 2
      };
  },
  render(h) {
      return h(`h${this.level}`, null, [`标题${this.level}`)]);
  }
});

然后,我们在App.vue的template中,用 TextDemo 这个组件去替换之前写的那些判断逻辑即可。

<template>
    <div>
        <TestDemo />
    </div>
</template>

总结:Vue中的template语法构建视图,编程性比较弱,体现的特点就是灵活性比较差;React中的JSX语法具备很强的编程性,灵活性更好。

3.JSX底层渲染机制

使用JSX语法构建好视图之后,放在页面时肯定是变成了真实DOM来进行渲染的,那么具体是怎么实现的呢?这就需要了解JSX底层渲染机制。

(1) 基于 babel-preset-react-app 语法包,将 jsx 语法渲染解析为 React.creatElement 格式

babeljs.io/ 中写好视图,通过 babel-preset-react-app 语法包变成右边的 React.creatElement 格式

image.png

将👆右边编译过的代码copy下来并整理:

"use strict";
React.createElement(
   "div",
   { classname: "box" },
   React.createElement(
     "h2",
     { className: "title" },
     "\u6807\u9898\u4E8C"
   ),
   React.createElement(
     "ul",
     {
       className: "list",
       style: {
         color: "red",
       },
     },
     React.createElement("li", null, "origin"),
     React.createElement("li", null, "banana"),
     React.createElement("li", null, "apple")
   )
);

遇到 html 标签,就会变成 createElement 格式,其第一个参数就是标签名/组件,标签上设置的各种属性放到第二个参数对象当中(如果没有任何属性,值是 null),接下来的参数就是该节点的子节点的信息,以此类推...(ps: 文本也是子节点)

(2)执行 React.createElement() 方法生成一个对象,我们将这个对象称为 jsx元素对象,亦或虚拟DOM对象/React child

变为 creatElement 格式之后,执行 React.createElement() 方法,将返回结果用一个变量接收并打印后得到:

image.png

{
    $$typeofSymbol(react.element),  // 证明当前是一个React元素
    type"div",  // 标签名/组件
    props: {}, // 含解析出来的各个属性,如果有子节点,则多一个 children 的属性,没有子节点及就没有这个属性。属性值可能是一个值或者是一个数组
    ref: null, // 获取DOM元素的属性
    key: null
}

(3)root.render() 把虚拟DOM对象转换为真实的DOM对象,放在浏览器中进行渲染

4.手写JSX底层渲染机制

这次手写主要实现上述JSX底层渲染机制中的(2)、(3)步即可

// 创建JSX元素对象
import React from "react";
const createElement = function createElement(type, props, ...children) {
    let len = children.length,
        virtualDOM = {
            $$typeofSymbol(React.element),
            type,
            props: {}
        };
    if (props !== null) virtualDOM.props = { ...props };
    if (len === 1) virtualDOM.props.children = children[0];
    if (len > 1) virtualDOM.props.children = children;
    return virtualDOM;
};

// 迭代对象
const each = function each(obj, callback) {
    if (typeof obj !== 'object') throw new TypeError('obj is not a object');
    if (typeof callback !== 'function') throw new TypeError('callback is not a function');
    let keys = Reflect.ownKeys(obj);
    keys.forEach(key => {
        let value = obj[key];
        callback(value, key);
    });
};

// 基于render渲染为真实DOM
const render = function render(virtualDOM, container) {
    let { type, props } = virtualDOM;
    // 如果type是一个字符串,渲染的是一个html标签
    if (typeof type === 'string') {
        let ele = document.createElement(type);
        // 给标签设置属性
        each(props, (value, key) => {
            // className
            if (key === 'className') {
                ele.setAttribute('class', value);
                return;
            }
            // style
            if (key === 'style') {
                // value ——> style对象
                each(value, (styleVal, styleKey) => {
                    ele.style[styleKey] = styleVal;
                });
                return;
            }
            // children
            if (key === 'children') {
                // value ——> children值(一个值,也可以是数组)
                let children = value;
                if (!Array.isArray(children)) children = [children];
                children.forEach(child => {
                    // child ——> 每一个字节点
                    // 如果是文本节点:直接插入进来
                    if (typeof child === 'string') {
                        let textNode = document.createTextNode(child);
                        ele.append(textNode);
                        return;
                    }
                    // 如果是元素节点:递归
                    render(child, ele);
                });
                return;
            }
            ele.setAttribute(key, value);
        });
        container.append(ele);
    }
};