React
React 是⼀个声明式,⾼效且灵活的⽤于构建⽤户界⾯的 JavaScript 库。使⽤ React 可以将⼀ 些简短、独⽴的代码⽚段组合成复杂的 UI 界⾯,这些代码⽚段被称作“ 组件” 。
MVC与MVVM
- MVC
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
这里主要讲的是前端的MVC实现,不要跟后端的MVC搞混了。它的目的是:
- 代码复用;
- 划分职责,方便后期维护;
Model(模型):负责保存应用数据,与后端数据进行同步
View(视图):负责视图展示,将model中的数据可视化
Controller(控制器):负责业务逻辑,根据用户行为对Model数据进行修改
// model
var myapp = {}; // 创建这个应⽤对象
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
/* 观察者模式 */
var self = this,
views = [];
this.register = function(view) {
views.push(view);
};
this.notify = function() {
for(var i = 0; i < views.length; i++) {
views[i].render(self);
}
};
};
// view
myapp.View = function(controller) {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
/* 绑定事件 */
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};
// controller
myapp.Controller = function() {
var model = null,
view = null;
this.init = function() {
/* 初始化Model和View */
model = new myapp.Model();
view = new myapp.View(this);
/* View向Model注册,当Model更新就会去通知View啦 */
model.register(view);
model.notify();
};
/* 让Model更新数值并通知View更新视图 */
this.increase = function() {
model.add(1);
model.notify();
};
this.decrease = function() {
model.sub(1);
model.notify();
};
};
// init
(function() {
var controller = new myapp.Controller();
controller.init();
})();
- MVVM
MVVM:Model、View、ViewModel。
总结
- 这二者都是框架的设计模式,设计的目的都是为了解决Model和View的耦合问题
- MVC出现比较早主要应用在后端,如Spring MVC、ASP.NET MVC等,在前端领域早期也有应用,如Backbone.js。优点是分层清晰,缺陷是数据流混乱,灵活性带来的维护问题。
- MVVM在前端领域有广泛应用,它不仅解决了MV耦合问题,还同时解决了维护二者映射关系的大量繁杂代码和DOM操作代码,在提高开发效率、可读性同时还保持了优越的性能表现。
JSX语法
JSX称为JS的语法扩展,将UI与逻辑层耦合在组件⾥,⽤{}标识
因为 JSX 语法上更接近 JS ⽽不是 HTML,所以使⽤ camelCase(⼩驼峰命名)来定义属性的名称; JSX ⾥的 class 变成了 className,⽽ tabindex 则变为 tabIndex。
可以用babel官网,查看转换效果
JSX 防止注入攻击
你可以安全地在 JSX 当中插入用户输入内容:
const title = response.potentiallyMaliciousInput;
直接使用是安全的:
const element = <h1>{title}</h1>;
jsx如何防止XSS攻击(原理)
React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<>
这几个字符进行转义,转义部分源码如下:
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '"';
break;
case 38: // &
escape = '&';
break;
case 39: // '
escape = ''';
break;
case 60: // <
escape = '<';
break;
case 62: // >
escape = '>';
break;
default:
continue;
}
}
这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:
// 一段恶意代码
<img src="empty.png" onerror ="alert('xss')">
// 转义后输出到 html 中
<img src="empty.png" onerror ="alert('xss')">
JSX 实际上是一种语法糖,Babel 会把 JSX 编译成 React.createElement()
的函数调用,最终返回一个 ReactElement
,以下为这几个步骤对应的代码:
// JSX
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 通过 babel 编译后的代码
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
// React.createElement() 方法返回的 ReactElement
const element = {
$$typeof: Symbol('react.element'),
type: 'h1',
key: null,
props: {
children: 'Hello, world!',
className: 'greeting'
}
...
}
我们可以看到,最终渲染的内容是在 Children 属性中,那了解了 JSX 的原理后,我们来试试能否通过构造特殊的 Children 进行 XSS 注入,来看下面一段代码:
const storedData = `{
"ref":null,
"type":"body",
"props":{
"dangerouslySetInnerHTML":{
"__html":"<img src=\"empty.png\" onerror =\"alert('xss')\"/>"
}
}
}`;
// 转成 JSON
const parsedData = JSON.parse(storedData);
// 将数据渲染到页面
render () {
return <span> {parsedData} </span>;
}
这段代码中, 运行后会报以下错误,提示不是有效的 ReactChild。
Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.
我们看一下 ReactElement 的源码:
const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 这个 tag 唯一标识了此为 ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 元素的内置属性
type: type,
key: key,
ref: ref,
props: props,
// 记录创建此元素的组件
_owner: owner,
};
...
return element;
}
注意到其中有个属性是 $$typeof`,它是用来标记此对象是一个 `ReactElement`,React 在进行渲染前会通过此属性进行校验,校验不通过将会抛出上面的错误。React 利用这个属性来防止通过构造特殊的 Children 来进行的 XSS 攻击,原因是 `$$typeof
是个 Symbol 类型,进行 JSON 转换后会 Symbol 值会丢失,无法在前后端进行传输。如果用户提交了特殊的 Children,也无法进行渲染,利用此特性,可以防止存储型的 XSS 攻击。
可能引起攻击的语法
- 使用 dangerouslySetInnerHTML
- 通过用户提供的对象来创建React组件
- 使用用户输入的值来渲染a标签的href属性,或类似img的src属性等
生命周期
由上图可知,React 16.8+的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。
当你使用生命周期钩子时候,你怎么优化?
React 16之后有三个生命周期被废弃:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
挂载阶段
constructor
如果不初始化 state 或不进⾏⽅法绑定,则不需要为 React 组件实现构造函数。
- 通过给 this.state 赋值对象来初始化内部 state。
- 为事件处理函数绑定实例
getDerivedStateFromProps
静态方法,当接收到新的
props
去更新state
时,可以使用getDerivedStateFromProps
render
纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
componentDidMount
组件挂载之后调用,可以操作dom与网络请求
更新阶段
- getDerivedStateFromProps 此方法在更新挂载阶段都可能会调用
- shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
,有两个参数nextProps
和nextState
,表示新的属性和变化之后的state
,返回一个布尔值,true
表示会触发重新渲染,false
表示不会触发重新渲染,默认返回true
,我们通常利用此生命周期来优化React程序性能 - render: 更新阶段也会触发此生命周期
- getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
,这个方法在render
之后,componentDidUpdate
之前调用,有两个参数prevProps
和prevState
,表示之前的属性和之前的state
,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate
,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate
搭配使用 - componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
,该方法在getSnapshotBeforeUpdate
方法之后被调用,有三个参数prevProps
,prevState
,snapshot
,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate
返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至getSnapshotBeforeUpdate
,然后在componentDidUpdate
中统一触发回调或更新状态。
卸载阶段
- componentWillUnmount 当组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作。
其他声明周期钩子
- UNSAFE_componentWillMount
UNSAFE_componentWillMount() 在挂载之前被调⽤; 它在 render() 之前调⽤,因此在此⽅法中同步调⽤ setState() 不会⽣效; 需要的话⽤componentDidMount替代。
- UNSAFE_componentWillReceiveProps
UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调⽤; 如果你需要更新状态以响应 prop 更改(例如,重置它),你可以⽐较 this.props 和 nextProps 并在此 ⽅法中使⽤ this.setState() 执⾏ state 转换。
UNSAFE_componentWillUpdate
- 当组件收到新的 props 或 state 时,会在渲染之前调⽤ UNSAFE_componentWillUpdate(); - 使⽤此作为在更新发⽣之前执⾏准备更新的机会; - 初始渲染不会调⽤此⽅法;
State
1. setState
构造函数是唯⼀可以给state赋值的地⽅
this.setState({comment: 'Hello'});
2. state更新可能是异步的
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
3. state更新会合并
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
// 相当于{post: response.posts, ...otherState}
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
// setState 异步
// 异步⽬的:batch 处理,性能优化
1. 合成事件
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新前的val --> 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
2. ⽣命周期
class App extends Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
3. 原⽣事件
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
4. setTimeout
class App extends Component {
state = { val: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
5. 批处理
class App extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
}
render() {
return (
<div onClick={this.batchUpdates}>
{`Counter is ${this.state.val}`} // 1
</div>
)
}
}
- setState 只在合成事件和⽣命周期中是“异步”的,在原⽣事件和 setTimeout 中都是同步的;
- setState的“异步”并不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的, 只是合成 事件和钩⼦函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形式 了所谓的“异步”, 当然可以通过第⼆个参数 setState(partialState, callback) 中的callback拿到更新后 的结果。
- setTimeout 中不会批量更新,在“异步”中如果对同⼀个值进⾏多次 setState , setState 的批量更新策 略会对其进⾏覆盖,取最后⼀次的执⾏,如果是同时 setState 多个不同的值,在更新时会对其进⾏合并 批量更新。
事件处理
- this绑定问题:在用户自定义的函数中没有this。
- React的数据流是单向的,没有数据绑定。
注意::
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写。且事件名称之后不能加 (),否则会直接执行
- 不能通过返回 false 的方式阻止默认行为。必须显式的使用 preventDefault
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
React组件中的事件处理函数
- constructor函数中bind
class ReactEvent extends Component {
constructor(props) {
super(props);
//强制绑定
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Click');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
- 使用箭头函数
- render中使用箭头函数
class ReactEvent extends Component {
handleClick() {
console.log('Click');
}
render() {
return <button onClick={() => this.handleClick()}>Click Me</button>;
}
}
- 使用class fields语法
class ReactEvent extends Component {
//此函数会被绑定到ReactEvent类的实例
handleClick = () => {
console.log('Click');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
- 在render中使用bind
class ReactEvent extends Component {
handleClick() {
console.log('Click');
}
render() {
return <button onClick={this.handleClick.bind(this)}>Click Me</button>;
}
}
(性能不太确定?)
影响 | constructor函数中bind | 使用class fields语法 | render中使用箭头函数 | 在render中使用bind |
---|---|---|---|---|
render时生成新函数 | 否 | 否 | 是 | 是 |
性能 | 无影响 | 无影响 | 有影响 | 有影响 |
可直接携带参数 | 否 | 否 | 是 | 是 |
简洁性 | 不好 | 好 | 好 | 好 |
事件处理中的参数传递
1、 传递额外参数
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
2、 接收自定义属性
handleClick = (e) =>{
console.log("ID:",e.target.dataset.id);
}
this.state.list.map((item)=>{
//render中使用箭头函数
return <button key={item.id} data-id={item.id} onClick={ this.handleClick }>{item.name}</button>
列表&key
元素的 key 只有放在就近的数组上下文中才有意义。
function ListItem(props) {
// 正确!这里不需要指定 key:
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下文中被指定
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。