ref是怎么引用到真实节点的
<input type="text" ref={(node)=>{this.myInput = node}}/>
这是我们在组件中书写的样式。我们知道,在HTML中这是个DOM节点,但是在React会经由jsx转化为React.createElement(...)
所以,这里其实相当于一个函数。
ReactElement.createElement = function(type, config, children) {
var propName;
// Reserved names are extracted
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
};
巴拉巴拉的细节逻辑就不看了,总之呢就是生成了一个ReactElement对象,而ref会保存为该对象的一个属性。
当这个reactElement开始挂载时会先将他包装成一个ReactComponent。(没错,惊喜不惊喜,以外不意外,你随便写的一div,input标签都是先被封装成组件再挂载。)实现代码如下,当然,这里精简了很多代码。
function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
console.log(node);
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
var type = element.type;
// Special case string values
if (typeof element.type === 'string') {
instance = ReactHostComponent.createInternalComponent(element);
} else {
instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
instance = ReactHostComponent.createInstanceForText(node);
}
return instance;
}
生成组件以后就会调用ReactReconciler.mountComponent
进行组件挂载。
mountComponent: function (internalInstance, transaction, hostParent, hostContainerInfo, context, parentDebugID) // 0 in production and for roots
{
//这里返回的markup就是渲染好的部分虚拟DOM树
var markup = internalInstance.mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
}
return markup;
},
从代码中可以看出,当ReactElement生成虚拟DOM节点以后,会进行判断,如果该节点有ref引用。则会把attachRefs函数和该节点传入回调函数序列。
那么这个回调序列又在哪儿触发的呢,原理还是transaction
机制。
transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
这段代码里的transaction是ReactReconcileTransaction
。
而ReactReconcileTransaction
包裹了三层wrapper
var SELECTION_RESTORATION = {
/**
* @return {Selection} Selection information.
* 返回选中的信息
*/
initialize: ReactInputSelection.getSelectionInformation,
/**
* @param {Selection} sel Selection information returned from `initialize`.
*/
close: ReactInputSelection.restoreSelection,
};
var EVENT_SUPPRESSION = {
initialize: function() {
//先确定当前是否可以进行事件处理。
var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
//防止此次事件处理过程中会发生其他transaction里面的事件
ReactBrowserEventEmitter.setEnabled(false);
return currentlyEnabled;
},
close: function(previouslyEnabled) {
ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
},
};
// 通过CallbackQueue回调函数队列机制,即this.reactMountReady
// 执行this.reactMountReady.enqueue(fn)注入componentDidMount、componentDidUpdate方法
// 通过Transaction添加前、后置钩子机制
// 前置钩子initialize方法用于清空回调队列;close用于触发回调函数componentDidMount、componentDidUpdate执行
var ON_DOM_READY_QUEUEING = {
initialize: function() {
this.reactMountReady.reset();
},
close: function() {
this.reactMountReady.notifyAll();
},
};
var TRANSACTION_WRAPPERS = [
SELECTION_RESTORATION,
EVENT_SUPPRESSION,
ON_DOM_READY_QUEUEING,
];
var Mixin = {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
}
}
其中的ON_DOM_READY_QUEUEING
正是子挂载结束后执行回调序列中的回调函数。
而ReactReconcileTransaction
事务则是在第一进入项目加载第一个节点时开启的。
大概流程如下
attachRefs
ref引用主要是ReactRef中的attachRef函数
//ref是我们标签中书写的ref
//component是书写ref的组件。上面提过,哪怕是input也会被封装成组件再挂载。
//owner是component所在的组件
function attachRef(ref, component, owner) {
if (typeof ref === 'function') {
ref(component.getPublicInstance());
} else {
// Legacy ref
ReactOwner.addComponentAsRefTo(component, ref, owner);
}
}
可以看到,正是在这边组件获取到了ref引用。
ref是function
当ref时function时,函数的参数传入的是component.getPublicInstance()
component上面说过,有可能是
- ReactEmptyComponent 空组件
- ReactHostComponent 对原生HTML标签的封装
- ReactCompositeComponent 用户自定义组件
这里不考虑空组件。主要是ReactHostComponent
和ReactCompositeComponent
首先看ReactCompositeComponent
。这个简单
getPublicInstance: function () {
var inst = this._instance;
return inst;
},
直接传入了实例化对象。通过这个方法,我们可以通过ref随时调用子组件的内部方法。
再看ReactHostComponent
getPublicInstance: function() {
return getNode(this);
},
function getNodeFromInstance(inst) {
if (inst._hostNode) {
return inst._hostNode;
}
var parents = [];
_prodInvariant('34') : void 0;
inst = inst._hostParent;
}
for (; parents.length; inst = parents.pop()) {
precacheChildNodes(inst, inst._hostNode);
}
return inst._hostNode;
}
这里是根据getNode获取的引用。getNode执行的又是ReactDOMComponentTree
中的getNodeFromInstance
方法。
从这个方法我们不能发现,这里ref获取到的参数就是实例的_hostNode
那么这个_hostNode
又是什么?参考源码,发现组件在挂载过程中
执行了这么句代码
el = ownerDocument.createElement(this._currentElement.type);
ReactDOMComponentTree.precacheNode(this, el);
//。。。。。我是分隔符
function getRenderedHostOrTextFromComponent(component) {
var rendered;
while (rendered = component._renderedComponent) {
component = rendered;
}
return component;
}
function precacheNode(inst, node) {
var hostInst = getRenderedHostOrTextFromComponent(inst);
hostInst._hostNode = node;
node[internalInstanceKey] = hostInst;
}
通过当前元素类型创造节点。接着将节点塞到了组件中,保存的正是_hostNode
。
ref不是函数
ref不是函数执行的是ReactOwner的attachRef
attachRef: function (ref, component) {
var inst = this.getPublicInstance();
var publicComponentInstance = component.getPublicInstance();
var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs;
refs[ref] = publicComponentInstance;
},
而这里传入的component正是有ref属性的组件(前面说过,哪怕写在input上,挂载时也是包装成组件再挂载),此时的ReactOwner(this)是component所在的组件。
可以看到此时将引用赋值给了this.refs的一个属性。属性名正是传入的ref。
很显然,ref可以如果不是function,也不一定是string。只要是合法的object key就可以了。当然,为了便于开发,还是推荐使用有意义的字符
ref拿到的到底是什么
很多文章里面说ref拿到的是真实DOM节点。其实这种说法很笼统。也很让人困惑,上面看到我们拿到的要么是实例(我们自定义组件)要么是component的_hostNode属性,这个好像不是真实DOM 啊
首先,什么叫真实DOM?
之前我以为嵌入DOM树的,在页面上显示的就是真实DOM节点。我们可以打印一下
这还只是部分属性。可以看到DOM其实就是拥有大量属性的一个对象。
只不过这个对象时塞入了DOM树。
而我们拿到的_hostNode是什么?也是我们通过document.createElement方法创建的element对象啊。只是这时候对象保存在了虚拟DOM中,然后再塞入真实DOM树。
所以说_hostNode和真实DOM树中的DOM的关系就是不同对象的不同属性指向的同一块存储空间,引用着同一个值而已。
我们虽然是通过虚拟DOM的_hostNode拿到这个值,但是对他的操作会体现在真实DOM节点上。说白了就是对象的引用赋值。
所以,ref拿到的是真实DOM的引用这个说法更准确。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。