虚拟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 nullreturn <div>这是我的第一个函数组件</div>
}
// 渲染组件
ReactDOM.render(<Hello />, document.getElementById('root'))
传参
- 传参 :
//1. 头标签内演示
//2. age='30' age是字符串30age={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 nullreturn <div>这是我的第一个class组件</div>}
}
传参
- 传参 : 同函数组件一样
ReactDOM.render(<Child name='zs'/>, document.getElementById('root'))
- 接收参数
// 类组件
class Child extends React.Component { constructor(props) { super(props)// 接收方式1console.log(props);}render () { // 接收方式2console.log(this.props);// 解构const { name, age } = this.propsreturn <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 其实就是调用父类的constructorthis.name = 'zs'}
}let c = new Chinese()
console.log(c)
函数组件和类组件的小结
- 函数组件 : 函数创建组件
- 函数名首字母一定要大写
- 把组件当成标签使用
- 函数内部 通过 return jsx
- 类组件 : 类创建组件
- 首字母也要大写
- 一定要继承(extends) React.Component
- 类里面一定要有一个 render 函数
- render 函数里面通过 return jsx
- 类组件也是当成标签一样使用的
函数组件和类组件的区别?
- 函数组件 : 没有状态的, 没有自己的数据
- 类组件 : 有状态 , 有自己的数据
状态 State 的简单说明
state 的定义
- 方式1 : constructor 里面
constructor() { super()//设置状态1this.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指向了外部的thisthis.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>