虚拟DOM(virtual DOM)是React的一大亮点。正是因为虚拟DOm,在大多数的应用场景中,我们都只要关注设置组件的状态(setState),不需要直接操作DOM。
那么虚拟DOM到底是什么呢?
其实,虚拟DOM(virtual DOM)是一个模拟DOM树的JavaScript对象。React使用虚拟DOM来渲染UI,当组件状态state有更新时,React会自动调用组件的render方法重新渲染整个组件的UI。
大多数时候,我们都应该呆在React的“虚拟浏览器”的世界里,因为它性能更加好,并且容易思考。但是,在某些情况下,为了实现某些需要,我们不得不去操作底层的DOM。比如:需要与一个没有使用React的第三方类库进行整合,或者执行一个React没有原生支持的操作(canvas)。
我们需要了解ReactDOM.render组件返回的是什么?
它会返回对组件的引用,也就是组件实例(对于无状态组件来说,返回null)。
注意:JSX返回的不是组件实例,它只是一个ReactElement对象。
在React内,它提供了一个可用于处理受其自身控制的DOM节点的方法,不过要注意的是,这些方法仅在组件声明周期的特定阶段才能被访问到。
1、refs
要访问受React控制的DOM节点,首先必须能够访问到负责控制这些DOM的组件,我们可以通过为子组件添加一个ref属性来实现。
var MyInput = React.createClass({
render: function(){
returen (
);
}
});
为子组件添加了ref属性后,我们就可以通过this.refs.myInput访问到组件了。
this返回的是当前组件。
注意:赋给每个子组件的ref值在所有子组件中必须是唯一的,也可以说是全局唯一。
到这里,我们已经可以访问到需要的子组件了,然后就可以通过它的getDOMNode()方法来访问到底层的DOM节点了。
但是请注意,我们不能在render方法中使用getDOMNode()方法,只有在render方法执行之后,并且react已经完成了DOM的更新,才能通过 this.refs.city.getDOMNode() 来拿到原生的DOM元素,也可以这样说,getDOMNode()仅在挂载的组件上有效(挂载:组件已经被放进DOM中),否则抛出异常。
一般我们会在 componentDidMount 事件回调中使用 getDOMNode 方法,当然,componentDidMount内部并不是getDOMNode方法的唯一执行环境。事件处理器也是在组件挂载后触发的,所以也可以在事件处理器中调用getDOMNode()方法。
var MyInput = React.createClass({
render: function(){
return ();
},
handleKeyUp: function(){
var input = this.refs.myInput;
console.log(input.value);
},
componentDidMount: function(){
console.log(this.refs.myInput);
}
});
ReactDOM.render(
,
document.body
);
上面的代码中,给input添加了一个keyup事件处理器,在handleKeyUp()方法里,我们也可以用this.refs.myInput访问到对应的input元素。
每一个挂载的React组件都有一个 getDOMNode() 方法。
如果不了解React的生命周期,可以看这里:React:组件的生命周期
特别注意:(getDOMNode()方法在v0.14版本中使用会报提醒,而在 v0.15版本中已经移除了)。因此,如果你使用的是 v0.15版本及以上的,this.refs.myInput获取到的已经是对应的DOM元素了,不过refs的表现和行为还是和之前的一致的。
2、ReactDOM.findDOMNode()
除了使用refs外,我们还可以使用react-dom提供的findDOMNode()方法拿到组件对应的DOM元素。
var MyInput = React.createClass({
render: function(){
return ();
},
handleKeyUp: function(){
var input = ReactDOM.findDOMNode(this);
//或者
input = ReactDOM.findDOMNode(this.refs.myInput);
console.log(input.value);
}
});
ReactDOM.render(
,
document.body
);
3、整合非React类库
要使用非React类库,保持它们的状态和React的状态之间的同步是成功整合的关键。
假如我们需要使用一个autocomplete类库:
(function(){
var autocomplete = function(params){
params = params || {};
if(!params.target) return;
var parent = params.target;
var data = params.data;
var list = '';
for(var i = 0; i < params.data.length; i++){
list += '
' + params.data[i] + '';}
parent.innerHTML = list;
if(params.events){
parent.addEventListener('click',function(e){
if(e.target.nodeName == 'LI'){
params.events.select(e.target.textContent);
}
});
}
};
window.autocomplete = autocomplete;
})();
调用示例代码:
autocomplte({
target: document.getElementById('cities'),
data: [
'广州','北京','深圳'
],
events: {
select: function(city){
alert('你选择的城市是' + city);
}
}
});
上面的autocomplete函数需要一个目标DOM节点、一个用作数据展现的字符串清单,以及一些事件监听器。
接下来,我们需要创建一个使用这两个库的React组件,然后需要添加一个componentDidMount处理器,在这个处理器内,可以将autocompleteTarget所指向子组件的底层DOM节点来连接这两个接口:
var AutocompleteCities = React.createClass({
render: function(){
return (
},
getDefaultProps: function(){
return {
data: ['广州','北京','深圳']
};
},
handleSelect: function(city){
alert('你选择的城市是' + city);
},
componentDidMount: function(){
autocomplete({
target: this.refs.autocompleteTarget, //也可以使用ReactDOM.findDOMNode(this.refs.autocompleteTarget)
data: this.props.data,
events: {
select: this.handleSelect
}
});
}
});
注意:componentDidMount方法只会为每个DOM节点调用一次。因此我们不用担心一个节点上两次调用autocomplete方法是否会有副作用。
对于简单的插件,最好是通过将其重写为React组件的形式来封装它。
总结
当仅使用虚拟DOM无法满足需求时,可以考虑ref属性,它允许访问指定的元素
在render方法执行之后,并且react已经完成了DOM的更新(一般是componentDidMount执行后)时,可以使用this.refs.name或者ReactDOM.findDOMNode()方法来访问底层的DOM节点。
可以将简单的第三方类库(非React类库)重写为React组件的形式来封装它。