React Native从入门到实战--ES6、ES7、ES8整体了解、React必备基础
ES6、ES7、ES8整体了解:
在上一次https://www.cnblogs.com/webor2006/p/14555391.html已经将RN的环境给搭建好了,接下来就可以正式进入RN的全面学习了,万丈高楼平地起,还是从基础开始来为未来的大厦的构建保驾护航。
由于RN是基于JS来开发的,而JS又是遵照ECMAScript标准来实现的,其中标红的字母拼起来就叫ES,只要接触过一点点前端的小伙伴肯定或多或少都听过ES语法,最熟知的就是箭头函数、Promise。所以接下来对于ES(6、7、8)进行一个全面整体的认识【脑子里有印象既可,平常在技术交流时也可以让自己能大致分辨它是属于es几的语法】,这里打算参考这位大佬https://www.devio.org/2018/09/09/ES6-ES7-ES8-Feature/的文章,本身所选择的学习教程也是他的,其实他的这篇文章已经记录得非常非常详细了,看一看就OK了,但是终归是作者的东东,没有形成自己的输出在大脑中的印象永远是比较浅的,秉着厚颜无耻的态度打算从头至尾的再来梳理一遍形成自己的笔记。
概述:
ES全称ECMAScript,ECMAScript是ECMA制定的标准化脚本语言。目前JavaScript使用的ECMAScript版本为ECMAScript-262。
ECMAScript 标准建立在一些原有的技术上,最为著名的是 JavaScript (网景) 和 JScript (微软)。它最初由网景的 Brendan Eich 发明,第一次出现是在网景的 Navigator 2.0 浏览器上。Netscape 2.0 以及微软 Internet Explorer 3.0 后续的所有浏览器上都有它的身影。
ECMAScript版本 | 发布时间 | 新增特性 |
---|---|---|
ECMAScript 2009(ES5) | 2009年11月 | 扩展了Object、Array、Function的功能等 |
ECMAScript 2015(ES6) | 2015年6月 | 类,模块化,箭头函数,函数参数默认值等 |
ECMAScript 2016(ES7) | 2016年3月 | includes,指数操作符 |
ECMAScript 2017(ES8) | 2017年6月 | sync/await,Object.values(),Object.entries(),String padding等 |
其中RN是基于ES6及以上进行开发的,其中引用使者的一句话:“了解这些特性, 不仅能使我们的编码更加的符合规范,而且能提高我们Coding的效率。”
ES6的特性:
ES6的特性比较多,在 ES5 发布近 6 年(2009-11 至 2015-6)之后才将其标准化。两个发布版本之间时间跨度很大,所以ES6中的特性比较多。
在这里列举几个常用的:
- 类
- 模块化
- 箭头函数
- 函数参数默认值
- 模板字符串
- 解构赋值
- 延展操作符
- 对象属性简写
- Promise
- Let与Const
下面则具体对上面常用的特性进行了解。
1.类(class)
对熟悉Java,object-c,c#等纯面向对象语言的开发者来说,都会对class有一种特殊的情怀。ES6 引入了class(类),让JavaScript的面向对象编程变得更加简单和易于理解。
下面来看以下在ES6中的类的定义,跟Java的非常类似:
class Animal { // 构造函数,实例化的时候将会被调用,如果不指定,那么会有一个不带参数的默认构造函数. constructor(name,color) { this.name = name; this.color = color; } // toString 是原型对象上的属性 toString() { console.log('name:' + this.name + ',color:' + this.color); } } var animal = new Animal('dog','white');//实例化Animal animal.toString(); console.log(animal.hasOwnProperty('name')); //true console.log(animal.hasOwnProperty('toString')); // false console.log(animal.__proto__.hasOwnProperty('toString')); // true
下面来运行一下,这里用WebStorm来进行代码的编写,新建个空工程:
然后创建一个js文件:
将上述代码拷进去,运行一下:
而对于上述的代码,只要熟悉面向对象的语言的小伙伴看着都非常亲切,构建这里用constructor来标识,而生成实例也是用new,其中特别要提出的是它:
另外还有一点就是对于成员变量是不需要声明的,直接用this就可以进行赋值,这点跟Java是不一样的:
对于面向对象来说三大特性中有一个很重要的是继承对吧,那在ES类语法中也支持继承,下面看代码:
class Cat extends Animal { constructor(action) { // 子类必须要在constructor中指定super 函数,否则在新建实例的时候会报错. // 如果没有置顶consructor,默认带super函数的constructor将会被添加、 super('cat','white'); this.action = action; } toString() { console.log(super.toString()); } } var cat = new Cat('catch') cat.toString(); // 实例cat 是 Cat 和 Animal 的实例,和Es5完全一致。 console.log(cat instanceof Cat); // true console.log(cat instanceof Animal); // true
下面将代码拷到工程中运行一下:
其中子类也是先初始化父类才行,另外判断实例的类型也是用instanceof,跟java一样。
2.模块化(Module)
ES5不支持原生的模块化,在ES6中模块作为重要的组成部分被添加进来。模块的功能主要由 export 和 import 组成。每一个模块都有自己单独的作用域,模块之间的相互调用关系是通过 export 来规定模块对外暴露的接口,通过import来引用其它模块提供的接口。同时还为模块创造了命名空间,防止函数的命名冲突。关于这块当时在学习小程序时有使用过,正好可以又来巩固一下这块的知识。
导出(export):
ES6允许在一个模块中使用export来导出多个变量或函数。
- 导出变量:
//test.js export var name = 'Rainbow'
心得:ES6不仅支持变量的导出,也支持常量的导出。
export const sqrt = Math.sqrt;//导出常量
ES6将一个文件视为一个模块,上面的模块通过 export 向外输出了一个变量。一个模块也可以同时往外面输出多个变量。
//test.js var name = 'Rainbow'; var age = '24'; export {name, age};
- 导出函数:
// myModule.js export function myModule(someArg) { return someArg; }
导入(import):
定义好模块的输出以后就可以在另外一个模块通过import引用。
import {myModule} from 'myModule';// main.js import {name,age} from 'test';// test.js
一条import 语句可以同时导入默认函数和其它变量。
import defaultMethod, { otherMethod } from 'xxx.js';
关于这块比较简单,就不运行了。
3.箭头(Arrow)函数
这是ES6中最令人激动的特性之一。=>
不只是关键字function的简写,它还带来了其它好处。箭头函数与包围它的代码共享同一个this
,能帮你很好的解决this的指向问题。有经验的JavaScript开发者都熟悉诸如var self = this;
或var that = this
这种引用外围this的模式。但借助=>
,就不需要这种模式了。
关于这块其实在学习“Java后台到全栈【这块学习已经落下了,之后找时间得补上】”时的小程序那块有学习过:
这是非常经典的一个问题,这种写法很显然不太简洁,而让其变得更加简洁的写法就是使用这里所说的箭头函数,如下:
其实像Java8中的Lambda表达式也有this的功效,像在匿名内部类要访问外部类是不是得要用xxxx.this来指向?而如果使用了->表达式,this就可以直接访问了。
箭头函数的结构:
箭头函数的箭头=>之前是一个空括号、单个的参数名、或用括号括起的多个参数名,而箭头之后可以是一个表达式(作为函数的返回值),或者是用花括号括起的函数体(需要自行通过return来返回值,否则返回的是undefined)。比如:
// 箭头函数的例子 ()=>1 v=>v+1 (a,b)=>a+b ()=>{ alert("foo"); } e=>{ if (e == 0){ return 0; } return 1000/e; }
卸载监听器时的陷阱:
不论是箭头函数还是bind,每次被执行都返回的是一个新的函数引用,因此如果你还需要函数的引用去做一些别的事情(譬如卸载监听器),那么你必须自己保存这个引用。
上面这句话啥意思呢?这里先有个超前的知识了解,在Reactive Native中组件中存在两个生命周期,一个是挂载,一个是卸载,通常会处理监听函数,比如:
class PauseMenu extends React.Component{ componentWillMount(){ AppStateIOS.addEventListener('change', this.onAppPaused.bind(this)); } componentWillUnmount(){ AppStateIOS.removeEventListener('change', this.onAppPaused.bind(this)); } onAppPaused(event){ } }
但是!!!上面这种写法是有问题的,因为每次bind的时候都会返回一个新的引用,导致监听器卸载会失败,而正确的做法应该是将bind的引用先记录一下,如下:
class PauseMenu extends React.Component{ constructor(props){ super(props); this._onAppPaused = this.onAppPaused.bind(this); } componentWillMount(){ AppStateIOS.addEventListener('change', this._onAppPaused); } componentWillUnmount(){ AppStateIOS.removeEventListener('change', this._onAppPaused); } onAppPaused(event){ } }
另外还有一种做法就是使用箭头函数,具体如下:
class PauseMenu extends React.Component{ componentWillMount(){ AppStateIOS.addEventListener('change', this.onAppPaused); } componentWillUnmount(){ AppStateIOS.removeEventListener('change', this.onAppPaused); } onAppPaused = (event) => { //把函数直接作为一个arrow function的属性来定义,初始化的时候就绑定好了this指针 } }
很显然有了箭头函数之后,写法也更加简单。
需要注意的是:不论是bind还是箭头函数,每次被执行都返回的是一个新的函数引用,因此如果你还需要函数的引用去做一些别的事情(譬如卸载监听器),那么你必须自己保存这个引用。
4.函数参数默认值
ES6支持在定义函数的时候为其设置默认值:
function foo(height = 50, color = 'red') { // ... }
不使用默认值:
function foo(height, color) { var height = height || 50; var color = color || 'red'; //... }
这样写一般没问题,但当参数的布尔值为false
时,就会有问题了。比如,我们这样调用foo函数:
foo(0, "")
因为0的布尔值为false
,这样height的取值将是50。同理color的取值为‘red’,很显然不符合本意了,本意是高度为0,但是目前它变成了50了。
所以说,函数参数默认值
不仅能使代码变得更加简洁而且能规避一些问题。
5.模板字符串
ES6支持模板字符串
,使得字符串的拼接更加的简洁、直观。
不使用模板字符串:
var name = 'Your name is ' + first + ' ' + last + '.'
而如果使用模板字符串此时写法就变为:
var name = `Your name is ${first} ${last}.`
在ES6中通过${}
就可以完成字符串的拼接,只需要将变量放在大括号之中。其实有很多语言都有类似的特性,如Flutter、Koltin等。
6.解构赋值
解构赋值语法是JavaScript的一种表达式,可以方便的从数组或者对象中快速提取值赋给定义的变量。关于这块可以参考https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment,而这个网站主要是学习WEB的一个交流平台:
对于学习WEB来说是一个非常好的网站,看一下啥是解构赋值。
从图中一看便知,下面具体再来看一下:
获取数组中的值:
//解构赋值 var foo = ["one", "two", "three", "four"]; var [one, two, three] = foo; console.log(one); // "one" console.log(two); // "two" console.log(three); // "three"
运行:
/usr/local/bin/node /Users/xiongwei/Documents/workspace/reactnativestudy/reactive-native-study/reactive_native_grammar_study/es/02_es_destructuring_assignment.js one two three Process finished with exit code 0
如果你要忽略某些值,你可以按照下面的写法获取你想要的值:
也可以这样写:
如果没有从数组中的获取到值,你可以为变量设置一个默认值。
通过解构赋值可以方便的交换两个变量的值。
获取对象中的值:
利用解构赋值不可以获得对象中的值,比如:
7.延展操作符(Spread operator)
延展操作符"..."
可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造对象时, 将对象表达式按key-value的方式展开。
语法:
函数调用时:
myFunction(...iterableObj);
数组构造或字符串:
[...iterableObj, '4', ...'hello', 6];
构造对象时,进行克隆或者属性拷贝(ECMAScript 2018【ES9】规范新增特性):
let objClone = { ...obj };
应用场景:
- 在函数调用时使用延展操作符:
function sum(x, y, z) { return x + y + z; } const numbers = [1, 2, 3]; //不使用延展操作符 console.log(sum.apply(null, numbers)); //使用延展操作符 console.log(sum(...numbers));// 6
可以看到,使用延展操作符之后让函数调用也变得简单了,运行:
/usr/local/bin/node /Users/xiongwei/Documents/workspace/reactnativestudy/reactive-native-study/reactive_native_grammar_study/es/03_es_spread_operator.js 6 6 Process finished with exit code 0
- 构造数组:
没有展开语法的时候,只能组合使用 push,splice,concat 等方法,来将已有数组元素变成新数组的一部分。有了展开语法, 构造新数组会变得更简单、更优雅:
和参数列表的展开类似,
...
在构造字数组时, 可以在任意位置多次使用。 - 数组拷贝:
展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。
- 连接多个数组:
在ECMAScript 2018中延展操作符增加了对对象的支持:
在React中的应用:
关于React在之后会专门学习的,先了解一下它:
通常我们在封装一个组件时,会对外公开一些 props 用于实现功能。大部分情况下在外部使用都应显示的传递 props 。但是当传递大量的props时,会非常繁琐,这时我们可以使用 ...(延展操作符,用于取出参数对象的所有可遍历属性)
来进行传递。比如一般情况下一个组件的属性是这么写的:
<CustomComponent name ='Jine' age ={21} />
此时可以使用延展操作符,就可以这样写了:
const params = { name: 'Jine', age: 21 } <CustomComponent {...params} />
配合解构赋值避免传入一些不需要的参数:
var params = { name: '123', title: '456', type: 'aaa' } var { type, ...other } = params; <CustomComponent type='normal' number={2} {...other} /> //等同于 <CustomComponent type='normal' number={2} name='123' title='456' />
也就是组件只需要使用params中的部分参数,下面来运行一下就知道了:
8.对象属性简写
在ES6中允许我们在设置一个对象的属性的时候不指定属性名。下面以两个维度来对比一下写法:
不使用ES6:
其中可以发现对象中必须包含属性和值,显得非常冗余。
使用ES6:
其中看到区别木有:
9.Promise
接下来这个特性就是非常非常棒的,也是经常被听说的,其实像Kotlin中也有类似的机制,Promise 是异步编程的一种解决方案,比传统的解决方案callback更加的优雅。它最早由社区提出和实现的,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。下面来看这么个例子,在不使用ES6的情况下写法如下:
嵌套两个setTimeout回调函数:
setTimeout(function() { console.log('Hello'); // 1秒后输出"Hello" setTimeout(function() { console.log('Hi'); // 2秒后输出"Hi" }, 1000); }, 1000);
这就是典型回调地狱的问题,接下来使用ES6,对比着感受一下:
var waitSecond = new Promise(function(resolve, reject) { setTimeout(resolve, 1000); }); waitSecond .then(function() { console.log("Hello"); // 1秒后输出"Hello" return waitSecond; }) .then(function() { console.log("Hi"); // 2秒后输出"Hi" });
上面的的代码使用两个then来进行异步编程串行化,避免了回调地狱。这个特性在未来Reactive Native会大量用到,需要好好提前掌握一下此特性。
10.支持let与const
在之前JS是没有块级作用域的,const与let填补了这方便的空白,const与let都是块级作用域。
使用var定义的变量为函数级作用域:
使用let与const定义的变量为块级作用域:
ES7的特性:
在ES6之后,ES的发布频率更加频繁,基本每年一次,所以自ES6之后,每个新版本的特性的数量就比较少。
Array.prototype.includes():
includes()
函数用来判断一个数组是否包含一个指定的值,如果包含则返回 true
,否则返回false
。
includes
函数与 indexOf
函数很相似,下面两个表达式是等价的:
arr.includes(x)
arr.indexOf(x) >= 0
接下来我们来判断数字中是否包含某个元素:
- 在ES7之前的做法:
使用
indexOf()
验证数组中是否存在某个元素,这时需要根据返回值是否为-1来判断: - 使用ES7的includes():
使用includes()验证数组中是否存在某个元素,这样更加直观简单:
指数操作符:
在ES7中引入了指数运算符**
,**
具有与Math.pow(..)
等效的计算结果。
- 不使用指数操作符:
使用自定义的递归函数calculateExponent或者Math.pow()进行指数运算:
- 使用指数操作符:
这时就比较简单了,使用指数运算符**,就像+、-等操作符一样:
ES8的特性:
async/await:
这个也是一个非常重磅的特性,在ES8中加入了对async/await
的支持,也就我们所说的异步函数
,这是一个很实用的功能。 async/await
将我们从头痛的回调地狱中解脱出来了,使整个代码看起来很简洁。在Flutter中也存在类似的语法,下面看一下使用async/await与不使用async/await的差别:
function login(userName) { return new Promise((resolve, reject) => { setTimeout(() => { resolve('1001'); }, 600); }); } function getData(userId) { return new Promise((resolve, reject) => { setTimeout(() => { if (userId === '1001') { resolve('Success'); } else { reject('Fail'); } }, 600); }); } // 不使用async/await ES7 function doLogin(userName) { login(userName) .then(getData) .then(result => { console.log(result) }) } doLogin("cexo")// Success
运行:
是不是可以感受到使用了该特性的一个好处,代码顺间变得清晰明了了。另外注意:要想使用此特性,一定是要先定义promise才行哟。
async/await的几种应用场景:
- 获取异步函数的返回值:
异步函数本身会返回一个
Promise
,所以我们可以通过then
来获取异步函数的返回值。// function login(userName) { // return new Promise((resolve, reject) => { // setTimeout(() => { // resolve('1001'); // }, 600); // }); // } // // function getData(userId) { // return new Promise((resolve, reject) => { // setTimeout(() => { // if (userId === '1001') { // resolve('Success'); // } else { // reject('Fail'); // } // }, 600); // }); // } // 不使用async/await ES7 // function doLogin(userName) { // login(userName) // .then(getData) // .then(result => { // console.log(result) // }) // } // doLogin("cexo")// Success // 使用async/await ES8 // async function doLogin2(userName) { // const userId = await login(userName); // const result = await getData(userId); // console.log(result) // } // // doLogin2("cexo")// Success //async和await几个使用场景 // 1、获取异步函数的返回值 async function charCountAdd(data1, data2) { const d1 = await charCount(data1); const d2 = await charCount(data2); return d1 + d2; } function charCount(data) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(data.length); }, 1000); }); } charCountAdd('Hello', 'Hi').then(console.log);//通过then获取异步函数的返回值。
下面运行一下:
- async/await在并发场景中的应用:
对于上述的例子,我们调用await
两次,每次都是等待1秒一共是2秒,效率比较低,而且两次await
的调用并没有依赖关系,那能不能让其并发执行呢,答案是可以的,接下来我们通过Promise.all
来实现await
的并发调用。
//2、async/await在并发场景中的应用 async function charCountAdd(data1, data2) { const [d1, d2] = await Promise.all([charCount(data1), charCount(data2)]); return d1 + d2; } function charCount(data) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(data.length); }, 1000); }); } charCountAdd('Hello', 'Hi').then(console.log);
此时结果跟上一次一样,不过是并发的,性能要好。通过上述代码我们实现了两次
charCount
的并发调用,Promise.all
接受的是一个数组,它可以将数组中的promise对象并发执行; - async/await的几种错误处理方式:
1、捕捉整个async/await函数的错误:
async function charCountAdd(data1, data2) { const d1=await charCount(data1); const d2=await charCount(data2); return d1+d2; } charCountAdd('Hello','Hi') .then(console.log) .catch(console.log);//捕捉整个async/await函数的错误 ...
这种方式可以捕捉整个
2、捕捉单个的await表达式的错误:charCountAdd
运行过程中出现的错误,错误可能是由charCountAdd
本身产生的,也可能是由对data1
的计算中或data2
的计算中产生的。
async function charCountAdd(data1, data2) { const d1=await charCount(data1) .catch(e=>console.log('d1 is null')); const d2=await charCount(data2) .catch(e=>console.log('d2 is null')); return d1+d2; } charCountAdd('Hello','Hi').then(console.log);
通过这种方式可以捕捉每一个
await
表达式的错误,如果既要捕捉每一个await
表达式的错误,又要捕捉整个charCountAdd
函数的错误,可以在调用charCountAdd
的时候加个catch,如下:
... charCountAdd('Hello','Hi') .then(console.log) .catch(console.log);//捕捉整个async/await函数的错误 ...
async function charCountAdd(data1, data2) { let d1,d2; try { d1=await charCount(data1); d2=await charCount(data2); }catch (e){ console.log('d1 is null'); } return d1+d2; } charCountAdd('Hello','Hi') .then(console.log); function charCount(data) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(data.length); }, 1000); }); }
Object.values():
Object.values()
是一个与Object.keys()
类似的新函数,但返回的是Object自身属性的所有值,不包括继承的值。
假设我们要遍历如下对象obj
的所有值:
const obj = {a: 1, b: 2, c: 3};
不使用Object.values() :ES7,如下:
使用Object.values() :ES8,如下:
Object.entries:
Object.entries()
函数返回一个给定对象自身可枚举属性的键值对的数组。
接下来我们来遍历上文中的obj
对象的所有属性的key和value:
不使用Object.entries() :ES7,如下:
const obj = {a: 1, b: 2, c: 3}; // 不使用Object.entries() :ES7 Object.keys(obj).forEach(key => { console.log('key:' + key + ' value:' + obj[key]); }) //key:a value:1 //key:b value:2 //key:c value:3
运行:
注意:其中的s是因为这句话太长导致:
不要觉得奇怪~~接下来再使用使用Object.entries() :ES8,如下:
String padding:
在ES8中String新增了两个实例函数String.prototype.padStart
和String.prototype.padEnd
,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
String.padStart(targetLength,[padString])
- targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
- padString:(可选)填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 “ “。
上面这个稍加解释一下:
String.padEnd(targetLength,padString]):
- targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
- padString:(可选) 填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 “ “;
这块就比较好理解,不多解释了。不过这个实际用得不多,了解既可。
函数参数列表结尾允许逗号【了解】:
这是一个不痛不痒的更新,主要作用是方便使用git进行多人协作开发时修改同一个函数减少不必要的行变更。
不使用ES8:
//程序员A var f = function(a, b ) { ... } //程序员B var f = function(a, b, //变更行 c //变更行 ) { ... } //程序员C var f = function(a, b, c, //变更行 d //变更行 ) { ... }
使用ES8:
//程序员A var f = function(a, b, ) { ... } //程序员B var f = function(a, b, c, //变更行 ) { ... } //程序员C var f = function(a, b, c, d, //变更行 ) { ... }
能看出差别么:
了解既可~~
Object.getOwnPropertyDescriptors():
Object.getOwnPropertyDescriptors()
函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
函数原型:
Object.getOwnPropertyDescriptors(obj)
返回obj
对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
const obj2 = { name: 'Jine', get age() { return '18' } }; console.log(Object.getOwnPropertyDescriptors(obj2))
运行:
/usr/local/bin/node /Users/xiongwei/Documents/workspace/reactnativestudy/reactive-native-study/reactive_native_grammar_study/es/13_es8_object_getOwnPropertyDescriptors.js { name: { value: 'Jine', writable: true, enumerable: true, configurable: true }, age: { get: [Function: get age], set: undefined, enumerable: true, configurable: true } } Process finished with exit code 0
总结:
以上基于大佬的文章利用CV大法走马观花式的对于ES语法进行了一个整体的了解,当然不可以有记得住,但是!!!最起码能在你大脑中有一个基本的印象,为未来进一步学习打下一个良好的基础,也是为之后学习中碰到语法记不起来的一个备忘,还是挺有意义的。
React必备基础:
概述:
既然咱们是要学习React Native,那么对于前面的这个“React”是啥总得知道吧,所以接下来从React的特点、如何使用React、JSX语法,然后会对组件(Component)以及组件的属性(props)、状态(state)、生命周期等方面进行学习,同样是参考大佬这篇文章,整体亲自过一遍:https://www.devio.org/2019/03/03/react-basis-for-react-native/。
React是什么?
React 是 Facebook 推出的开源 JavaScript Library,它是一个用于组建用户界面的JavaScript库,让你以更简单的方式来创建交互式用户界面,它的出现让许多革新性的 Web 观念开始流行起来,例如:Virtual DOM、Component,声明式渲染等。
声明式渲染:
其中“声明式渲染”标黄了,什么是声明式呢?与它对应的是命令式,先来对这俩基本概念有一个了解:
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
直观感受声明式和渲染式编程:
这里可以到https://codepen.io/crazycodeboy/pen/VVJLYm?editors=0010这个网址直观的感受一下:
而如果是命令式编程,则是这样:
这里列出React它的特点:
- 当数据改变时,React将高效的更新和渲染需要更新的组件。声明式视图使你的代码更可预测,更容易调试。
- 构建封装管理自己的状态的组件,然后将它们组装成复杂的用户界面。由于组件逻辑是用JavaScript编写的,而不是模板,所以你可以轻松地通过您的应用程序传递丰富的数据,并保持DOM状态。
- 一次学习随处可写,学习React,你不仅可以将它用于Web开发,也可以用于React Native来开发Android和iOS应用。
另外再看一下React组件的特点:不是模板却比模板更加灵活,比如之后实战要写的一个项目的样子为例:
上面这个页面是通过不同的组件封装而成的,组件化的开发模式,使得代码在更大程度上得到复用,而且组件之间的组装很灵活。
如何使用?
构建一个新的 React 单页应用,可以通过Create React App来完成。它可以帮助你配置开发环境,以便你可以使用最新的 JavaScript 特性,还能提供一个友好的开发体验,并为生产环境优化你的应用。
npm install -g create-react-app create-react-app my-app cd my-app npm start
"dependencies": { "react": "^16.6.3",//是 React 的核心库 "react-dom": "^16.6.3",//提供与 DOM 相关的功能 "react-scripts": "2.1.1"//create-react-app 的一个核心包,一些脚本和工具的默认配置都集成在里面 },
ReactDOM.render
在上面直观感受声明式和命令式渲染时,代码上有这么一句话:
其中用到:
ReactDOM.render(element, container[, callback])
它是渲染一个 React 元素到由 container 提供的 DOM 中,并且返回组件的一个 引用(reference) (或者对于 无状态组件 返回 null )。
JSX
JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。 每一个XML标签都会被JSX转换工具转换成纯JavaScript代码,使用JSX,组件的结构和组件之间的关系看上去更加清晰。 JSX并不是React必须使用的,但React官方建议我们使用 JSX , 因为它能定义简洁且我们熟知的包含属性的树状结构语法。 下面看一个对比的示例代码:
React.render(//使用JSX <div> <div> <div>content</div> </div> </div>, document.getElementById('example') ); React.render(//不使用JSX React.createElement('div', null, React.createElement('div', null, React.createElement('div', null, 'content') ) ), document.getElementById('example') );
很明显,使用JSX的语法代码简洁非常多。
createElement
先来看一下它的使用原型:
React.createElement(
type,
[props],
[...children]
)
根据给定的类型创建并返回新的 React element 。参数type既可以是一个html标签名称字符串(例如’div’ 或 ‘span’ ),也可以是一个 React component 类型(一个类或一个函数)。比如:
React.createElement(Hello, {toWhat: 'World'}, 'hello'), //等价于 <Hello toWhat="World">hello</Hello>,
HTML标签 与 React组件 对比
React 可以渲染 HTML 标签 (strings) 或 React 组件 (classes)。 要渲染 HTML 标签,只需在 JSX 里使用小写字母开头的标签名,比如下面这个代码:
var myDivElement = <div className="foo" />; React.render(myDivElement, document.root);
上面是用于Web开发,其中div的"d"一定是小写字母。
要渲染 React 组件,只需创建一个大写字母开头的本地变量。
var MyComponent = ...; var myElement = <MyComponent someProperty={true} />; React.render(myElement, document.body);
其中渲染组件时,组件的首字母一定是大写,也就是上述代码的“M”。
提示:
- React 的 JSX 里约定分别使用首字母大、小写来区分本地组件的类和 HTML 标签。
- 由于 JSX 就是 JavaScript,一些标识符像 class 和 for 不建议作为 XML 属性名。作为替代, React DOM 使用 className 和 htmlFor 来做对应的属性。
JavaScript 表达式
属性表达式:
要使用 JavaScript 表达式作为属性值,只需把这个表达式用一对大括号 {}
包起来,不要用引号 ""
。比如:
// 输入 (JSX): var person = <Person name={window.isLoggedIn ? window.name : ''} />; // 输出 (JS): var person = React.createElement( Person, {name: window.isLoggedIn ? window.name : ''} );
其中组件Person的属性需要用到一个表达式,则用大括号来包起来既可:
子节点表达式
同样地,JavaScript 表达式可用于描述子结点:
// 输入 (JSX): var content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>; // 输出 (JS): var content = React.createElement( Container, null, window.isLoggedIn ? React.createElement(Nav) : React.createElement(Login) );
也就是说:
注释
JSX 里添加注释很容易,它们只是 JS 表达式而已。你只需要在一个标签的子节点内(非最外层)用 {} 包围要注释的部分。
class ReactDemo extends Component { render() { return ( <View style={styles.container}> {/*标签子节点的注释*/} <Text style={styles.welcome} //textAlign='right' textShadowColor='yellow' /*color='red' textShadowRadius='1'*/ > React Native! </Text> </View> ); } }
心得:在标签节点以外注释,和通常的注释是一样的,多行用“/**/” 单行用“//”;
JSX延展属性
不要试图去修改组件的属性:
不推荐做法:
var component = <Component />; component.props.foo = x; // 不推荐 component.props.bar = y; // 不推荐
这样修改组件的属性,会导致React不会对组件的属性类型(propTypes)进行的检查。从而引发一些预料之外的问题。
推荐做法:
var component = <Component foo={x} bar={y} />;
延展属性(Spread Attributes):
还记得在上面咱们学习了:
它是ES6的语法。你可以使用 JSX 的新特性 - 延展属性:
var props = {}; props.foo = x; props.bar = y; var component = <Component {...props} />;
传入对象的属性会被复制到组件内。
它能被多次使用,也可以和其它属性一起用。注意顺序很重要,后面的会覆盖掉前面的。比如:
var props = { foo: 'default' }; var component = <Component {...props} foo={'override'} />;
console.log(component.props.foo); // 'override'
也就是:
最终foo属性会把之前延长属性中的foo给覆盖的。
Component【重点】
React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。
class Hello extends React.Component{ render() { return <h1>Hello {this.props.name}</h1>; } } ReactDOM.render( <Hello name="John" />, document.getElementById('example') );
上面代码中,变量 HelloMessage 就是一个组件类。模板插入 <HelloMessage />
时,会自动生成 HelloMessage 的一个实例。所有组件类都必须有自己的 render 方法,用于输出组件。
注意:
- 组件类的第一个字母必须大写
- 组件类只能包含一个顶层标签?
这里上https://codepen.io/crazycodeboy/pen/ZmdjMK?editors=0010可以看一下,其实不是的,组件类是可以返回多个平级的标签的,如下:
组件的属性(props)
我们可以通过this.props.xx
的形式获取组件对象的属性,对象的属性可以任意定义,但要避免与JavaScript关键字冲突。 比如在上图中就已经使用过了:
遍历对象的属性:
this.props.children
会返回组件对象的所有属性。 React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map
或React.Children.forEach
来遍历子节点。
React.Children.map:
React.Children.map(children, function[(thisArg)])
在包含在 children 里的每个子级上调用函数,调用的函数的 this 设置为 thisArg 。如果 children 是一个嵌套的对象或数组,它将被遍历。如果 children 是 null 或 undefined ,返回 null 或 undefined 而不是一个空数组。
React.Children.forEach:
React.Children.forEach(children, function[(thisArg)])
Usage:
class NotesList extends React.Component{ render(){ return ( <ol> { React.Children.map(this.props.children,(child)=> { return <h1>{child}</h1>; }) } </ol> ); } } ReactDOM.render(<NotesList> <span>hello</span> <span>world</span> <span>world</span> </NotesList>, document.getElementById('root'));
可以上这里https://codepen.io/crazycodeboy/pen/EOBpzE?editors=0010看一下代码效果:
这里稍加解释一下:
而此时想要遍历该组件的属性就可以用:
[PropTypes]
组件的属性可以接受任意值,字符串、对象、函数等等都可以。有时,我们需要一种机制,验证别人使用组件时,提供的参数是否符合要求。 组件类的PropTypes属性,就是用来验证组件实例的属性是否符合要求。
React.PropTypes 从 React v15.5开始被移入了
prop-types
,使用时需要留意;
import PropTypes from 'prop-types' class MyTitle extends React.Component{ static propTypes={ title: PropTypes.string.isRequired, }; render() { return <h1> title:{this.props.title} </h1>; } } ReactDOM.render(<MyTitle />, document.getElementById('root'));
上面的MyTitle组件有一个title属性。PropTypes 告诉 React,这个 title 属性是必须的,而且它的值必须是字符串。现在,我们设置 title 属性的值是一个数值时:
var data = 123; ReactDOM.render( <MyTitle title={data} />, document.body );
这样一来,title属性就通不过验证了。控制台会显示一行错误信息。
Warning: Failed propType: Invalid prop `title` of type `number` supplied to `MyTitle`, expected `string`.
更多的PropTypes设置,可以查看官方文档。
默认属性
此外,可以通过defaultProps用来设置组件属性的默认值。
class MyTitle extends React.Component{ static defaultProps={ shortName:'MyTitle' }; render() { return <h1> {this.props.shortName}</h1>; } } ReactDOM.render(<MyTitle/>, document.getElementById('root'));
此时运行就会输出默认值“MyTitle”:
ref 属性(获取真实的DOM节点)
组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。
但是,有时需要从组件获取真实 DOM 的节点,这时就要用到 ref 属性。比如有这么一个组件:
class Alert extends React.Component { showAlert(message) { alert(`Debug:${message}`); } render() { return null; } }
该组件不是有一个showAlert方法么,接下来咱们又声明一个组件,想用调用Alert组件的showAlert方法,就可以使用ref属性了,如下:
class MyTitle extends React.Component { onClick = () => { this.refs.alert.showAlert('MyTitle'); }; render() { return <div> <h1 onClick={this.onClick}>Click me</h1> <Alert ref='alert'/> </div>; } } ReactDOM.render(<MyTitle/>, document.getElementById('root'));
上面代码中,组件 MyTitle 的子节点有一个Alert组件,为了调用这个组件提供的方法,这时就必须获取真实的 DOM 节点,虚拟 DOM 是拿不到用户输入的。为了做到这一点,我们在使用这个组件的时候必须为其设置一个ref属性,然后 this.refs.[refName] 就会返回这个真实的 DOM 节点。
需要注意的是,由于 this.refs.[refName] 属性获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个属性,否则会报错。上面代码中,通过为组件指定 Click 事件的回调函数,确保了只有等到真实 DOM 发生 Click 事件之后,才会读取 this.refs.[refName] 属性。
React 组件支持很多事件,除了 Click 事件以外,还有 KeyDown 、Copy、Scroll 等,完整的事件清单请查看官方文档。
心得:ref属性在开发中使用频率很高,使用它你可以获取到任何你想要获取的组件的对象,有了这个对象你就可以灵活地做很多事情,比如:读写对象的变量,甚至调用对象的函数。
state
在上面已经学习了props了:
组件会根据props的变化来进行渲染,但组件无法改变自身的props,那么组件为了实现交互,可以使用组件的 state 。state 是组件私有的,可以通过state={}方式初始化,通过调用 this.setState()
来改变它。当 state 更新之后,组件就会重新渲染自己。
render() 方法依赖于 this.props 和 this.state ,框架会确保渲染出来的 UI 界面总是与输入( this.props 和 this.state )保持一致。
初始化state:
可以通过一下两种方式来初始化state,在组件的生命周期中仅执行一次,用于设置组件的初始化 state 。
constructor(props){ super(props); this.state={ name:'' } } //or state={ name:'' }
更新state:
通过this.setState()
方法来更新state,调用该方法后,React会重新渲染相关的UI。 this.setState({favorite:!this.state.favorite});
Usage:
class FavoriteButton extends React.Component{ state={ favorite:false }; handleClick=()=>{ this.setState({favorite:!this.state.favorite}); }; render(){ const text=this.state.favorite? 'favorite':'un favorite'; return ( <h1 onClick={this.handleClick}> You {text} this. Click to toggle. </h1> ); } }
上面代码是一个 FavoriteButton 组件,它的通过 state={}初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法就修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。
心得:由于 this.props 和 this.state 都用于描述组件的特性,可能会产生混淆。一个简单的区分方法是,this.props 表示那些本组件无法改变的特性,而 this.state 是会随着用户互动而产生变化的特性。
组件的生命周期
在iOS中UIViewController
提供了(void)viewWillAppear:(BOOL)animated
, - (void)viewDidLoad
,(void)viewWillDisappear:(BOOL)animated
等生命周期方法,在Android中Activity
则提供了 onCreate()
,onStart()
,onResume()
,onPause()
,onStop()
,onDestroy()
等生命周期方法,这些生命周期方法描述了一个界面从创建到销毁的一生。
那么在React 中组件(Component)也是有自己的生命周期方法的,先来看一张图:
该图是来自于官网https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/,而体验生命周期的代码可以参考https://codepen.io/crazycodeboy/pen/oQKvJZ?editors=0011:
class LifeCycle extends React.Component { state = {}; constructor(props) { super(props); console.log('Debug:', 'constructor'); } static getDerivedStateFromProps(nextProps, prevState) { console.log('Debug:', 'getDerivedStateFromProps'); return null; } shouldComponentUpdate(nextProps, nextState) { console.log('Debug:', 'shouldComponentUpdate'); return true; } render() { console.log('Debug:', 'render'); return <h1>LifeCycle:{this.props.count}</h1> } componentDidMount() { console.log('Debug:', 'componentDidMount'); } getSnapshotBeforeUpdate(prevProps, prevState) { console.log('Debug:', 'getSnapshotBeforeUpdate'); return null; } componentDidUpdate(prevProps, prevState, snapshot) { console.log('Debug:', 'componentDidUpdate'); } componentWillUnmount() { console.log('Debug:', 'componentDidUpdate'); } } class Test extends React.Component { state = { count: 0 }; render() { return <div> <LifeCycle count={this.state.count}/> <h2 onClick={() => { this.setState({ count: this.state.count + 1 }) }}>Add</h2> </div> } } ReactDOM.render(<Test/>, document.getElementById('root'));
组件的生命周期分成三个时期:
在上图中也可以看到有如下三个生命周期:
- Mounting:创建时
- Updating:更新时
- Unmounting:卸载时
不安全的方法
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
也就是:
使用这些生命周期方法通常会导致错误和不一致,因此将来会被弃用。在新的React版本中他们被标记为UNSAFE。那如果想要用到这三个不安全的方法,可以参考https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md来找到替换的方法。
生命周期---Mounting(装载)
constructor()
constructor(props)
React组件的构造函数将会在装配之前被调用。当为一个React.Component子类定义构造函数时,你应该在任何其他的表达式之前调用super(props)。否则,this.props在构造函数中将是未定义,并可能引发异常。如在上面体验生命周期的代码中也能看到此构造函数:
构造函数是初始化状态的合适位置。若你不初始化状态且不绑定方法,那你也不需要为你的React组件定义一个构造函数。
static getDerivedStateFromProps()
static getDerivedStateFromProps(nextProps, prevState)
组件实例化后和接受新属性时将会调用getDerivedStateFromProps。它应该返回一个对象来更新状态,或者返回null来表明新属性不需要更新任何状态。
注意,如果父组件导致了组件的重新渲染,即使属性没有更新,这一方法也会被调用。如果你只想处理变化,那么可以通过比较新旧值来完成。
调用this.setState() 通常不会触发 getDerivedStateFromProps()。
render
ReactComponent render()
render()
方法是必须的。
当被调用时,其会检查this.props 和 this.state并返回以下类型中的一个:
- React元素。 通常是由 JSX 创建。该元素可能是一个原生DOM组件的表示,如<div />,或者是一个你定义的复合组件。
- 字符串和数字。 这些将被渲染为 DOM 中的 text node。
- Portals。 由 ReactDOM.createPortal 创建。【不常见,了解】
- null。 什么都不渲染。
- 布尔值。 什么都不渲染。(通常存在于 return test && 写法,其中 test 是布尔值。)
返回null 或 false时,ReactDOM.findDOMNode(this) 将返回 null。
render()
函数应该是纯粹的,也就是说该函数不修改组件的state
,每次调用都返回相同的结果,不读写 DOM 信息,也不和浏览器交互(例如通过使用setTimeout
)。如果需要和浏览器交互,在componentDidMount()
中或者其它生命周期方法中做这件事。保持render()
纯粹,可以使服务器端渲染更加切实可行,也使组件更容易被理解。
提示:若 shouldComponentUpdate()返回false,render()函数将不会被调用。
componentDidMount()【常用 】
componentDidMount()
componentDidMount()在组件被装配后立即调用,通常在该方法中进行一些初始化操作【貌似在上面constructor()构造也可以进行初始化操作,但是它不可以获取到DOM节点,所以这点需要区分开】。·初始化时需要DOM节点的操作可以放到这里进行`。若你需要从远端加载数据,这是一个适合实现网络请求的地方。在该方法里设置状态将会触发重渲。
这一方法是一个发起任何订阅的好地方。如果你这么做了,别忘了在componentWillUnmount()退订。
另外,在这个方法中调用setState()将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了即使render()将会调用两次,但用户不会看到中间状态。
生命周期---Updating (更新)
shouldComponentUpdate:
shouldComponentUpdate(nextProps, nextState)
在接收到新的 props 或者 state,将要渲染之前调用,以让React知道当前状态或属性的改变是否不影响组件的输出。
该方法在初始化渲染的时候不会调用,在使用 forceUpdate 方法的时候也不会。如果确定新的 props 和 state 不需要重新渲染,则此处应该 返回 false。
心得:重写它你可以根据实际情况,来灵活的控制组件当 props 和 state 发生变化时是否要重新渲染组件。
getSnapshotBeforeUpdate【不常用】:
getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate()在最新的渲染输出提交给DOM前将会立即调用。它让你的组件能在当前的值可能要改变前获得它们。这一生命周期返回的任何值将会 作为参数被传递给componentDidUpdate()。
componentDidUpdate:
componentDidUpdate(prevProps, prevState, snapshot)
在组件的更新已经同步到 DOM 中之后立刻被调用。
该方法不会在初始化渲染的时候调用。使用该方法可以在组件更新之后操作 DOM 元素。
生命周期---Unmounting(移除)
componentWillUnmount:
componentWillUnmount()
在组件从 DOM 中移除的时候立刻被调用。
在该方法中执行任何必要的清理,比如无效的定时器,或者清除在 componentDidMount 中创建的 DOM 元素。
总结:
以上就一口气通过大佬整理好的文章从头到尾强制自己看了一遍并形成自己的笔记,内容相当多,当然不可能记住,但是当在未来深入学习时如果遇到不会的可以回过头来查找一下,也是非常有意义的,为接下来进一步学习也奠定了良好的基础。