firefox中input隐藏之后用js获取选择起始和结束位置引起错误(源码分析)
var textEl = document.getElementById("testText"); textEl.style.display = "none"; try{ var a = textEl.selectionStart; }catch(e){ alert(e); }
textEl是一个很简单的html的input输入框。但是在设置隐藏之后获取选中的起始和结束位置就会报异常。
异常如下:
"[Exception... "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMHTMLInputElement.selectionStart]" nsresult: "0x80004005 (NS_ERROR_FAILURE)" location: "JS frame :: file:///C:/1.html :: <TOP_LEVEL> :: line 15" data: no]"
很明显这是从firefox内核中报出来的异常。
我们来看下获取input选中起始位置的源码,在content\html\content\src\nsHTMLInputElement.cpp文件中:
nsresult nsHTMLInputElement::GetSelectionRange(PRInt32* aSelectionStart, PRInt32* aSelectionEnd) { nsresult rv = NS_ERROR_FAILURE; nsIFormControlFrame* formControlFrame = GetFormControlFrame(PR_TRUE); if (formControlFrame) { nsITextControlFrame* textControlFrame = do_QueryFrame(formControlFrame); if (textControlFrame) rv = textControlFrame->GetSelectionRange(aSelectionStart, aSelectionEnd); } return rv; }
重要的是nsIFormControlFrame* formControlFrame = GetFormControlFrame(PR_TRUE);这句代码,
在input隐藏的时候,它返回了null。为什么呢?因为传进去的参数是PR_TRUE!!!
ok,我们跟进去看为什么会返回null,经过中间几个小方法的调用,我们看content\html\content\src\nsGenericHTMLElement.cpp中的方法:
// static nsIFormControlFrame* nsGenericHTMLElement::GetFormControlFrameFor(nsIContent* aContent, nsIDocument* aDocument, PRBool aFlushContent) { if (aFlushContent) { // Cause a flush of the frames, so we get up-to-date frame information aDocument->FlushPendingNotifications(Flush_Frames); } nsIFrame* frame = GetPrimaryFrameFor(aContent, aDocument); if (frame) { nsIFormControlFrame* form_frame = do_QueryFrame(frame); if (form_frame) { return form_frame; } // If we have generated content, the primary frame will be a // wrapper frame.. out real frame will be in its child list. for (frame = frame->GetFirstChild(nsnull); frame; frame = frame->GetNextSibling()) { form_frame = do_QueryFrame(frame); if (form_frame) { return form_frame; } } } return nsnull; }
这个方法里面用到了我们的PR_TRUE参数,是会调用aDocument->FlushPendingNotifications(Flush_Frames);
我们继续跟进,我们会来到layout\base\nsPresShell.cpp中的FlushPendingNotifications方法,在这个方法中,firefox会处理本shell(本iframe)中以前挂起的一些操作(比如说我们设置display为none),其中有一段代码就是处理挂起的样式操作,如下所示:
// Process pending restyles, since any flush of the presshell wants // up-to-date style data. if (!mIsDestroying) { mPresContext->FlushPendingMediaFeatureValuesChanged(); // Flush any pending update of the user font set, since that could // cause style changes (for updating ex/ch units, and to cause a // reflow). mPresContext->FlushUserFontSet(); nsAutoScriptBlocker scriptBlocker; mFrameConstructor->ProcessPendingRestyles(); }
在处理pending restyles的时候会进入到mFrameConstructor->ProcessPendingRestyles()中,它会处理一个
mPendingRestyles列表中被添加的所有的pending restyles。 我们设置的display为none会产生一个hint为
nsChangeHint_ReconstructFrame的pending restyles。
firefox会根据这个hint重现构建这个frame(不可见的容器会从nsFrameManager中移除),就是调用的
RecreateFramesForContent这个方法,这个方法里调用ContentRemoved方法,ContentRemoved方法
::DeletingFrameSubtree(frameManager, childFrame)来从nsFrameManager中删除掉这个不可见的元素,所以
在FlushPendingNotifications之后我们就无法取到这个formControlFrame了,所以firefox内核返回了
NS_ERROR_FAILURE,导致了js抛出异常。
附1: 设置display为none是怎么加到pending restyles中的
看下调用堆栈
void
nsCSSFrameConstructor::PostRestyleEvent(nsIContent* aContent,
nsReStyleHint aRestyleHint,
nsChangeHint aMinChangeHint)
{
if (NS_UNLIKELY(mPresShell->IsDestroying())) {
return;
}
if (aRestyleHint == 0 && !aMinChangeHint) {
// Nothing to do here
return;
}
NS_ASSERTION(aContent->IsNodeOfType(nsINode::eELEMENT),
"Shouldn't be trying to restyle non-elements directly");
RestyleData existingData;
existingData.mRestyleHint = nsReStyleHint(0);
existingData.mChangeHint = NS_STYLE_HINT_NONE;
mPendingRestyles.Get(aContent, &existingData);
existingData.mRestyleHint =
nsReStyleHint(existingData.mRestyleHint | aRestyleHint);
NS_UpdateHint(existingData.mChangeHint, aMinChangeHint);
mPendingRestyles.Put(aContent, existingData);
PostRestyleEventInternal();
}
ok,就是在这个方法里面加入进去的。
附2:为什么得到input的value是没问题的
input设置隐藏之后,即使从nsFrameManager移除掉对应的frame,也可以得到正确的value的值,这是为什么,
我们看下content\html\content\src\nsHTMLInputElement.cpp中对应的方法:
NS_IMETHODIMP nsHTMLInputElement::GetValue(nsAString& aValue) { if (mType == NS_FORM_INPUT_TEXT || mType == NS_FORM_INPUT_PASSWORD) { // No need to flush here, if there's no frame created for this // input yet, there won't be a value in it (that we don't already // have) even if we force it to be created nsIFormControlFrame* formControlFrame = GetFormControlFrame(PR_FALSE); PRBool frameOwnsValue = PR_FALSE; if (formControlFrame) { nsITextControlFrame* textControlFrame = do_QueryFrame(formControlFrame); if (textControlFrame) { textControlFrame->OwnsValue(&frameOwnsValue); } else { // We assume if it's not a text control frame that it owns the value frameOwnsValue = PR_TRUE; } } if (frameOwnsValue) { formControlFrame->GetFormProperty(nsGkAtoms::value, aValue); } else { if (!GET_BOOLBIT(mBitField, BF_VALUE_CHANGED) || !mValue) { GetDefaultValue(aValue); } else { CopyUTF8toUTF16(mValue, aValue); } } return NS_OK; } if (mType == NS_FORM_INPUT_FILE) { if (nsContentUtils::IsCallerTrustedForCapability("UniversalFileRead")) { if (!mFileNames.IsEmpty()) { aValue = mFileNames[0]; } else { aValue.Truncate(); } } else { // Just return the leaf name nsCOMArray<nsIFile> files; GetFileArray(files); if (files.Count() == 0 || NS_FAILED(files[0]->GetLeafName(aValue))) { aValue.Truncate(); } } return NS_OK; } // Treat value == defaultValue for other input elements if (!GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue) && (mType == NS_FORM_INPUT_RADIO || mType == NS_FORM_INPUT_CHECKBOX)) { // The default value of a radio or checkbox input is "on". aValue.AssignLiteral("on"); } if (mType != NS_FORM_INPUT_HIDDEN) { aValue = nsContentUtils::TrimCharsInSet(kWhitespace, aValue); } return NS_OK; }
GetValue方法也会首先去获取frame,但是它传进去的值是PR_FALSE,也就是说如果在设置隐藏后不首先调用
textEl.selectionStart之类的方法的话,此处得到的是正确的frame,那也就是可以得到正确的值。但是如果我们首先调用了textEl.selectionStart的话,此input对应的frame已经被移除,所以得到的frame将是空的,会得到错误的值么?不会!
其实nsHTMLInputElement使用了一种模型+控件的模式,你在设置值的时候,如果可以得到对应的frame(控件),则值被设置到控件上,如果得不到frame(input设置了隐藏等),则firefox会首先把值保存在模型上,而firefox会保持一些监听器,等控件重现展现的时候把模型的值同步到控件上。而你取值的时候也会保持同样的规则。
附3: 哪些方法在input隐藏后调用会报错
在nsHTMLInputElement使用到GetFormControlFrame(PR_TRUE)的有下面6个方法:
void
nsHTMLInputElement::SelectAll
NS_IMETHODIMP
nsHTMLInputElement::SetSelectionRange
NS_IMETHODIMP
nsHTMLInputElement::SetSelectionStart
NS_IMETHODIMP
nsHTMLInputElement::SetSelectionEnd
nsresult
nsHTMLInputElement::GetSelectionRange
NS_IMETHODIMP
nsHTMLInputElement::GetPhonetic
但是由于SelectAll没有返回值,GetPhonetic即使frame为null也会返回NS_OK。所以在input框隐藏的时候,用js来设置和获取选中的起始和结束位置都会在js里抛出异常,而其他方法都是安全的。
估计Mozilla认为在文本框隐藏的时候,选择位置都是没有意义的,不应该在文本框隐藏的时候去涉及选择位置。