react(一)
1、es6的class、箭头函数
1)ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } var p1 = new Point(1, 1); console.log(p1.toString()); // (1, 1) console.log(typeof Point, Point === Point.prototype.constructor); // function true
上面代码定义了一个“类”,可以看到里面有一个constructor
方法,这就是构造方法,而this
关键字则代表实例对象。也就是说,ES5 的构造函数Point
,对应 ES6 的Point
类的构造方法。
Point
类除了构造方法,还定义了一个toString
方法。注意,定义“类”的方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用new
命令,跟构造函数的用法完全一致。
构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。
class Point { constructor() { // ... } toString() { // ... } } // 等同于 Point.prototype = { constructor() {}, toString() {} };
类必须使用new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__ === p2.__proto__; //true
上面代码中,p1
和p2
都是Point
的实例,它们的原型都是Point.prototype
,所以__proto__
属性是相等的。
__proto__
并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法/属性。
2)Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class ColorPoint extends Point {
}
上面代码定义了一个ColorPoint
类,该类通过extends
关键字,继承了Point
类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point
类。
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } } let cp = new ColorPoint(1,1,'red'); console.log(cp, cp.toString()); // ColorPoint {x: 1, y: 1, color: "red"} "red (1, 1)"
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。另一个需要注意的地方是,在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super
方法才能返回父类实例。
如果子类没有定义constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
class ColorPoint extends Point { } // 等同于 class ColorPoint extends Point { constructor(...args) { super(...args); } }
let cp = new ColorPoint(25, 8, 'green'); cp instanceof ColorPoint // true cp instanceof Point // true
上面代码中,子类ColorPoint实例对象cp
同时是ColorPoint
和Point
两个类的实例,这与 ES5 的行为完全一致。
3)ES6 允许使用“箭头”(=>
)定义函数
var f = v => v; // 等同于 var f = function (v) { return v; };
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; };
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回。
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" });
2、react
- 第一个简单的react程序
参考:http://react.css88.com/docs/cdn-links.html
<div id="root"></div> <!-- react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能 --> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <!-- 上面的版本只适合开发环境,不适合生产环境。下面为压缩优化版本 <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> --> <!-- babel:将 JSX 语法转为 JavaScript 语法,这一步很消耗时间,实际上线的时候,应该将它放到服务器完成 --> <script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script> <!-- <script> Uncaught SyntaxError: Unexpected token <--> <!-- 控制台有警告:You are using the in-browser Babel transformer. Be sure to precompile your scripts for production 网页中实时将ES6代码转为ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本 React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是使用 JSX 的地方,都要加上 type="text/babel" --> <script type="text/babel"> const element = <h1>Hello, world</h1>; ReactDOM.render( element, document.getElementById('root') ); </script>
浏览器渲染结果:
a)crossorigin(实现更好的错误处理体验)
引入跨域的脚本(比如用了 apis.google.com 上的库文件),如果这个脚本有错误,因为浏览器的限制(根本原因是协议的规定),是拿不到错误信息的。当本地尝试使用 window.onerror
去记录脚本的错误时,跨域脚本的错误只会返回 Script error
。但 HTML5 新的规定,是可以允许本地获取到跨域脚本的错误信息,但有两个条件:一是跨域脚本的服务器必须通过 Access-Control-Allow-Origin
头信息允许当前域名可以获取错误信息,二是当前域名的 script
标签也必须指明 src
属性指定的地址是支持跨域的地址,也就是 crossorigin 属性。(参考:https://www.chrisyue.com/what-the-hell-is-crossorigin-attribute-in-html-script-tag.html)
b)更通用格式 UMD(Universal Module Definition)-希望提供一个前后端跨平台的解决方案
UMD的实现很简单,先判断是否支持NodeJS模块格式(exports是否存在),存在则使用NodeJS模块格式。再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。前两个都不存在,则将模块公开的全局(window或global)。
c)ReactDOM.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。
- JSX(JavaScript eXtension)- 允许 HTML 与 JavaScript 混写
参考:https://segmentfault.com/q/1010000003877594/a-1020000003878406
JavaScript 的一种扩展语法,推荐在 React 中使用这种语法来描述 UI 信息。React为了代码的可读性、更方便地创建虚拟DOM等原因,加入了一些类似XML的语法扩展。JSX是可选的,对于使用 React 而言不是必须的。
JSX 的基本语法规则:遇到 HTML 标签(以 <
开头),就用 HTML 规则解析;遇到代码块(以 {
开头),就用 JavaScript 规则解析。
JSX代码并不能直接运行,需要将它编译成正常的JavaScript表达式才能运行,jsxTransformer.js就是这一编译器的角色。React官方博客在2015年6月发布了一篇文章,声明用于JSX语法解析的编译器JSTransform已经过期,不再维护,React JS和React Native已经全部采用第三方Babel的JSX编译器实现。Babel作为专门的JavaScript语法编译工具,提供了更为强大的功能。
function formatName(user) { return user.firstName + ' ' + user.lastName; } const user = { firstName: 'Harper', lastName: 'Perez' }; const title = '我是h1'; const element = ( <div> <h1 title={title}>Hello, {formatName(user)}!</h1> <img className="img" src="http://img2.imgtn.bdimg.com/it/u=3723784612,2573513060&fm=200&gp=0.jpg"/> </div> ); ReactDOM.render( element, document.getElementById('root') );
为便于阅读,我们将 JSX 分割成多行。我们推荐使用括号将 JSX 包裹起来,虽然这不是必须的,但这样做可以避免分号自动插入的陷阱。
在属性中嵌入 JavaScript 表达式时,不要使用引号来包裹大括号。否则,JSX 将该属性视为字符串字面量而不是表达式。对于字符串值你应该使用引号,对于表达式你应该使用大括号,但两者不能同时用于同一属性。
jsx里的标签都应该闭合。
比起 HTML , JSX 更接近于 JavaScript , 所以 React DOM 使用驼峰属性命名约定, 而不是HTML属性名称。例如,class
在JSX中变为className。
render()函数中返回的所有元素需要包裹在一个"根"元素里面。
React DOM 会将元素及其子元素与之前版本逐一对比, 并只对有必要更新的 DOM 进行更新, 以达到 DOM 所需的状态。
- 组件(Components) 和 属性(Props)
Props 是只读的:无论你用函数或类的方法来声明组件, 它都无法修改其自身 props.
1)函数式组件
所有的React组件都有一个render
函数,它指定了React组件的HTML输出。
<div id="root"></div>
以下代码在页面上渲染 “Hello, Sara”
// 接收一个 props 参数, 并返回一个 React 元素 function Welcome(props) { return <h1>Hello, {props.name}</h1>; } // 用户定义组件(Welcome 组件)将 JSX 属性以一个单独对象的形式传递给相应的组件,我们将其称为 “props” 对象 const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );
组件名称总是以大写字母开始,否则会报错;组件可以在它们的输出中引用其它组件。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } function App() { return ( <div> <Welcome name="Sara" /> <Welcome name="Cahal" /> <Welcome name="Edite" /> </div> ); } ReactDOM.render( <App />, document.getElementById('root') );
2)提取组件
不要害怕把一个组件分为多个更小的组件。提取组件可能看起来是一个繁琐的工作,但是在大型的 Apps 中可以回报给我们的是大量的可复用组件。一个好的经验准则是如果你 UI 的一部分需要用多次 ,或者本身足够复杂,最好的做法是使其成为可复用组件。
props是只读的。
所有 React 组件都必须是纯函数,并禁止修改其自身 props 。当然,应用 UI 总是动态的,并且随时有可以改变。state(状态)
允许 React 组件在不违反上述规则的情况下, 根据用户操作, 网络响应, 或者其他, 来动态地改变其输出。
3)类组件
类组件允许我们在其中添加本地状态和生命周期钩子
class Clock extends React.Component { // 添加一个类构造函数初始化this.state constructor(props) { super(props); this.state = {date: new Date()}; } // 挂载—组件输出被渲染到 DOM 之后运行(设置定时器) componentDidMount() { // 如果需要存储一些不用于视觉输出的内容,则可以手动向类中添加额外的字段,如下面的timerID // 如果在 render() 方法中没有被引用, 它不应该出现在 state 中 this.timerID = setInterval( () => this.tick(), 1000 ); } // 卸载—DOM 被销毁时运行(清除计时器) componentWillUnmount() { clearInterval(this.timerID); } tick() { this.setState({ date: new Date() }); // this.state.date = new Date(); //这样将不会重新渲染一个组件 } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );
分析:
-
当
<Clock />
被传入ReactDOM.render()
时, React 会调用Clock
组件的构造函数。 因为Clock
要显示的是当前时间,所以它将使用包含当前时间的对象来初始化this.state
。我们稍后会更新此状态。 -
然后 React 调用了
Clock
组件的render()
方法。 React 从该方法返回内容中得到要显示在屏幕上的内容。然后,React 然后更新 DOM 以匹配Clock
的渲染输出。 -
当
Clock
输出被插入到 DOM 中时,React 调用componentDidMount()
生命周期钩子。在该方法中,Clock
组件请求浏览器设置一个定时器来一次调用tick()
。 -
浏览器会每隔一秒调用一次
tick()
方法。在该方法中,Clock
组件通过setState()
方法并传递一个包含当前时间的对象来安排一个 UI 的更新。通过setState()
, React 得知了组件state
(状态)的变化, 随即再次调用render()
方法,获取了当前应该显示的内容。 这次,render()
方法中的this.state.date
的值已经发生了改变, 从而,其输出的内容也随之改变。React 于是据此对 DOM 进行更新。 -
如果通过其他操作将
Clock
组件从 DOM 中移除了, React 会调用componentWillUnmount()
生命周期钩子, 所以计时器也会被停止。
正确地使用 State(状态)
a)不要直接修改 state。
例如,这样将不会重新渲染一个组件:this.state.date = new Date();
用 setState()
代替:this.setState({ date: new Date() });
唯一可以分配 this.state
的地方是构造函数。
b)state更新可能是异步的
React 为了优化性能,有可能会将多个 setState()
调用合并为一次更新
class Clock extends React.Component { // 添加一个类构造函数初始化this.state constructor(props) {
super(props); this.state = { date: new Date(), counter: 0 }; } // 挂载—组件输出被渲染到 DOM 之后运行(设置定时器) componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ); } // 卸载—DOM 被销毁时运行(清除计时器) componentWillUnmount() { clearInterval(this.timerID); } tick() { this.setState({ date: new Date() }); this.setState((prevState, props) => ({ counter: prevState.counter + parseInt(props.increment) })); } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> <h2>counter:{this.state.counter}</h2> </div> ); } } ReactDOM.render( <Clock increment = '2'/>, document.getElementById('root') );
因为 this.props
和 this.state
可能是异步更新的,你不能依赖他们的值计算下一个state。例如, 以下代码可能导致 counter
(计数器)更新失败
// 错误 this.setState({ counter: this.state.counter + this.props.increment, });
要弥补这个问题,使用另一种 setState() 的形式,它接受一个函数而不是一个对象。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数。
// 正确 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));
c)state更新会被合并
当你调用 setState()
, React 将合并你提供的对象到当前的状态中。this.setState({comments})会完全替换this.state.comments
4)数据向下流动
一个组件可以选择将 state(状态) 向下传递,作为其子组件的 props(属性)。这通常称为一个“从上到下”,或者“单向”的数据流。任何 state始终由某个特定组件所有,并且从该 state导出的任何数据 或 UI 只能影响树中 “下方” 的组件。
class Clock extends React.Component { // ... render() { return ( <div> <FormattedDate date={this.state.date}/> </div> ); } } function FormattedDate(props) { return <h2>It is {props.date.toLocaleTimeString()}.</h2> } ReactDOM.render( <Clock />, document.getElementById('root') );