虚拟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 }) { ... }
- 函数的参数接收 props
箭头函数改造
const Child = () => (
<div>
<div>这是一个div</div>
<div>这是一个div</div>
</div>
)
const Hello = ()=> <div>这是哈哈的啊</div>
组件2 - 类组件
基本使用
- 类组件 : 使用 ES6 中class 创建的组件, 叫
类组件
- 约定 (同 函数组件)
- 其他约定1 : 类组件必须继承自
React.Component 父类
, 然后,才可以使用父类中提供的属性或方法 - 其他约定2 : 必须提供
render 方法
, 来指定要渲染的内容, render 方法必须有返回值
- 其他约定1 : 类组件必须继承自
// 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>