文章目录
1.认识jsx
jsx实际上是JavaScript语法上的一个扩展,也是JavaScript XML的缩写。它的作用就是用来描述我们的用户界面,使有关界面的代码更好的与JavaScript代码融合在一起。
jsx不同于vue的模板语法,在vue中我们经常将一些指令嵌入在html代码中来完成一些逻辑,例如v-for,v-show等等。在react中我们使用jsx,然后直接使用js的代码进行实现我们的需求。这样做的目的使更加灵活,相比之下,vue模板指令就有了一定的局限性。
1.1 react为什么使用jsx
官方给出:
- JSX在定义类似HTML这种树形结构时,十分的简单明了。
- 简明的代码结构更利于开发和维护。
- XML有着开闭标签,在构建复杂的树形结构时,比函数调用和对象字面量更易读。
我们来自己总结一下:
这就和react的理念有关系了,react认为渲染的逻辑本质上与UI逻辑之间存在耦合,他们之间的关系可以说是密不可分的,因此在react下,它的开发模式并没有将标记分离。这也是All in Js含义的一部分。
当然我们也可以不使用jsx来进行UI的开发,我们可以使用react为我们提供的createElement
函数的方式来进行DOM树的构建
var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);
但是这样做的代码可读性真的比较差,因此,react官方就推出了jsx,react的作者也极力推荐开发者使用jsx。例如以上的代码可以用jsx表示为以下内容:
var root =(
<ul className="my-list">
<li>First Text Content</li>
<li>Second Text Content</li>
</ul>
);
以上的两块代码实际上原理使相同的,后者是使用jsx的模式进行构建界面之后再通过转义器将其转为JavaScript代码由浏览器执行。这里的转义器通常使用babel。
因此我们可以得出一个一个结论:
jSX本身并不是什么高深的技术,可以说只是一个比较高级但很直观的语法糖。它非常有用,却不是一个必需品,没有JSX的React也可以正常工作。只是方便与不方便的问题。
1.2 使用前须知
我列举了几点关于再使用jsx前的需要注意的点:
语法支持:
我们的jsx的内容是在是与js代码的内容再一起的,因此就在script标签内部。
我们需要为script标签的type属性设置为text/babel
,因为这是react独有的jsx语法,凡是只要是使用jsx的地方都必须加上这个属性。
关于依赖库:
对于初识react的人来说,一般使用cdn引入库,我们知道通常使用三个依赖库:分别是react.js 、react-dom.js 和 browser.min.js,他们必须首先被加载
react.js: react的核心库
react-dom.js: 提供与DOM相关的功能
browser.min.js:将jsx的语法转为JavaScript的代码
关于关键字:
在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript,JSX的语法不会带来任何性能影响。另外,由于JSX只是一种语法,因此JavaScript的关键字class, for等也不能出现在XML中,而要如例子中所示,使用className, htmlFor代替,这和原生DOM在JavaScript中的创建也是一致的。
2. jsx基操
如果我们jsx中的内容是动态的,我们可以通过表达式来获取。书写规则就是一堆大括号,大括号内可以是任意的变量、字符串或者表达式。
2.1 jsx中的注释
为什么要连最基本的注释都要将呢?因为再jsx中用错了注释会报错。
因为jsx是嵌入在JavaScript中的一种语法,所以再编写jsx时我们需要通过jsx的语法啦编写
<div>
{/*注释*/}
<h2>{this.state.counts}</h2>
</div>
2.2 jsx嵌入变量
嵌入变量这里一般分为三种情况:
- 当变量是Number、String、Array等类型时是正常显示的
- 当变量是Null、undefined、Boolean类型时,内容为空(如果希望显示,则转为字符串即可)
- react规定变量不能为对象类型
我们可以看一下第三点:
this.state = {
firend: {
name: '张三'
}
}
<p>{this.state.firend}</p>
也就是说上面的代码是会报错的,会报这个 Objects are not valid as a React child
2.3 jsx嵌入表达式
具体可以嵌入什么表达式?
一般我们经常会加一些运算操作、三元运算或者是执行一些函数。
render() {
const {firstName,lastName,age}=this.state;
return (
<div>
<p>{firstName+' '+lastName}</p>
<p>{age>18?'成年':'未成年'}</p>
<p>{this.print('haha')}</p>
</div>
)
}
2.4 jsx绑定属性
使用jsx动态的绑定属性实际上做法也是使用嵌入表达式的形式,我们也可以控制属性的动态展示,这里就相当的灵活了,我们可以使用任意操作来进行属性的定义。
-
因为我们jsx实际上是js操作虚拟dom的语法糖,因此我们的有些关键字就不能与js的冲突,例如class属性、label中的for等。
-
当我们需要改变为dom增添class属性时,需要使用className而不是class。for要使用htmlFor。
-
也可以添加style属性,需要注意的是style后面跟的是一个对象类型,对象中是样式的属性名和属性值;
render() {
const { title, imgUrl, flag } = this.state
return (
<div>
<h2 title={title}>React Demo</h2>
<img src={getimgUrlSize(imgUrl, 100)} alt="" />
<div className='box'>div</div>
<div className={"box " + (flag ? 'activity':'')}>我也时div元素</div>
<div style={{color:'red'}}>ttttt</div>
</div>
)
}
3. jsx事件绑定
再react中的事件绑定,事件名称时采用小驼峰式的写法。我们也需要一对{}传入一个事件处理函数。这个函数会在发生时执行。
render() {
return (
<div>
<h2>React Demo</h2>
<button onClick={this.btnfun}>button</button>
</div>
)
}
btnfun(){
console.log('zhixing');
}
上面就是一个基本的写法。
3.1 jsx之间绑定中的this指向问题
当时如果这是我有一个需求,我点击以下按钮,来获取我们state中的数据。
btnfun(){
console.log(this);//undefined
console.log(this.state.data);//not undefined
}
这里为什么会出现这个问题呢?因为btnfun()函数并不是我们主动调用的,而是当触发button的事件时,react内部再去调用这个函数。而react的内部时不知道这里的this是指向哪里的。
为了解决这个问题,我们给出了三种解决方案:
方案一:使用bind为事件方法绑定this:
在使用事件方法时直接通过bind改变事件方法的指向,就像如下代码一样:
<button onClick={this.btnfun.bind(this)}>button</button>
btnfun(){
console.log(this);//undefined
console.log(this.state.data);//not undefined
}
这是当react内部调用我们的事件函数时,就可以得知函数中的this指向何处。
上面的写法是一解决方案,但是当我们有 若干个事件都要去执行这个函数怎么办呢?我们可以使用下面这种方式进行优化。
constructor(props) {
super(props),
this.state={
data:'message'
},
this.btnfun=this.btnfun.bind(this)
}
我们可以将为事件方法绑定this的操作放在构造器里,这样在后面使用时就直接使用this.btnfun
即可。
方案二:使用ES6的class fields语法
这种语法是给类定义属性的方法,称之为class field语法。因为这里在赋值时使用了箭头函数,而箭头函数的this在任何情况下都会去上一个作用域中查找,而我们事件方法的上一个作用域恰巧就是当前的对象。
btnfun=() => {
console.log(this);//当前对象
console.log(this.state.data);//正常输出
}
这里须知:
箭头函数体内的
this
对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。
方案三:事件监听时传入箭头函数(推荐)
这种方式其实就是当给onClick事件中直接传入一个箭头函数,然后在箭头函数中进行我们的操作,可以直接写在函数中,也可以调用外部的函数。这里的this也就是箭头函数中的this,指向的时上一个作用域的this,而这里也会发生隐式绑定,将this的指向变为我们想要的结果。
写法如下:
render() {
return (
<div>
<h2>React Demo</h2>
{/*<button onClick={this.btnfun}>button</button>*/}
<button onClick={() => this.btnfun()}>button</button>
</div>
)
}
btnfun() {
console.log(this);//undefined
console.log(this.state.data);//not undefined
}
3.2 事件参数传递
在处理事件时,有时候也会有一些需要传递的参数,最常见的就是event对象。在jsx中其实在我们的事件方法中默认就给我们传递了event对象
如下代码所示:
render() {
return (
<div>
<h2>React Demo</h2>
<button onClick={this.btnclick}>button</button>
</div>
)
}
btnclick(e){
console.log(e);//打印event对象
}
}
但是如果我们还想要传递一些其他的参数呢?还记得我们this绑定问题的方案3吗,我们可以使用上方法进行参数的传递,包括event对象(是箭头函数的参数)
看以下需求:我们想要输出一个列表,并且点击单个li标签,打印对象的item和index以及event对象。
class App extends React.Component {
constructor(props) {
super(props),
this.state = {
movies: ['A', 'B', 'C', 'D']
}
this.btnclick = this.btnclick.bind(this)
}
render() {
return (
<div>
<h2>React Demo</h2>
<ul>
{
this.state.movies.map((item, index) => {
return <li onClick={(e) => {
this.liclick(item, index,e)
}}>{item}</li>
})
}
</ul>
</div>
)
}
liclick(item, index,e) {
console.log('li发生了点击', item, index,e);
}
}
4. jsx渲染
关于react的渲染值得一提就是条件渲染了吧,条件渲染即判断dom是否渲染。熟悉vue的同学应该知道在vue中,条件渲染通常使用v-if或是v-show的指令来完成。但是在react中,我们通常使用普通的JavaScript代码来进行操作。
我们可以看一下面的例子:
需求:判断登录未登录,以及文本的切换:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
flag: false
}
}
render() {
let dom = this.state.flag ? <h2>欢迎您</h2> : <h2>请登录</h2>
return (
<div>
{dom}
<button onClick={() => {
this.btnclick()
}}>{this.state.flag ? '退出' : '登录'}</button>
<hr/>
{/* 逻辑与判断 */}
<div>{this.state.flag &&'你好啊'}</div>
</div>
)
}
btnclick(){
this.setState({
flag: !this.state.flag
})
}
}
jsx渲染还包括列表渲染等,其中就包括了一些js高阶函数的使用。
5.Jsx原理解析
实际上jsx就是React中一个函数的语法糖,这个函数就是CreateElement
,前面提到我们通常在引入React时会引入三个文件,分别是分别是react.js 、react-dom.js 和 browser.min.js,bebel在这里的作用就是将jsx语法转义为React可以识别的代码。
我们可以看一下以下代码通过Bebel转义后的结果:
<div>
<h2>React Demo</h2>
<div className="header">
<h1>我是标题</h1>
</div>
<div className="content">
<p>content</p>
<button>按钮</button>
</div>
<div className="footer">Footer</div>
</div>
可以通过Babel官方的转换工具(Babeljs.io),注意勾选react选项。
上面代码编译完成输出以下结果:
/*#__PURE__*/
React.createElement("div", null, /*#__PURE__*/React.createElement("h2", null, "React Demo"), /*#__PURE__*/React.createElement("div", {
className: "header"
}, /*#__PURE__*/React.createElement("h1", null, "\u6211\u662F\u6807\u9898")), /*#__PURE__*/React.createElement("div", {
className: "content"
}, /*#__PURE__*/React.createElement("p", null, "content"), /*#__PURE__*/React.createElement("button", null, "\u6309\u94AE")), /*#__PURE__*/React.createElement("div", {
className: "footer"
}, "Footer"));
我们不难看出jsx最终被编译成上图的第二种形式,也就是说我们也可以不使用jsx,直接使用createElement函数,当时这样写也就无疑时增加了开发的难度。
createElement函数:
这个函数通常接收三个参数
参数一:type,是当前参数的类型,如果是标签元素就是用字符串来表示标签名,如果是组件元素,就直接使用组件的名称。
参数二:config,所有jsx的属性都以对象的形式以键值对出现
参数三:children,存放在标签中的内容
children参数详情:
我们可以从上面转义后的代码中可以看出,最上层的为div标签,但是我们的最上层的容器有四个平级元素,这就导致我们的children参数有四个,之后子元素的在进行嵌套,这就是我们整个流程。
它的底层是这样处理的:
通过argument参数变量来判断当前的参数长度再去判断children的个数,如果children个数为1,则直接将其挂载在props对象上。如果不为1,就是有多个参数的情况,通过for将children的值赋给新定义的childArray。因此props的children的值的类型是根据它的长度不同而变化的,长度为1时就是一个对象(ReactElement对象),当长度>1是就是一个数组。
6.react的虚拟DOM
在react渲染DOM时使用的reactDOM.render函数渲染的并不是真实的DOM,而是JavaScript对象,也就是虚拟的DOM。
在上一讲我们提到了,当createElement函数的第三个参数为一个时props的children值会是一个对象,也就是说crateElement函数返回一个对象,我们可以看一下源码:
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
在react的源码中createElement函数返回了一个ReactElement对象。事实上这个对象的作用就是react为了组成一棵JavaScript的对象树而设定的。也就是我们所说的虚拟DOM。
我们可以打印一下上一节研究createElement函数的代码:
render() {
let vdom=<div>
<h2>React Demo</h2>
<div className="header">
<h1>我是标题</h1>
</div>
<div className="content">
<p>content</p>
<button>按钮</button>
</div>
<div className="footer">Footer</div>
</div>
console.log(vdom);
return vdom;
}
最终的打印结果如下图:
很明显我们可以看出,这就形成了一颗层级分明的JavaScript的树结构。
转为真实dom:
既然jsx为我们形成了一颗虚拟dom,那么我们如何将虚拟dom转为真实的dom呢?
开头我们也提到了,就是使用reactDOM.render函数,这个函数就是将我们的虚拟dom映射到我们真实的dom中去。
总结:
虚拟DOM从创建到渲染整个流程:
jsx->createElement函数->ReactElement(虚拟对象树)->ReactDOM.render->真实DOM
其实react native(RN)中的渲染dom的流程和这个时相似的,只是映射的真实dom不用:
jsx->createElement函数->ReactElement(虚拟对象树)->ReactDOM.render->原生控件(IOS/Android)
7.为什么使用虚拟DOM
为什么要采用虚拟的DOM而不是直接进行修改真实的DOM呢?
- 真实的DOM很难追踪状态,不方便针对应用程序进行调试
- 操作真实的DOM性能较低,传统的开发模式会进行频繁的DOM操作,这会造成性能较低的现象。
- DOM操作很慢,轻微的操作都可能导致⻚面重新排 版,⾮常耗性能
然而相对于DOM对象,js对象处理起来更快, 而且更简单。通过diff算法对比新旧vdom之间的差异,可以 批量的、最⼩化的执行dom操作,从而提高性能
官网给我们的描述是:
虚拟DOM是一种编程理念,在这种理念中UI是以理想化或者说虚拟化的方式保存在内存中,并且他是一个相对简单的JavaScript对象;我们可以通过ReactDOM.render让虚拟DOM和真实DOM同步起来,这个过程叫做协调