React核心内容归纳总结 【*】
状态、属性、组件API、组件的生命周期
当react的状态改变时,自动执行this.render()方法更新组件
ES6写React的时候,事件里不会自动绑定this,需要自己绑定,或者直接在constructor里写方法
constructor(props) {
super(props);
this.state = {
liked: false
};
this.handleClick = (e) => {
this.setState({liked: !this.state.liked});
};
}
状态
import React from 'react';
class LikeButton extends React.Component {
// es6用constructor代替getInitialState
constructor(props) {
super(props);
this.state = {
liked: false
}
}
handleClick (e) {
this.setState({liked: !this.state.liked})
}
render () {
let text = this.state.liked ? '喜欢': '不喜欢';
return (
<div>
<p onClick={this.handleClick.bind(this)}>
你喜欢红茶吗?{text}
</p>
</div>
)
}
}
export default LikeButton;
props
HelloMessage.jsx
import React from 'react';
class HelloMessage extends React.Component {
render () {
return (
<div>
Hello, {this.props.name}
</div>
)
}
}
export default HelloMessage;
main.js
import React from 'react';
import ReactDOM from 'react-dom';
import HelloMessage from './component/HelloMessage';
const app = document.getElementById('app');
ReactDOM.render(<HelloMessage name="Runoob"/>, app);
props验证
可以保证应用组件被正确使用
React.PropTypes提供很多验证器(validator)来验证传入数据是否有效。当props传入无效数据时,JavaScript控制台会抛出错误。
import React from 'react';
class HelloMessage extends React.Component {
render () {
return (
<div>
Hello, {this.props.name}
</div>
)
}
}
// getDefaultProps要写在外部
HelloMessage.defaultProps = {
name: 'this is default name'
};
HelloMessage.propTypes = {
name: React.PropTypes.string.isRequired
};
export default HelloMessage;
更多验证器
React.createClass({
propTypes: {
// 可以声明 prop 为指定的 JS 基本数据类型,默认情况,这些数据是可选的
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,
// 可以被渲染的对象 numbers, strings, elements 或 array
optionalNode: React.PropTypes.node,
// React 元素
optionalElement: React.PropTypes.element,
// 用 JS 的 instanceof 操作符声明 prop 为类的实例。
optionalMessage: React.PropTypes.instanceOf(Message),
// 用 enum 来限制 prop 只接受指定的值。
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
// 可以是多个对象类型中的一个
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),
// 指定类型组成的数组
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
// 指定类型的属性构成的对象
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
// 特定 shape 参数的对象
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),
// 任意类型加上 `isRequired` 来使 prop 不可空。
requiredFunc: React.PropTypes.func.isRequired,
// 不可空的任意类型
requiredAny: React.PropTypes.any.isRequired,
// 自定义验证器。如果验证失败需要返回一个 Error 对象。不要直接使用 `console.warn` 或抛异常,因为这样 `oneOfType` 会失效。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error('Validation failed!');
}
}
},
/* ... */
});
React组件API
setState 设置状态
replaceState 替换状态
setProps 设置属性
replaceProps 替换属性
forceUpdate 强制更新
findDOMNode 获取DOM节点,查找真实DOM
React.findDOMNode()只在mounted组件中调用,如果在组件的render()方法中调用React.findDOMNode()就会抛出异常。
React组件的生命周期
组件生命周期的三个状态
Mounting: 已插入真实DOM
Updating: 正在被重新渲染
Unmounting: 已移除真实DOM
生命周期方法
componentWillMount 在渲染前调用,在客户端也在服务端
componentDidMount 在第一次渲染后调用,只在客户端。
之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。
如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异部操作阻塞UI)。
componentWillReceiveProps 在初始化render时不会被调用,当组件接收到一个新的prop时被调用
shouldComponentUpdate 返回一个布尔值。当确认不需要更新组件时使用。在组件接收到新的state或新的props时被调用。在初始化时或使用forceUpdate时不被调用
componentWillUpdate 初始化不会调用。组件接收到新的state或者props但还没有render时被调用
componentDidUpdate 初始化不会调用。组件完成更新后立即调用。
componentWillUnmount 组件从DOM中移除的时候立刻被调用。
用state判断是否已经插入真实DOM
componentWillMount() {
this.mounted = false;
}
componentDidMount() {
this.mounted = true;
}
需求:写一个实例,在组件加载以后,通过componentDidMount设置一个定时器,改变组件的透明度并重新渲染
HelloMessage.jsx
import React from 'react';
class HelloMessage extends React.Component {
constructor (props) {
super(props);
this.state = {
opacity: 1.0
};
}
componentDidMount () {
this.timer = setInterval(() => {
let opacity = this.state.opacity;
opacity -= 0.05;
if(opacity <= 0.1) {
opacity = 1.0;
}
this.setState({
opacity: opacity
});
}, 100);
}
render () {
let myStyle = {
fontFamily: 'microsoft yahei',
width: '100%',
height: '100px',
background: 'red',
opacity: this.state.opacity
};
return (
<div style={myStyle}>
Hello, {this.props.name}
<p>{this.state.opacity}</p>
</div>
)
}
}
// getDefaultProps要写在外部
HelloMessage.defaultProps = {
name: 'this is default name'
};
HelloMessage.propTypes = {
name: React.PropTypes.string.isRequired
};
export default HelloMessage;
另一个实例,体现react的生命周期
import React from 'react';
class Content extends React.Component {
componentWillMount () {
console.log('组件将被插入到真实DOM');
}
componentDidMount () {
console.log('已经插入真实DOM');
}
componentWillReceiveProps (newProps) {
console.log('组件将要接收属性,新属性是:');
console.log(newProps);
}
shouldComponentUpdate (newProps, newState) {
return true;
}
componentWillUpdate (nextProps, nextState) {
console.log('组件将要更新,下一个属性:' + nextProps + ',下一个状态:' + nextState);
console.log(nextProps);
}
componentDidUpdate (prevProps, prevState) {
console.log('组件已更新,上一个属性:' + prevProps + ',上一个状态:' + prevState);
console.log(prevProps);
}
componentWillUnmount () {
console.log('已经移除真实DOM');
}
render () {
return (
<div>
次数:{this.props.myNum}
</div>
)
}
}
class Increment extends React.Component {
constructor (props) {
super(props);
this.state = {
data: 0
};
}
setNewNumber () {
this.setState({
data: this.state.data + 1
});
}
render () {
return (
<div>
<button onClick={this.setNewNumber.bind(this)}>Increment</button>
<Content myNum={this.state.data}/>
</div>
)
}
}
export default Increment;
https://github.com/dzfrontend/start-with-react.js
1.React 环境配置与调试技巧
1-1.使用npm配置react环境
安装react环境:
下面安装react开发所必须的插件,也可以直接在package.json所在的目录里面直接npm install安装所有的插件。
初始化:npm init
安装:npm install react react-dom babelify babel-preset-react babel-preset-es2015 --save
1-2.webpack热加载配置
安装webpack和webpack-dev-server:
npm install webpack -g 和--save-dev
npm install webpack-dev-server --save-dev
webpack.config.js
webpack.config.js里面为webpack打包的配置,最后生成打包文件bundle.js:
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: path.join(__dirname),
entry: "./src/js/index.js",
module: {
loaders: [
{
test: /\.js?$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
}
]
},
output: {
path: __dirname,
filename: "./src/bundle.js"
}
};
这里配置的webpack.config.js,命令行输入webpack-dev-server,将入口文件src/js/index.js生成打包文件/src/bundle.js。
所以在index.html里面只需要引入打包好的bundle.js文件就可以访问整个项目。访问http://localhost:8080/webpack-dev-server/就可以访问根目录index.html并且实现热加载自动刷新浏览器;
1-3.调试技巧- Chrome React插件的使用
谷歌应用商店搜索React Developer Tools安装
1-4.开发工具Atom介绍
atom开发相关插件
emmet 代码补全
atom-ternjs 对es5 es6 nodejs等语法支持
atom-beautify 对html css js等格式化代码
open-in-browser 打开浏览器
file-icons 文件图标
highlight-line 高亮行,光标定位
highlight-selected 高亮选择,选择相同的内容
2.React 组件基础
2-1.React组件
组件写在js文件里面
React导入:
在React组件定义之前需要导入
import React from 'react';
import ReactDOM from 'react-dom';
组件定义:
class + 组件名 + extends React.Component{
render(){
return(jsx代码)
}
}
规范:组件名大写
组件导出:
在React组件定义之后需要导出
export default
组件导入:
其他页面使用该组件用import导入
如import ComponentHeader from './components/header';
入口定义:
想要将组件显示到页面中,还需要定义入口:
ReactDOM.render('组件名',document.getElementById('id'));
2-2.React多组件嵌套
将三个组件< ComponentHeader />,< ComponentBody />和< ComponentFooter />写在一起,然后放到指定的入口最终显示到页面中
// 多组件嵌套
class Index extends React.Component{
render(){
return(
<div>
<ComponentHeader/>
<ComponentBody/>
<ComponentFooter/>
</div>
)
}
}
// 入口定义:
ReactDOM.render(
<Index/>,
document.getElementById('example')
);
2-3.JSX内置表达式
三元表达式:
绑定属性:
属性:{变量} 绑定变量不需要引号
JSX注释:
代码
class ComponentBody extends React.Component{
render(){
var username = 'dz';
var boolInput = true;
return(
<div>
<h1>这里是页面主体内容</h1>
{/*三元表达式*/}
<p>{username === '' ? '用户还未登录' : '登录用户名:' + username}</p>
{/*绑定属性*/}
<p><input type="submit" value="提交" disabled={boolInput} /></p>
</div>
)
}
}
2-4.React生命周期
生命周期函数图解:
3.React 属性与事件
3-1.state属性
初始化state: 放在构造函数constructor里
constructor() {
super(); //调用基类的所有的初始化方法
this.state = {
username: "Parry",
age: 20
}; //初始化赋值
}
修改state:
放在render函数里
this.setState({username:'db',age:19});
显示:
jsx里
{this.state.username} {this.state.age}
完整代码:
import React from 'react';
export default class ComponentBody extends React.Component{
constructor(){
super();//调用基类的所有的初始化方法
//初始化state,需要放在构造函数constructor里面
this.state = {
username:'dz',
age:20
};
}
render(){
setTimeout(() => {
//修改state
this.setState({username:'db',age:19});
},2000);
return(
<div>
<h1>这里是页面主体内容</h1>
<h1>state</h1>
<p>{this.state.username} {this.state.age}</p>
</div>
)
}
}
3-2.props属性
state对于模块属于自身属性,当一个组件想要接收参数的时候用props接收,props对于组件属于外来属性
1.组件传递参数
<ComponentBody userid={123456}/>
2.组件中接收参数
this.props.userid
3-3.事件与数据的双向绑定
事件的绑定:
首先在组件React.Component中写事件函数
changeUserInfo() {
this.setState({age: 21});
};
然后调用时绑定
render(){
return(
<div>
<p>事件绑定数据:{this.state.age}</p>
{/*事件绑定,这里bind(this)实现继承*/}
<input type="submit" value="改变1" onClick={this.changeUserInfo.bind(this)}/>
</div>
)
}
子页面向父页面传递参数的方法
在子页面中调用父页面传递过来的事件,用props接收事件函数,进行组件间的参数传递
在父组件中:
handleChildValueChange(event){
//event.target.value接收表单输入的值
this.setState({age: event.target.value});
}
render(){
return(
<div>
<p>事件绑定数据:{this.state.age}</p>
{/*子组件向父组件传递参数:需要父页面向子组件传递事件函数*/}
<ComponentBodyChild handleChildValueChange={this.handleChildValueChange.bind(this)}/>
</div>
)
}
在子组件ComponentBodyChild中:
render(){
return(
<div>
{/*子组件中用props接收事件函数*/}
<p>子页面输入:<input type="text" onChange={this.props.handleChildValueChange}/></p>
</div>
)
}
3-4.可复用组件
PropTypes属性验证:
PropTypes 提供很多验证器 (validator) 来验证传入数据的有效性。当向 props 传入无效数据时,JavaScript 控制台会抛出警告。
import React from 'react';
import PropTypes from 'prop-types'; //npm install prop-types
export default class ComponentBody extends React.Component{
render(){
return(
<div>
<p>接收父组件参数为:{this.props.userid}</p>
</div>
)
}
}
//PropTypes属性验证在类定义后追加属性propTypes
ComponentBody.propTypes = {
userid : PropTypes.number //userid只能为number,否则会报错
};
默认prop值:
//比如userid或username在父页面没有传递值的时候,需要设置默认值
const defaultProps = {
username:'这是一个默认用户名'
};
ComponentBody.defaultProps = defaultProps;
传递所有参数的快捷方式:
父页面向孙子页面传递多个参数的时候,在子页面用语法糖获取所有的参数
<Component {...this.props 其他属性="属性值"}
3-5.组件的Refs
组件的Refs:获取原生的html节点
import React from 'react';
import ReactDOM from 'react-dom';
export default class ComponentBody extends React.Component{
changeUserInfo1(){
//DOM操作第一种方式
var btn = document.getElementById('idButton');
console.log(btn);
ReactDOM.findDOMNode(btn).style.color = 'red';
};
changeUserInfo2(){
//DOM操作第二种方式
console.log(this.refs.refButton); //获取原生dom节点
this.refs.refButton.style.color = 'red';
};
render(){
return(
<div>
<input id="idButton" type="button" value="DOM操作第一种方式" onClick={this.changeUserInfo1.bind(this)}/>
<input ref="refButton" type="button" value="DOM操作第二种方式" onClick={this.changeUserInfo2.bind(this)}/>
</div>
)
}
}
推荐的是使用组件的Refs进行dom操作。
Refs是访问到组件内部DOM节点唯一可靠的方法;
Refs会自动管理销毁对子组件的引用;
不要在render或render之前对Refs进行调用,因为组件还没加载好;
不要滥用Refs,能用state解决的用state
3-6.独立组件间共享Mixins
Mixins在组件间进行事件的共享:不同组件间共用功能、共享代码
Mixins用ES6写法需要安装插件:https://github.com/brigand/react-mixin;ES5写法看react官方文档,代码用的是ES6写法。
安装:
npm install --save react-mixin@2
使用:
var reactMixin = require('react-mixin');
var someMixin = require('some-mixin');
class Foo extends React.Component {
render: function(){ return <div /> }
}
reactMixin(Foo.prototype, someMixin);
reactMixin(Foo.prototype, someOtherMixin);
看不懂上面的看demo,demo里面有。
4.React 样式
4-1.内联样式
内联样式:css需要用驼峰写法
外部样式:index.html文件中全局引用外部css文件,class需要改成className
内联样式中的表达式:三元表达式
// React导入
import React from 'react';
// 组件定义和导出:
export default class ComponentHeader extends React.Component{ //继承React.Component则这个类就是一个组件
constructor(){
super();
this.state = {
miniHeader: true
};
};
switchHeader(){
this.setState({
miniHeader: !this.state.miniHeader
});
};
render(){
const styleComponentHeader = {
header:{
backgroundColor:(this.state.miniHeader) ? '#c73949' : '#666',
color:'#fff',
paddingTop:(this.state.miniHeader) ? '15px' : '5px',
paddingBottom:(this.state.miniHeader) ? '15px' : '5px'
},
//还可以定义其他的样式
};
return(
<header style={styleComponentHeader.header} className="smallFontSize" onClick={this.switchHeader.bind(this)}>
<h1>这里是头部组件 点我</h1>
</header>
)
};
}
4-2.css模块化
安装style-loader、css-loader => css模块化
安装babel-plugin-react-html-attrs插件 => 可以直接写class不用改写className
在webpack.config.js的loaders里面加上css模块化的配置:
{
test: /\.css$/,
loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]'
}
css模块化:
import React from 'react';
//css模块化
var footerCss = require('../../css/footer.css');
export default class ComponentFooter extends React.Component{
render(){
console.log(footerCss);
return (
<footer class={footerCss.miniFooter}>
<h1>这里是尾部组件</h1>
</footer>
)
}
}
4-3.Ant Design样式框架
使用请看官方文档:https://ant.design/docs/react/introduce-cn
5.React Router
作用是在单页面中进行组件间的页面跳转。
github地址:https://github.com/ReactTraining/react-router
react-router现在到了4.x的版本,这里面使用的是3.0.5版本,有差异。
5.1.React Router
配置路由:
Rendering a Route:文档参考 https://github.com/reactjs/react-router-tutorial/tree/master/lessons/02-rendering-a-route
root.js作为页面入口文件,webpack-dev-server运行启动项目
import React from 'react';
import ReactDOM from 'react-dom';
// 导入路由组件
import Index from './index';
import ComponentList from './components/list';
//导入路由配置插件
import {Router,Route,hashHistory} from 'react-router';
//入口页面
export default class Root extends React.Component{
render(){
return (
//这里替换了之前的 Index,变成了程序的入口
// 用hashHistory实现路由,下面为路由配置:
<Router history={hashHistory}>
<Route component={Index} path="/"></Route>
<Route component={ComponentList} path="list"></Route>
</Router>
);
};
}
// 入口定义:
ReactDOM.render(<Root/>, document.getElementById('example'));
当访问根目录也就是访问http://localhost:8080/的时候,会访问Index组件,也就是< Route component={Index} path="/" >< /Route >这句话表示的意思;上面配置了两个路由http://localhost:8080/#/和http://localhost:8080/#/list分别链接到两个组件页面。
路由链接:
Navigating with Link:文档参考 https://github.com/reactjs/react-router-tutorial/tree/master/lessons/03-navigating-with-link
在路由配置好了之后,在任意一个页面都可以写上路由链接:
某个子页面:
import React from 'react';
//导入link组件
import {Link} from 'react-router';
export default class ComponentHeader extends React.Component{
render(){
return(
<div>
<h1>这里是头部组件</h1>
<ul>
<li><Link to={`/`}>首页</Link></li>
<li><Link to={`/list`}>list页面</Link></li>
</ul>
</div>
)
}
}
这样就可以访问配置的路由了。
路由嵌套:
组件Index路由嵌套一个组件ComponentDetails路由:
render(){
return (
<Router history={hashHistory}>
<Route component={Index} path="/">
<Route component={ComponentDetails} path="details"></Route>
</Route>
</Router>
);
};
嵌套后,还需要在Index组件中将嵌套路由展示出来
render(){
return(
<div>
{/* 路由嵌套展示区域 */}
<div>
{this.props.children}
</div>
</div>
)
}
5.2.React Router参数传递
如果链接后面加上访问参数:
<Link to={`/list`}>list页面</Link>
加上访问参数1234
<Link to={`/list/1234`}>list页面</Link>
则路由需要配置为:
加一个/:id
<Route component={ComponentList} path="list/:id"></Route>
接收页面传递的参数为:
{this.props.params.id}
虚拟DOM内部是如何工作的?
流程图展现VDOM在Preact中如何工作
虚拟DOM (VDOM 也叫 VNode)非常有魔力 ✨ 但是也非常复杂和难以理解😱. React, 在Preact和一些类似的JS库的核心代码中使用. 不幸的是我发现没有一篇好的文章或者文档简洁明了的来介绍它。 因此我决定自己写一篇.
注意: 这篇文章很长. 我已经添加尽可能多的图片来使其理解更简单一些,但是我发现这样的话,文章更长了.
我用的是 Preact’s 代码 和 VDOM,因为它很小,你可以在将来很舒适的阅读它。 但是我相信几乎所有的概念同样适用于React.
我希望你读完这篇文章后,能够很容易的理解像React或者Preact的库,甚至对你写出类似的库也是有帮助的
在这篇博客中,我将会举一些简单示例,并且复习一下不同的小知识,给你一个关于它们到底如何工作的概念。特别地,我会复习:
-
Babel 和 JSX
-
创建一个VNode - 一个简单的虚拟DOM元素
-
处理组件及子组件
-
初始化渲染并且创建一个DOM元素
-
重新渲染
-
移除DOM元素
-
替换DOM元素
关于这个demo:
这是一个简单过滤搜索应用, 仅包含有两个组件“FilteredList” 和 “List”。这个List组件渲染列表项(默认是“California” 和 “New York”)。这个应用有一个搜索的区域,可以根据字母来过滤列表项。非常的直观。
相关图片(点击放大,查看更多细节,原文在medium里是可以放大的,这里貌似不行)
大图
高级一点儿,我们用JSX写了组件,可以通过babel的命令行工具将其转换为原生的JS.然后Preact的“h”函数将它转换为虚拟DOM树(也称为 VNode)。最后Preact的虚拟DOM算法,根据虚拟DOM创建一个真实的DOM,来构成我们的应用。
大图
在我们深入理解VDOM的生命周期之前,让我们理解下JSX,它为库提供了基础
1. Babel 和 JSX
在React中,像Preact这样的库,没有HTML语法,取而代之的是一切皆Javascript。因此我们需要用Javascript来写HTML。但是用原生JS写DOM是一种噩梦。 😱
对于我们的应用,我们将会像下面这样书写HTML:
注意: 等会儿我会介绍“h”
这就是JSX的由来,JSX本质上允许我们在Javascript中书写HTML!并且允许我们在HTML中的{}号中使用JS的语法。
JSX帮助我们像下面这样很容易的书写组件:
将JSX树转换为Javascript
JSX很酷,但是不是合法的JS,但是根本上我们还是需要真实的DOM。JSX仅仅是帮助我们书写真实DOM的一种方法。除此之外,它毫无用处。
因此我们需要一种方法将JSX转换为正确的JSON对象(VDOM 也是一个“树”形的结构),我们需要将JSX作为创建真实DOM的基础。我们函数来做这样的事情.
在Preact中这个函数就是“h”函数.它作用和React中的React.createElement作用是一样的。
“h”是指 hyperscript - 一种可以通过JS来创建HTML的库。
但是怎样将JSX转换为“h”函数式的调用?这就是Babel的由来。Babel可以很轻松的遍历JSX的节点,然后将它们转换为“h”函数式的调用。
Babel JSX (React Vs Preact)
在React中babel会将JSX转换为React.createElement函数调用
左边: JSX 右边: React 的JS版本 (点击放大)
我们可以像下面这样增加[Babel Pragma]配置,可以很轻松为Preact的函数的名字起任何一个你想起的名字。
Option 1:
//.babelrc
{ "plugins": [
["transform-react-jsx", { "pragma": "h" }]
]
}
Option 2:
//Add the below comment as the 1st line in every JSX file
/** @jsx h */
“h” —通过Babel的配置 (点击放大)
挂载到真实DOM
组件的的render方法中的代码不仅被转换为“h”函数,而且开始挂载。
这是执行和一切的开始
//Mount to real DOM
render(<FilteredList/>, document.getElementById(‘app’));
//Converted to "h":
render(**h(FilteredList)**, document.getElementById(‘app’));
“h”函数的输出
The “h” function takes the output of JSX and creates something called a “VNode” (React’s “createElement” creates ReactElement). A Preact’s “VNode” (or a React’s “Element”) is simply a JS object representation of a single DOM node with it’s properties and children.
看起来像下面这样:
{
"nodeName": "",
"attributes": {},
"children": []
}
举个例子,我们的应用的Input表单的VNode像这样:
{
"nodeName": "input",
"attributes": {
"type": "text",
"placeholder": "Search",
"onChange": ""
},
"children": []
}
Note: “h” function doesn’t create the entire tree! It simply creates JS object for a given node. But since the “render” method already has the DOM JSX in a tree fashion, the end result will be a VNode with children and grand children that looks like a tree. 注意“h”函数不会创建完整的树 它仅仅对于给定的node创建了一个JS对象。但是。 最后的结果将会是一个带有子元素和看起来像树的重要子元素的VNode.
参考代码:
“h” :https://github.com/developit/preact/blob/master/src/h.js
VNode: https://github.com/developit/preact/blob/master/src/vnode.js
“render”:https://github.com/developit/preact/blob/master/src/render.js
“buildComponentFromVNode:https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102
好了,让我们看下虚拟DOM如何工作的?
Preact虚拟DOM的算法流程图
下面的流程图展现了组件和子组件如何被Preact创建,更新,删除的。也展现了生命周期的不同阶段,对应的回调函数被调用,像“componentWillMount”。
注意: 我们会一步一步的复习每一部分,如果你会觉复杂,不用担心。
是的,立马理解所有的知识很难。让我们一步一步得通过浏览不同的情景,来复习流程图的不同部分。
注意: 当讨论到关键的生命周期的部分我将会用黄色高亮。
情景 1: APP创建初始化1.1 — 对一个给定的组件创建一个VNode
黄色高亮区域对于一个给定的组件创建虚拟DOM数,初始化处理循环。注意没有为子组件创建虚拟DOM(这是个不同的循环)
黄色区域展示了虚拟DOM的创建
下面这张图片展示了当我们应用第一次加载的时候发生了什么。这个库最终为主要组件“FilteredList”创建了一个带有子元素和属性的VNode。
注意: 它连着调用了生命周期方法“componentWillMount” 和 “render”.(看上面图片绿色的部分)
(click to zoom)
这个时候,我们有了个“div”的父元素,它包含了子节点“input”和“list”。
引用:
大多数的生命周期事件,像componentWillMount,render等等:https://github.com/developit/preact/blob/master/src/vdom/component.js
1.2 — 如果不是一个组件,创建一个真实的DOM
这一步,它仅会对父元素div创建一个真实的DOM,并且对于子节点(“input” 和 “List”)重复这一步骤。
黄色的循环部分展现了子组件的创建。
这一步,下面的图片中仅仅“div”被显示出来了。
引用:
document.createElement: https://github.com/developit/preact/blob/master/src/dom/recycler.js
1.3 — 对所有的子元素重复这一步
这一步,对所有的子元素将会重复这一步。在我们的应用中,会对“input” 和 “List” 重复。
对每一个子元素重复
1.4 — 处理子元素,并且把它加到父元素上.
这一步我们将会处理子树。既然“input”有一个父元素“div”,我们将会把input作为一个子元素加到div中。然后停止,返回创建“List”(第二个div子元素)。
结束处理子树
这一步,我们的应用看起来像下面这样:
注意: “input”被创建后,由于没有任何一个子元素,不会理解循环和创建“List”。它会首先将“input”加入到父元素“div”中,然后返回处理“List”。
引用:
appendChild: https://github.com/developit/preact/blob/master/src/vdom/diff.js
1.5 处理子组件(们)
控制流程回到1.1,对“List”组件开始所有的。但是“List”是一个组件,它调用“List”组件的方法render,得到一组新的虚DOM,像下面这样
对一个子组件重复所有的操作
对List组件重复操作之后,返回VNode像下面这样:
引用:
“buildComponentFromVNode:https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102
1.6 对所有子节点重复1.1到1.4步骤
它会再一次对每一个节点重复上面的步骤。一旦它到达子节点,就会把它加入到节点的父节点,并且重复处理。
重复这一步骤,直到所有的父子节点被创建和添加。
下面的图片展示了每个节点的添加(提示: 深度优先)
真实的DOM树如何被虚拟DOM算法创建的。
1.7 结束处理
这一步,结束处理。它仅对所有的组件调用了“componentDidMount”(从子组件到父组件)并且停止。
重要提示: 一旦所有所有做完之后,一个真实DOM的引用被添加到每个组件的实例上去。这个引用被用来更新(创建,更新,删除)比较,避免重复创建同样的DOM节点。
情景 2: 删除叶子节点
当我们输入“cal” 关键字,确认。将会移除掉第二个list节点,保留所有的父节点。
让我们看下,怎么样看这个情景?
2.1 像之前那样创建VNodes.
当初始化渲染之后,未来的每一个变化都是一个更新。当需要创建VNodes时,更新的周期工作跟创建的周期非常的相似,并且再一次创建所有的VNodes。
既然是一个组件的更新(不是创建),每个组件和子组件都会调用“componentWillReceiveProps”, “shouldComponentUpdate”, 和 “componentWillUpdate”
另外, update cycle, 如果那些元素已经存在不会重复创建真实的DOM。
更新组件的生命周期
引用
removeNode:https://github.com/developit/preact/blob/master/src/dom/index.js#L9
insertBefore:https://github.com/developit/preact/blob/master/src/vdom/diff.js#L253
2.2 用引用的真实DOM,避免创建重复的nodes
像之前提到的,在初始化加载期间,每个组件相对应我们创建的真实DOM树有一个引用。下面这张图片展现了这一刻我们的应用的引用。
显示每一个组件 和 之前的DOM的差异
当虚拟DOM被创建,每个虚拟DOM的属性都会跟真实DOM的属性进行比较如果真实DOM存在,循环处理将会进行下一步
真实DOM已经存在(在更新期间)
引用
innerDiffNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L185
2.3 如果他们在真实的DOM中是额外的节点,移除他们
下面的图片展现了真实DOM和虚拟DOM的差异
(click to zoom)
这里有一点儿不同。在真实节点中的“New York”节点被算法移除了像下面流程图那样。当所有工作进行完毕算法也会调用“componentDidUpdate”。
移除DOM节点生命周期
情景 3 — 卸载整个组件
让我们看看在filter组件中输入blabla,既然没有匹配到“California” 和 “New York”, 我们不会渲染子组件“List”,这意味着我们需要卸载整个组件。
如果没有结果的话List组件没有被移除
组件FilteredList的render方法
删除一个组件跟删除一个单一节点差不多,当我们删除一个相对于组件有引用的节点,框架会调用“componentWillUnmount”,然后安全的删除所有的DOM元素。当所有的元素从真实DOM移除,将会调用引用的组件的“componentDidUnmount”方法。
下面的图片显示在真实的DOM“ul”中,“List”组件的引用。
下面流程图的高亮部分展现了移除和卸载组件的过程
移除和卸载组件