React

React 是⼀个声明式,⾼效且灵活的⽤于构建⽤户界⾯的 JavaScript 库。使⽤ React 可以将⼀ 些简短、独⽴的代码⽚段组合成复杂的 UI 界⾯,这些代码⽚段被称作“ 组件” 。

MVC与MVVM

  • MVC

image-20220410153617949

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

这里主要讲的是前端的MVC实现,不要跟后端的MVC搞混了。它的目的是:

  1. 代码复用;
  2. 划分职责,方便后期维护;

Model(模型):负责保存应用数据,与后端数据进行同步

View(视图):负责视图展示,将model中的数据可视化

Controller(控制器):负责业务逻辑,根据用户行为对Model数据进行修改

image-20220410211426219

// 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

image-20220410154311521

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官网,查看转换效果

image-20220504204251231

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 = '&quot;';
            break;
        case 38: // &
            escape = '&amp;';
            break;
        case 39: // '
            escape = '&#x27;';
            break;
        case 60: // <
            escape = '&lt;';
            break;
        case 62: // >
            escape = '&gt;';
            break;
        default:
            continue;
    }
}

这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:

// 一段恶意代码
<img src="empty.png" onerror ="alert('xss')"> 
// 转义后输出到 html 中
&lt;img src=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt; 

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属性等

生命周期

image-20220410230106681

由上图可知,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),有两个参数nextPropsnextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能
  • render: 更新阶段也会触发此生命周期
  • getSnapshotBeforeUpdate getSnapshotBeforeUpdate(prevProps, prevState),这个方法在render之后,componentDidUpdate之前调用,有两个参数prevPropsprevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用
  • componentDidUpdate componentDidUpdate(prevProps, prevState, snapshot),该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevPropsprevStatesnapshot,表示之前的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>
 )
 }
}
  1. setState 只在合成事件和⽣命周期中是“异步”的,在原⽣事件和 setTimeout 中都是同步的;
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的, 只是合成 事件和钩⼦函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形式 了所谓的“异步”, 当然可以通过第⼆个参数 setState(partialState, callback) 中的callback拿到更新后 的结果。
  3. setTimeout 中不会批量更新,在“异步”中如果对同⼀个值进⾏多次 setState , setState 的批量更新策 略会对其进⾏覆盖,取最后⼀次的执⾏,如果是同时 setState 多个不同的值,在更新时会对其进⾏合并 批量更新。

事件处理

  1. this绑定问题:在用户自定义的函数中没有this。
  2. 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组件中的事件处理函数

  1. 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>; 
    } 
}
  1. 使用箭头函数
  • 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')
);

参考文章


看见了
876 声望16 粉丝

前端开发,略懂后台;