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认为在文本框隐藏的时候,选择位置都是没有意义的,不应该在文本框隐藏的时候去涉及选择位置。

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值