React基础学习(第二天)

虚拟DOM

  • JSX 涉及到 虚拟DOM ,简单聊一下

定时器渲染问题

// 方法
function render() {
  //2. 创建react对象
  let el = (
    <div>
      <h3>时间更新</h3>
      <p>{ new Date().toLocaleTimeString()}</p>
    </div>
  )
  //3. 渲染
  ReactDOM.render(el, document.getElementById('root'))
}

//4. 开启一个定时器, 每秒渲染一次
setInterval(() => {
  render()
}, 1000)

渲染模式 :

/**
 * 最早的更新模式
 * 1. 数据
 * 2. 模板
 * 3. 数据+模板 => 真实的DOM
 * 4. 数据变了 => 最简单直接的方式 => 最新数据+模板 => 新的DOM
 * 5. 新的DOM 把 旧的DOM完全直接替换掉
 * 6. 显示新的DOM
 *
 * 缺点 : 完全替换,  性能不好
 *
 * 1. 数据
 * 2. 模板
 * 3. 数据+模板 => 真实的DOM
 * 4. 数据变化了 => 最新的数据 + 模板 => 新的DOM
 * 5. 新的DOM 和 旧的DOM 进行一一比较, 找到需要更新的地方
 * 6. 只需要更新需要改变的地方即可
 *
 * 优点 : 不再是全部替换掉,
 * 缺点 : DOM比较就有性能问题了 , 会有多余的DOM和属性进行比较(打印一级属性),有损性能
 *  p 和 p对比  h3 和h3对比(多余的对比)
 *
 * 1. 数据
 * 2. 模板
 * 3. 数据 + 模板 => 虚拟DOM (js对象) => 真实的DOM
 * 4. 数据发生改变(zs=>ls) => 最新的数据 + 模板 => 新的虚拟DOM
 * 5. 新的虚拟DOM 和 旧的虚拟DOM 通过 diff算法 进行比较
 * 6. 找到有差异的地方,(需要更新的地方)
 * 7. 更新一下就可以看到最新的DOM了
 */
  • 打印属性 :
let root = document.querySelector('#root')

let str = ''
let count = 0
for (let k in root) {
  str += k + ' '
  count++
}
console.log(str, count)
  • 查看图片 : 演示对比找差异渲染
  • 文字描述 :
这就是所谓的 Virtual DOM 算法。包括几个步骤:

- 1.用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
- 2.当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 3.2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了

DIff 算法

React 中有两种假定:

  • 1 两个不同类型的元素会产生不同的树
  • 2 开发者可以通过 key 属性指定不同树中没有发生改变的子元素

Diff 算法的说明 - 1

  • 如果两棵树的根元素类型不同,React 会销毁旧树,创建新树
// 旧树
<div>
  <Counter />
</div>

// 新树
<span>
  <Counter />
</span>

执行过程: 删除 div , 创建 span

Diff 算法的说明 - 2

  • 对于类型相同的 React DOM 元素,React 会对比两者的属性是否相同,只更新不同的属性
  • 当处理完这个 DOM 节点,React 就会递归处理子节点。
// 旧
<div className="before" title="stuff"></div>
// 新
<div className="after" title="stuff"></div>
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}}></div>
// 新
<div style={{color: 'green', fontWeight: 'bold'}}></div>
只更新:color属性

Diff 算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>1</li>
  <li>2</li>
</ul>

// 新
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

执行过程:
React会匹配新旧两个<li>1</li>,匹配两个<li>2</li>,然后添加 <li>3</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>1</li>
  <li>2</li>
</ul>

// 新
<ul>
  <li>3</li>   li3 插入最前面
  <li>1</li>
  <li>2</li>
</ul>

执行过程:
React将改变每一个子节点,而非保持 <li>1</li><li>2</li> 不变

key 属性

为了解决以上问题,React 提供了一个 key 属性。当子节点带有 key 属性,React 会通过 key 来匹配原始树和后来的树。

// 旧
<ul>
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

// 新
<ul>
  <li key="3">3</li>
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

执行过程:
 现在 React 知道带有key '3' 的元素是新的,对于 '1''2' 仅仅移动位置即可

补充说明:

  • key 属性在 React 内部使用,但不会传递给你的组件

  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>

  • 注意:key 只需要保持与他的兄弟节点唯一即可,不需要全局唯一

  • 注意:尽可能的减少数组 index 作为 key,数组中插入元素的等操作时,会使得效率底下 。 如果数组比较简单, 开发中没有删除和移动操作,使用 index 也是可以的

组件

组件1 - 函数组件

基本使用

  • 函数组件 : 使用函数创建的组件叫组函数组件
  • 约定:
    • 约定1 : 组件名称必须是大写字母开头, 也就是函数名称需要首字母大写
    • 约定2 : 函数组件必须有返回值
      • 不渲染内容 : return null
      • 渲染内容 : return JSX
    • 约定3 : 只能有一个唯一的根元素
    • 约定4 : 结构复杂, 使用() 包裹起来
  • 使用 : 把 组件名 当成 标签名 一样使用
// 函数组件
function Hello() {
  // return null
  return <div>这是我的第一个函数组件</div>
}
// 渲染组件
ReactDOM.render(<Hello />, document.getElementById('root'))

传参

  • 传参 :
//1. 头标签内演示
//2. age='30'  age是字符串30
     age={30}   age是number 30
ReactDOM.render(<Child name='zs' age={30}/>, document.getElementById('root'))
  • 接收 :
    • 函数的参数接收 props
      • funciton Hello (props) { ... }
    • 读取 : { props.name } 、 { props.age }
    • 注意 :
      • 传过来的props 不能添加属性
      • 传过来的props 不能修改属性值
    • 也可以直接解构 props里的属性
      • funciton Hello ({ name, age }) { ... }

箭头函数改造

const  Child = () => (
  <div>
    <div>这是一个div</div>
    <div>这是一个div</div>
  </div>
)

const Hello = ()=> <div>这是哈哈的啊</div>

组件2 - 类组件

基本使用

  • 类组件 : 使用 ES6 中class 创建的组件, 叫类组件
  • 约定 (同 函数组件)
    • 其他约定1 : 类组件必须继承自 React.Component 父类 , 然后,才可以使用父类中提供的属性或方法
    • 其他约定2 : 必须提供 render 方法, 来指定要渲染的内容, render 方法必须有返回值
// 2 创建类组件
class Hello extends React.Component {
  // 钩子 
  render() {
    // return null
    return <div>这是我的第一个class组件</div>
  }
}

传参

  • 传参 : 同函数组件一样
ReactDOM.render(<Child name='zs'/>, document.getElementById('root'))
  • 接收参数
// 类组件
class Child extends React.Component { 

  constructor(props) { 
    super(props)
    // 接收方式1
     console.log(props);
  }
  render () { 
    // 接收方式2
    console.log(this.props);
    // 解构
    const { name, age } = this.props
    
    return <div>哈哈{ this.props.name }</div>
  }
}

ES6 - class

介绍

 ** es6 之前 创建对象 都是通过构造函数 实现的, 给原型添加方法,  给实例添加属性
 * es6 之后, 给我们提供了一个字段 class 
 *  通过class 创建对象 
 *   class :*     -: 一类对象, 对象的抽象 , 我们可以通过类创建对象
 *     - 动物        人 
 * 
 *     - 对象: 具体的事物, 它有特征(属性)和行为(方法)
 *     -//鸟     张三/王春春
 */

使用 class 创建对象

  • 使用class 创建一个类 class Person { }
  • 创建对象 let p = new Person()
  • 添加属性 在类里面的 constructor() { } 的里面添加属性
  • 添加方法 直接在类里面添加方法
class Person { 
  constructor() { 
    this.name = 'zs';
    this.age = 30
  }
  say () { 
      console.log('说话了');
   }
}

let p = new Person()
console.log(p);
p.say()

继承

  • 继承 : 之前混入, 原型继承… 都是对象继承… (对象与对象之间,只要拿过来用就是继承)
  • class 继承 : 是 类与类之间的继承 extends
// 人
class Person {
  constructor() {
    this.maxAge= 120
  }
}

let p = new Person()
console.log(p)

// ---------------------------------------------
/**
 * 以后凡是 extends 继承
 * 当前类里面的 constructor 里面一定要加上super()
 * 因为底层都是给我们加的super,所以我们才能够继承过来,
 * 如果我们直接写 constructor,而不写super,意外着,底层的constructor会被覆盖掉
 */
// 中国人
class Chinese extends Person {
  constructor() {
    super() // super 其实就是调用父类的constructor
    this.name = 'zs'
  }
}

let c = new Chinese()
console.log(c)

函数组件和类组件的小结

  • 函数组件 : 函数创建组件
    • 函数名首字母一定要大写
    • 把组件当成标签使用
    • 函数内部 通过 return jsx
  • 类组件 : 类创建组件
    • 首字母也要大写
    • 一定要继承(extends) React.Component
    • 类里面一定要有一个 render 函数
    • render 函数里面通过 return jsx
    • 类组件也是当成标签一样使用的

函数组件和类组件的区别?

  • 函数组件 : 没有状态的, 没有自己的数据
  • 类组件 : 有状态 , 有自己的数据

状态 State 的简单说明

state 的定义

  • 方式1 : constructor 里面
 constructor() { 
    super()
     
    //设置状态1
    this.state = {
      name : 'zs'
    }
  }
  • 方法2 : 属性初始化语法
// 设置状态2
state = {
    name :'zhangsan '
 }

获取 状态 值 :

 // 直接获取
 <p> { this.state.name }</p>
 
 // 解构获取
 const { name } = this.state
 <p> { name }</p>

修改 状态 值

// 钩子函数 -  组件挂载完全  render调用完后会调用
componentDidMount() {
    
    //1. 直接修改
    // 如果使用 this.state.name = '春春' , 这样只会修改state里面的数据,但是无法更新视图
    // this.state.name = '春春'
    
    //2. 使用 setState 修改
    // 1-修改数据 2-重新调用render, 更新视图
    this.setState({
        name: '春春'
    })
}

安装 React 插件 ==> 查看 state 的值

  • react-developer-tools.crx
  • 安装步骤 : 后缀crx 改为 zip, 解压到当前文件, 安装 拓展程序

使用 state 修改定时器

//2. 类组件
class Child extends React.Component {
  state = {
    time : new Date()     # + 
  }
  render() {
    return (
      <div>
        <h3>我是h3</h3>
        <p>{ this.state.time.toLocaleTimeString() }</p>   # + 
      </div>
    )
  }

  componentDidMount () { 

    setInterval(() => {

      this.setState({      # + 
        time: new Date()   # + 
      })                   # + 
      
    }, 1000);
  }
}

总结 :

  • 函数组件
    • 没有状态 ( 没有自己的私有数据 )
    • 木偶组件, 组件一旦写好, 基本就不会改变
    • 传参 : function Child( props ) { } , props 只读
  • 类组件
    • 有状态 ( 有自己的私有数据 state )
    • 智能组件, 状态发生改变, 就会更新视图
    • 传参 :
      • this.props
  • 优点
    • 类组件 : 有状态,有生命周期钩子函数, 功能比较强大
    • 函数组件 : 渲染更快
  • 以后区分使用 ?
    • 就看要不要状态 , 要状态(类组件) , 不要状态 (函数组件)
  • props 和 state 区别?
    • state - 自己的私有数据 类似 vue 里的data
    • props - 外界传进来的 类似 vue 里面的props

事件处理

事件注册

  • 以前注册事件
<button onclick='fn'>按钮</button>
  • react 中注册事件
    • 注册事件属性采用的是驼峰的 onClick=…
    • 注册事件的事件处理函数 , 写为函数形式,不能为字符串
    • onClick={ this.fn }, {} 可以拿到它的原始类型
// 注册事件
<button onClick={ this.fn }>按钮</button>

// 事件处理函数
fn() {
    console.log('我被点击了')
}

事件中 this 的处理

  • 演示this问题
// 注册
<button onClick={this.fn}>按钮</button>
// 事件
fn () { 
    console.log('点击了');
    this.setState({
        name : 'ls'
    })
}
// 报错 : Uncaught TypeError: Cannot read property 'setState' of undefined
// react 中的this=undefined  是需要处理的
  • bind 和 call 的回忆使用
function f() {
  console.log(this)
}

let obj = { name: 'zs' }

bind 的使用
f.bind(obj) :  f里面的this 指向了obj
绑定之后没有打印,不是bind出了问题,而是bind和call,和apply()
call和apply 1-调用 2-指向
bind   1-指向 2-返回一个新函数
let newF = f.bind(obj)

newF()
  • 方式1 : bind
- 第一种
 constructor() { 
    super()
    this.fn1 = this.fn1.bind(this)
  }
 - 第二种 : <button onClick={   this.fn1.bind(this)    }>按钮</button>
  • 方式2 : 属性初始化语法
// 注册事件
return <button onClick={this.fn2}>按钮</button>
// 属性初始化语法
fn2 = () => { 
   // 箭头函数里面的this指向了外部的this
     this.setState({
         name : 'ls'
     })
 }
  • 方式3 : 箭头函数
 <button onClick={     () =>  this.fn3()      }>按钮</button>
 // 箭头函数里面的this 指向外部的this
// 谁调当前this所在的函数, this就执行谁
fn3(){
  ...
}
  • 总结
// 方法1 : bind (常用)
return <button onClick={     this.fn.bind(this) }>按钮</button>
// 方法2 : 属性初始化语法
return <button onClick={     this.fn1           }>按钮</button>
// 方法3 : 箭头函数
return <button onClick={    () => this.fn()     }>按钮</button>
  • 三者使用场景
- 方式1和方式3 常用
- 如果函数体内的代码比较少,比如就1~2=> 方式3
- 大部分情况下 => 优先使用 方式2
- 如果涉及到传参  => 方式3

点击事件传参 + 配合this处理

  • 演示效果
return <button onClick={this.fn(123)}>按钮</button>
不能这样传参,因为还没有开始点击,就已经开始调用了fn , 并且把参数传过去了
  • 处理this方式1 : bind
 // 注册事
return <button onClick={    this.fn.bind(this,123)     }>按钮</button>
 // 传参
fn( num ) { 
    console.log('点击了',num);
}
  • 处理this方式2 : 属性初始化语法 – 不能传参
  • 处理this方式3 : 箭头函数
 // 注册事
return <button onClick={   () => this.fn(123)          }>按钮</button>
 // 传参
fn( num ) { 
    console.log('点击了',num);
}

获取事件对象 + 配合this处理

// 方式1 : bind , `处理函数最后一个参数`就是 事件对象 e
return <button onClick={this.fn1.bind(this, 123, 456)}>按钮</button>
// 方式2 : 属性初始化语法  不传参数 默认形参就是事件对象 e
return <button onClick={this.fn2}>按钮</button>
// 方式3 : 箭头函数 通过箭头函数获取e,再继续传
return <button onClick={e => this.fn3(e)}>按钮</button>

猜你喜欢

转载自blog.csdn.net/weixin_44694682/article/details/107620994