const title = <h1 className="title">Hello, world!</h1>;
这段代码并不是合法的js代码,它是一种被称为jsx的语法扩展
,通过它我们就可以很方便的在js代码中书写html片段。
本质上,jsx
是语法糖,上面这段代码会被babel
转换成如下代码
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
React
定义组件的方式可以分为两种:函数
和类
,函数定义可以看做是类定义的一种简单形式。
区分组件和原生DOM
的工作,是babel-plugin-transform-react-jsx
帮我们做的
例如在处理<Welcome name="Sara" />
时,createElement
方法的第一个参数tag
,实际上就是我们定义Welcome
的方法:
function Welcome( props ) {
return <h1>Hello, {props.name}</h1>;
}
组件基类React.Component
通过类的方式定义组件,我们需要继承React.Component
:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
React.Component
包含了一些预先定义好的变量和方法,我们来一步一步地实现它:
先定义一个Component
类:
class Component {}
state & props
通过继承React.Component
定义的组件有自己的私有状态state
,可以通过this.state
获取到。同时也能通过this.props
来获取传入的数据。
所以在构造函数中,我们需要初始化state和props
// React.Component
class Component {
constructor( props = {} ) {
this.state = {};
this.props = props;
}
}
setState
组件内部的state和渲染结果相关,当state改变时通常会触发渲染,为了让React
知道我们改变了state
,我们只能通过setState
方法去修改数据。我们可以通过Object.assign
来做一个简单的实现。
在每次更新state
后,我们需要调用renderComponent
方法来重新渲染组件
import { renderComponent } from '../react-dom/render'
class Component {
constructor( props = {} ) {
// ...
}
setState( stateChange ) {
// 将修改合并到state
Object.assign( this.state, stateChange );
renderComponent( this );
}
}
需要修改ReactDOM.render
方法,让其支持渲染组件
。
修改之前我们先来回顾一下对ReactDOM.render
的实现:
function render( vnode, container ) {
return container.appendChild( _render( vnode ) );
}
function _render( vnode ) {
if ( vnode === undefined || vnode === null || typeof vnode === 'boolean' ) vnode = '';
if ( typeof vnode === 'number' ) vnode = String( vnode );
if ( typeof vnode === 'string' ) {
let textNode = document.createTextNode( vnode );
return textNode;
}
const dom = document.createElement( vnode.tag );
if ( vnode.attrs ) {
Object.keys( vnode.attrs ).forEach( key => {
const value = vnode.attrs[ key ];
setAttribute( dom, key, value );
} );
}
vnode.children.forEach( child => render( child, dom ) ); // 递归渲染子节点
return dom;
}
我们需要在其中加一段用来渲染组件的代码:
function _render( vnode ) {
// ...
if ( typeof vnode.tag === 'function' ) {
const component = createComponent( vnode.tag, vnode.attrs );
setComponentProps( component, vnode.attrs );
return component.base;
}
// ...
}
组件渲染和生命周期
在上面的方法中用到了createComponent
和setComponentProps
两个方法,组件的生命周期方法也会在这里面实现。
生命周期方法是一些在特殊时机执行的函数,例如componentDidMount
方法会在组件挂载后执行
createComponent
方法用来创建组件实例,并且将函数定义组件扩展为类定义组件进行处理,以免其他地方需要区分不同定义方式。
// 创建组件
function createComponent( component, props ) {
let inst;
// 如果是类定义组件,则直接返回实例
if ( component.prototype && component.prototype.render ) {
inst = new component( props );
// 如果是函数定义组件,则将其扩展为类定义组件
} else {
inst = new Component( props );
inst.constructor = component;
inst.render = function() {
return this.constructor( props );
}
}
return inst;
}
setComponentProps
方法用来更新props
,在其中可以实现componentWillMount
,componentWillReceiveProps
两个生命周期方法
// set props
function setComponentProps( component, props ) {
if ( !component.base ) {
if ( component.componentWillMount ) component.componentWillMount();
} else if ( component.componentWillReceiveProps ) {
component.componentWillReceiveProps( props );
}
component.props = props;
renderComponent( component );
}
renderComponent
方法用来渲染组件,setState
方法中会直接调用这个方法进行重新渲染,在这个方法里可以实现componentWillUpdate
,componentDidUpdate
,componentDidMount
几个生命周期方法。
export function renderComponent( component ) {
let base;
const renderer = component.render();
if ( component.base && component.componentWillUpdate ) {
component.componentWillUpdate();
}
base = _render( renderer );
if ( component.base ) {
if ( component.componentDidUpdate ) component.componentDidUpdate();
} else if ( component.componentDidMount ) {
component.componentDidMount();
}
if ( component.base && component.base.parentNode ) {
component.base.parentNode.replaceChild( base, component.base );
}
component.base = base;
base._component = component;
}
每次更新都重新渲染
整个应用或者整个组件,DOM
操作十分昂贵,这样性能损耗
非常大。
为了减少DOM
更新,我们需要找渲染前后真正变化
的部分,只更新这一部分DOM
。而对比变化,找出需要更新部分的算法称之为diff算法
。
对比当前真实的DOM
和虚拟DOM
,在对比过程中直接更新真实DOM
只对比同一层级
的变化
需要实现一个diff方法
,它的作用是对比真实DOM
和虚拟DOM
,最后返回更新后的DOM
/**
* @param {HTMLElement} dom 真实DOM
* @param {vnode} vnode 虚拟DOM
* @returns {HTMLElement} 更新后的DOM
*/
function diff( dom, vnode ) {
// ...
}
虚拟DOM
的结构可以分为三种,分别表示文本
、原生DOM节点
以及组件
。
最简单的文本节点,如果当前的DOM
就是文本节点,则直接更新内容,否则就新建
一个文本节点
,并移除
掉原来的DOM
。
// diff text node
if ( typeof vnode === 'string' ) {
// 如果当前的DOM就是文本节点,则直接更新内容
if ( dom && dom.nodeType === 3 ) { // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
if ( dom.textContent !== vnode ) {
dom.textContent = vnode;
}
// 如果DOM不是文本节点,则新建一个文本节点DOM,并移除掉原来的
} else {
out = document.createTextNode( vnode );
if ( dom && dom.parentNode ) {
dom.parentNode.replaceChild( out, dom );
}
}
return out;
}
文本节点十分简单,它没有属性,也没有子元素,所以这一步结束后就可以直接返回结果了。
如果vnode
表示的是一个非文本的DOM节点
,那就要分两种情况了:
情况一:如果真实DOM不存在
,表示此节点是新增
的,或者新旧
两个节点的类型不一样
,那么就新建
一个DOM
元素,并将原来的子节点
(如果有的话)移动到新建的DOM节点下
。
if ( !dom || dom.nodeName.toLowerCase() !== vnode.tag.toLowerCase() ) {
out = document.createElement( vnode.tag );
if ( dom ) {
[ ...dom.childNodes ].map( out.appendChild ); // 将原来的子节点移到新节点下
if ( dom.parentNode ) {
dom.parentNode.replaceChild( out, dom ); // 移除掉原来的DOM对象
}
}
}
情况二:如果真实DOM
存在,并且和虚拟DOM
是同一类型的,那我们暂时不需要做别的,只需要等待后面对比属性和对比子节点。
实际上diff算法
不仅仅是找出节点类型
的变化,它还要找出来节点的属性以及事件监听
的变化。我们将对比属性
单独拿出来作为一个方法:
function diffAttributes( dom, vnode ) {
const old = {}; // 当前DOM的属性
const attrs = vnode.attrs; // 虚拟DOM的属性
for ( let i = 0 ; i < dom.attributes.length; i++ ) {
const attr = dom.attributes[ i ];
old[ attr.name ] = attr.value;
}
// 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined)
for ( let name in old ) {
if ( !( name in attrs ) ) {
setAttribute( dom, name, undefined );
}
}
// 更新新的属性值
for ( let name in attrs ) {
if ( old[ name ] !== attrs[ name ] ) {
setAttribute( dom, name, attrs[ name ] );
}
}
}
节点本身对比完成了,接下来就是对比它的子节点
。
这里会面临一个问题,前面我们实现的不同diff方法
,都是明确知道哪一个真实DOM
和虚拟DOM
对比,但是子节点
是一个数组
,它们可能改变了顺序
,或者数量
有所变化,我们很难确定要和虚拟DOM
对比的是哪一个。
为了简化逻辑,我们可以让用户提供一些线索:给节点设一个key
值,重新渲染时对比key
值相同的节点。
// diff方法
if ( vnode.children && vnode.children.length > 0 || ( out.childNodes && out.childNodes.length > 0 ) ) {
diffChildren( out, vnode.children );
}
function diffChildren( dom, vchildren ) {
const domChildren = dom.childNodes;
const children = [];
const keyed = {};
// 将有key的节点和没有key的节点分开
if ( domChildren.length > 0 ) {
for ( let i = 0; i < domChildren.length; i++ ) {
const child = domChildren[ i ];
const key = child.key;
if ( key ) {
keyed[ key ] = child;
} else {
children.push( child );
}
}
}
if ( vchildren && vchildren.length > 0 ) {
let min = 0;
let childrenLen = children.length;
for ( let i = 0; i < vchildren.length; i++ ) {
const vchild = vchildren[ i ];
const key = vchild.key;
let child;
// 如果有key,找到对应key值的节点
if ( key ) {
if ( keyed[ key ] ) {
child = keyed[ key ];
keyed[ key ] = undefined;
}
// 如果没有key,则优先找类型相同的节点
} else if ( min < childrenLen ) {
for ( let j = min; j < childrenLen; j++ ) {
let c = children[ j ];
if ( c && isSameNodeType( c, vchild ) ) {
child = c;
children[ j ] = undefined;
if ( j === childrenLen - 1 ) childrenLen--;
if ( j === min ) min++;
break;
}
}
}
// 对比
child = diff( child, vchild );
// 更新DOM
const f = domChildren[ i ];
if ( child && child !== dom && child !== f ) {
// 如果更新前的对应位置为空,说明此节点是新增的
if ( !f ) {
dom.appendChild(child);
// 如果更新后的节点和更新前对应位置的下一个节点一样,说明当前位置的节点被移除了
} else if ( child === f.nextSibling ) {
removeNode( f );
// 将更新后的节点移动到正确的位置
} else {
// 注意insertBefore的用法,第一个参数是要插入的节点,第二个参数是已存在的节点
dom.insertBefore( child, f );
}
}
}
}
}
对比组件
如果vnode
是一个组件,我们也单独拿出来作为一个方法:
function diffComponent( dom, vnode ) {
let c = dom && dom._component;
let oldDom = dom;
// 如果组件类型没有变化,则重新set props
if ( c && c.constructor === vnode.tag ) {
setComponentProps( c, vnode.attrs );
dom = c.base;
// 如果组件类型变化,则移除掉原来组件,并渲染新的组件
} else {
if ( c ) {
unmountComponent( c );
oldDom = null;
}
c = createComponent( vnode.tag, vnode.attrs );
setComponentProps( c, vnode.attrs );
dom = c.base;
if ( oldDom && dom !== oldDom ) {
oldDom._component = null;
removeNode( oldDom );
}
}
return dom;
}
function renderComponent( component ) {
// ...
// base = base = _render( renderer ); // 将_render改成diff
base = diff( component.base, renderer );
// ...
// 去掉这部分
// if ( component.base && component.base.parentNode ) {
// component.base.parentNode.replaceChild( base, component.base );
// }
// ...
}
diff
算法,通过它做到了每次只更新需要更新的部分
,极大地减少了DOM
操作。Reac
t实现远比这个要复杂,特别是在React 16
之后还引入了Fiber
架构,但是主要的思想是一致的。
实现diff算法
可以说性能有了很大的提升,但是在别的地方仍然后很多改进的空间:每次调用setState
后会立即调用renderComponent
重新渲染组件,但现实情况是,我们可能会在极短的时间内多次调用setState
。
onClick() {
for ( let i = 0; i < 100; i++ ) {
this.setState( { num: this.state.num + 1 } );
}
}
那以目前的实现,每次点击都会渲染100次组件,对性能肯定有很大的影响。
就要来改进setState
方法
优化setState:
异步更新state
,将短时间内的多个setState
合并成一个
class App extends Component {
constructor() {
super();
this.state = {
num: 0
}
}
componentDidMount() {
for ( let i = 0; i < 100; i++ ) {
this.setState( { num: this.state.num + 1 } );
console.log( this.state.num ); // 会输出100个0
//是因为React会将多个setState的调用合并成一个来执行,当调用setState时,state并不会立即更新导致的。
}
}
render() {
return (
<div className="App">
<h1>{ this.state.num }</h1>//渲染结果为1
</div>
);
}
}
react给出了一种解决方案:setState
接收的参数还可以是一个函数
,在这个函数中可以拿先前的状态
,并通过这个函数的返回值得到下一个状态
。
componentDidMount() {
for ( let i = 0; i < 100; i++ ) {
this.setState( prevState => {
console.log( prevState.num );// 此时打印0到100,渲染100
return {
num: prevState.num + 1
}
} );
}
}
合并setState
:
setState( stateChange ) {
Object.assign( this.state, stateChange );
renderComponent( this );//每次调用setState都会更新state并马上渲染一次。
}
setState
队列:
为了合并setState
,我们需要一个队列
来保存每次setState
的数据,然后在一段时间
后,清空
这个队列并渲染
组件。
队列是一种数据结构,它的特点是“先进先出
”,可以通过js数组
的push
和shift
方法模拟
const queue = [];
function enqueueSetState( stateChange, component ) {
queue.push( {
stateChange,
component
} );
}
然后修改组件的setState
方法,不再直接更新state
和渲染
组件,而是添加进更新队列
。
setState( stateChange ) {
enqueueSetState( stateChange, this );
}
清空队列:
function flush() {
let item;
// 遍历
while( item = setStateQueue.shift() ) {
const { stateChange, component } = item;
// 如果没有prevState,则将当前的state作为初始的prevState
if ( !component.prevState ) {
component.prevState = Object.assign( {}, component.state );
}
// 如果stateChange是一个方法,也就是setState的第二种形式
if ( typeof stateChange === 'function' ) {
Object.assign( component.state, stateChange( component.prevState, component.props ) );
} else {
// 如果stateChange是一个对象,则直接合并到setState中
Object.assign( component.state, stateChange );
}
component.prevState = component.state;
}
}
这只是实现了state
的更新,我们还没有渲染组件。渲染组件不能在遍历队列时进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列
保存所有组件,不同之处是,这个队列内不会有重复
的组件。
我们在enqueueSetState
时,就可以做这件事
const queue = [];
const renderQueue = [];
function enqueueSetState( stateChange, component ) {
queue.push( {
stateChange,
component
} );
// 如果renderQueue里没有当前组件,则添加到队列中
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
}
在flush
方法中,我们还需要遍历renderQueue
,来渲染
每一个组件
function flush() {
let item, component;
while( item = queue.shift() ) {
// ...
}
// 渲染每一个组件
while( component = renderQueue.shift() ) {
renderComponent( component );
}
}
延迟执行
现在还有一件最重要的事情:什么时候执行flush
方法。
我们需要合并一段时间内所有的setState
,也就是在一段时间后才执行flush
方法来清空队列,关键是这个“一段时间
“怎么决定。
一个比较好的做法是利用js的事件队列机制
。
setTimeout( () => {
console.log( 2 );
}, 0 );
Promise.resolve().then( () => console.log( 1 ) );
console.log( 3 );
//3 1 2
我们可以利用事件队列,让flush
在所有同步任务后执行
function enqueueSetState( stateChange, component ) {
// 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
if ( queue.length === 0 ) {
defer( flush );
}
queue.push( {
stateChange,
component
} );
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
}
定义defer
方法,利用Promise.resolve
function defer( fn ) {
return Promise.resolve().then( fn );
}
这样在一次“事件循环
“中,最多只会执行一次flush
了,在这个“事件循环
”中,所有的setState
都会被合并,并只渲染一次组件
。除了用Promise.resolve().then( fn )
,我们也可以用setTimeout( fn, 0 )
,setTimeout
的时间也可以是别的值,例如16毫秒
。
react基础:
React 使用 JSX
来替代常规的JavaScript
。
JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
它是类型安全的,在编译过程中就能发现错误。
使用 JSX 编写模板更加简单快速。
单向数据流state
state 和 props
主要的区别在于props
是不可变的,而state
可以根据与用户交互来改变。
react生命周期
componentWillMount
在渲染前调用,在客户端也在服务端。
componentDidMount
: 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()
来进行访问。 如果你想和其他JavaScript
框架一起使用,可以在这个方法中调用setTimeout, setInterval
或者发送AJAX请求
等操作(防止异步操作阻塞UI
)。
componentWillReceiveProps
在组件接收到一个新的props
(更新后)时被调用。这个方法在初始化render()
时不会被调用。
shouldComponentUpdate
返回一个布尔值。在组件接收到新的props
或者state
时被调用。在初始化
时或者使用forceUpdate
时不被调用。
可以在你确认不需要更新组件时使用。
componentWillUpdate
在组件接收到新的props或者state
但还没有render
时被调用。在初始化时不会被调用。
componentDidUpdate
在组件完成更新后立即调用。在初始化时不会被调用。
componentWillUnmount
在组件从 DOM 中移除之前立刻被调用。
不常用.png
class Content extends React.Component {
componentWillMount() {
console.log('Component WILL MOUNT!')
}
componentDidMount() {
console.log('Component DID MOUNT!')
}
componentWillReceiveProps(newProps) {
console.log('Component WILL RECEIVE PROPS!')
}
shouldComponentUpdate(newProps, newState) {
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log('Component WILL UPDATE!');
}
componentDidUpdate(prevProps, prevState) {
console.log('Component DID UPDATE!')
}
componentWillUnmount() {
console.log('Component WILL UNMOUNT!')
}
render() {
return (
<div>
<h3>{this.props.myNumber}</h3>
</div>
);
}
}
ReactDOM.render(
<div>
<Button />
</div>,
document.getElementById('example')
);
React 组件的数据可以通过componentDidMount
方法中的Ajax
来获取,当从服务端获取数据时可以将数据存储在state
中,再用 this.setState
方法重新渲染 UI
。
当使用异步加载数据时,在组件卸载前使用componentWillUnmount
来取消未完成的请求。
事件处理
HTML 通常写法是:
<button onclick="activateLasers()">
激活按钮
</button>
React 中写法为:
<button onClick={activateLasers}>
激活按钮
</button>
在 React 中另一个不同是你不能使用返回 false 的方式阻止默认行为, 你必须明确使用preventDefault
。
<a href="#" onclick="console.log('点击链接'); return false">点我</a>
在 React 的写法为:
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('链接被点击');
}
return (
<a href="#" onClick={handleClick}>
点我
</a>
);
}
条件渲染
function UserGreeting(props) {
return <h1>欢迎回来!</h1>;
}
function GuestGreeting(props) {
return <h1>请先注册。</h1>;
}
function Greeting(props) {//创建一个 Greeting 组件,它会根据用户是否登录来显示其中之一
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
// 尝试修改 isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('example')
);
与运算符 &&
三目运算符
条件渲染的另一种方法是使用 JavaScript 的条件运算符:
condition ? true : false。
列表 key
Keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。因此你应当给数组中的每一个元素赋予一个确定的标识。
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('example')
);
var ListItem = (props) => { //es6中箭头函数
return <li>{props.value}</li>;
}
function NumberList(props) {
var numbers; //声明在外面是因为 {} 中不能出现var,const,let等这种关键字
return (
<ul>
{
numbers = props.numbers, //注意这里要加逗号
numbers.map((number) =>
<ListItem key={number}
value={number} />
)}
</ul>
);
}
var arr = [1,2,3]; //要传递的参数
ReactDOM.render(
<NumberList numbers={arr}/>, //这里的numbers就是props下的numbers,即props.numbers
document.all('example')
);
组件API
设置状态:setState
替换状态:replaceState
设置属性:setProps
替换属性:replaceProps
强制更新:forceUpdate
获取DOM节点:findDOMNode
判断组件挂载状态:isMounted
React 支持一种非常特殊的属性Ref
,你可以用来绑定到render()
输出的任何组件上。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); }
render() {
return <div ref={this.myRef} />; }
}
网友评论