介绍
字符串是 JavaScript 中的重要数据类型,其重要性不仅体现在字符串是应用最多最广泛的数据类型,更体现在V8中使用了大量的技术手段来修饰和优化字符串的操作。接下来的几篇文章将集中讲解字符串的相关操作。本文先讲解 String.prototype.replace 的源码以及相关数据结构,再通过测试用例演示 String.prototype.replace 的调用、加载和执行过程。我们会看字符串的类型、长度等因素如何影响 replace 的实现方式和效率。
注意 (1)Sea of Nodes 是本文的先导知识,请参考 Cliff 1993年发表的论文 From Quads to Graphs。(2)本文所用环境为:V8 7.9、win10 x64、VS2019。
String.prototype.replace 源码
测试用例代码如下:
var str="Visit Microsoft!Visit Microsoft!";var res=str.replace("Microsoft","Runoob");console.log(res);
replace() 采用 TF_BUILTIN 实现,replace() 在 V8 中的函数名是 StringPrototypeReplace,编号是 594,源码如下:
1. TF_BUILTIN(StringPrototypeReplace, StringBuiltinsAssembler) {2. Label out(this);3. TNode<Object> receiver = CAST(Parameter(Descriptor::kReceiver));4. Node* const search = Parameter(Descriptor::kSearch);5. Node* const replace = Parameter(Descriptor::kReplace);6. TNode<Context> context = CAST(Parameter(Descriptor::kContext));7. TNode<Smi> const smi_zero = SmiConstant(0);8. RequireObjectCoercible(context, receiver, "String.prototype.replace");9. MaybeCallFunctionAtSymbol(/*省略.....*/);10. TNode<String> const subject_string = ToString_Inline(context, receiver);11. TNode<String> const search_string = ToString_Inline(context, search);12. TNode<IntPtrT> const subject_length = LoadStringLengthAsWord(subject_string);13. TNode<IntPtrT> const search_length = LoadStringLengthAsWord(search_string);14. {15. Label next(this);16. GotoIfNot(WordEqual(search_length, IntPtrConstant(1)), &next);17. GotoIfNot(IntPtrGreaterThan(subject_length, IntPtrConstant(0xFF)), &next);18. GotoIf(TaggedIsSmi(replace), &next);19. GotoIfNot(IsString(replace), &next);20. TNode<Uint16T> const subject_instance_type =21. LoadInstanceType(subject_string);22. GotoIfNot(IsConsStringInstanceType(subject_instance_type), &next);23. GotoIf(TaggedIsPositiveSmi(IndexOfDollarChar(context, replace)), &next);24. Return(CallRuntime(Runtime::kStringReplaceOneCharWithString, context,25. subject_string, search_string, replace));26. BIND(&next); }27. TNode<Smi> const match_start_index =28. CAST(CallBuiltin(Builtins::kStringIndexOf, context, subject_string,29. search_string, smi_zero));30. {31. Label next(this), return_subject(this);32. GotoIfNot(SmiIsNegative(match_start_index), &next);33. GotoIf(TaggedIsSmi(replace), &return_subject);34. GotoIf(IsCallableMap(LoadMap(replace)), &return_subject);35. ToString_Inline(context, replace);36. Goto(&return_subject);37. BIND(&return_subject);38. Return(subject_string);39. BIND(&next); }40. TNode<Smi> const match_end_index = SmiAdd(match_start_index, SmiFromIntPtr(search_length));41. VARIABLE(var_result, MachineRepresentation::kTagged, EmptyStringConstant());42. {43. Label next(this);44. GotoIf(SmiEqual(match_start_index, smi_zero), &next);45. TNode<Object> const prefix =46. CallBuiltin(Builtins::kStringSubstring, context, subject_string,47. IntPtrConstant(0), SmiUntag(match_start_index));48. var_result.Bind(prefix);49. Goto(&next);50. BIND(&next); }51. Label if_iscallablereplace(this), if_notcallablereplace(this);52. GotoIf(TaggedIsSmi(replace), &if_notcallablereplace);53. Branch(IsCallableMap(LoadMap(replace)), &if_iscallablereplace,54. &if_notcallablereplace);55. BIND(&if_iscallablereplace);56. {57. Callable call_callable = CodeFactory::Call(isolate());58. Node* const replacement =59. CallJS(call_callable, context, replace, UndefinedConstant(),60. search_string, match_start_index, subject_string);61. TNode<String> const replacement_string =62. ToString_Inline(context, replacement);63. var_result.Bind(CallBuiltin(Builtins::kStringAdd_CheckNone, context,64. var_result.value(), replacement_string));65. Goto(&out); }66. BIND(&if_notcallablereplace);67. {68. TNode<String> const replace_string = ToString_Inline(context, replace);69. Node* const replacement =70. GetSubstitution(context, subject_string, match_start_index,71. match_end_index, replace_string);72. var_result.Bind(CallBuiltin(Builtins::kStringAdd_CheckNone, context,73. var_result.value(), replacement));74. Goto(&out);}75. BIND(&out);76. {77. TNode<Object> const suffix =78. CallBuiltin(Builtins::kStringSubstring, context, subject_string,79. SmiUntag(match_end_index), subject_length);80. TNode<Object> const result = CallBuiltin(81. Builtins::kStringAdd_CheckNone, context, var_result.value(), suffix);82. Return(result);83. }}
上述代码第 3 行代码 receiver 代表测试用例字符串 “Visit Microsoft!Visit Microsoft!”;
第 4 行代码 search 代表测试用例字符串 “Microsoft”;
第 5 行代码 replace 代表测试用例字符串 “Runoob”;
第 9 行代码当 search 为正则表达式时,使用 MaybeCallFunctionAtSymbol() 实现 replace 功能。
下面将 replace 源码划分为五个子功能单独说明:
(1) 功能 1:receiver 长度大于 0xFF、search 长度大于 1 以及 replace 为 ConsString,这三个条件同时成立时
第 10-13 行代码转换 search 和 replace 的数据类型,并计算他们的长度;
第 16 行代码判断 search 的长度是否等于 1,不等于则跳转到第 26 行;
第 17 行代码判断 receiver 的长度是否大于 0xFF,小于等于则跳转到第 26 行;
第 18-19 行判断 replace 是否为小整数或者 replace 不是字符串,结果为真则跳转到第 26 行;
第 20-22 行判断 replace 是否为 ConsString 类型,结果为假则跳转到第 26 行;提示 ConsString不是一个独立的字符串,它是使用指针把两个字符串拼接在一起的字符串对。在 V8 中,两个字符串相加的结果常是 ConsString 类型的字符串对。
第 23 行判断 PositiveSmi 类型;
第 24 行采用 Runtime::kStringReplaceOneCharWithString() 完成 replace。
(2) 功能 2:计算 receiver 中的前缀字符串
第 27-32 行计算 search的第一个字符在 receiver 中的位置 match_start_index,如果 match_start_index 不是负数则跳转到 39 行;
第 33-35 行判断 replace 是否为 SMI 或 函数,是则跳转到 37 行;
第 40 行计算 match_end_index;
第 44 行如果 match_start_index = 0(也就是 search[0]=receiver[0]),则跳转到 49 行;
第 45 行取出 receiver 中 match_start_index 位置之前的字符,保存为 prefix;也就是获取测试用例中的 “Visit ” 字符串;
第 50-54 行判断 replace 是否为 SMI 或 函数,根据判断结果跳转到相应的行号。
(3) 功能 3:replace 是函数类型
第 58-63 行计算 replace 的结果,将该结果拼接在 prefix 后面组成新的字符串 var_result;
(4) 功能 4:replace 是字符串类型
第 58-72 行将 replace 拼接在 prefix 后面组成新的字符串 var_result;
(5) 计算 receiver 中的后缀字符串
第 77-82 行取出 receiver 中 match_end_index 之后的字符串 suffix,将 suffix 拼接在 var_result 后面组成并返回新的字符串,replace 完毕。
下面简单说明 replace 中使用的 runtime 方法:
1. RUNTIME_FUNCTION(Runtime_StringReplaceOneCharWithString) {2. HandleScope scope(isolate);3. DCHECK_EQ(3, args.length());4. CONVERT_ARG_HANDLE_CHECKED(String, subject, 0);5. CONVERT_ARG_HANDLE_CHECKED(String, search, 1);6. CONVERT_ARG_HANDLE_CHECKED(String, replace, 2);7. const int kRecursionLimit = 0x1000;8. bool found = false;9. Handle<String> result;10. if (StringReplaceOneCharWithString(isolate, subject, search, replace, &found,11. kRecursionLimit).ToHandle(&result)) {12. return *result;13. }14. if (isolate->has_pending_exception())15. return ReadOnlyRoots(isolate).exception();16. subject = String::Flatten(isolate, subject);17. if (StringReplaceOneCharWithString(isolate, subject, search, replace, &found,18. kRecursionLimit).ToHandle(&result)) {19. return *result;20. }21. if (isolate->has_pending_exception())22. return ReadOnlyRoots(isolate).exception();23. return isolate->StackOverflow();24. }
上述代码中第 10 执行 StringReplaceOneCharWithString 方法,该方法实现了 replace 功能;
第 14 行代码检测到异常情况时,先执行 String::Flatten,将ConsString字符处理成为单一字符串之后再次执行 StringReplaceOneCharWithString 方法。
图1给出 StringPrototypeReplace 的函数调用堆栈。
String.prototype.replace 测试
测试用例的字节码如下:
1. //省略........2. 000001A2EE982AC6 @ 16 : 12 01 LdaConstant [1]3. 000001A2EE982AC8 @ 18 : 15 02 04 StaGlobal [2], [4]4. 000001A2EE982ACB @ 21 : 13 02 00 LdaGlobal [2], [0]5. 000001A2EE982ACE @ 24 : 26 f9 Star r26. 000001A2EE982AD0 @ 26 : 29 f9 03 LdaNamedPropertyNoFeedback r2, [3]7. 000001A2EE982AD3 @ 29 : 26 fa Star r18. 000001A2EE982AD5 @ 31 : 12 04 LdaConstant [4]9. 000001A2EE982AD7 @ 33 : 26 f8 Star r310. 000001A2EE982AD9 @ 35 : 12 05 LdaConstant [5]11. 000001A2EE982ADB @ 37 : 26 f7 Star r412. 000001A2EE982ADD @ 39 : 5f fa f9 03 CallNoFeedback r1, r2-r413. 000001A2EE982AE1 @ 43 : 15 06 06 StaGlobal [6], [6]14. 000001A2EE982AE4 @ 46 : 13 07 08 LdaGlobal [7], [8]15. 000001A2EE982AE7 @ 49 : 26 f9 Star r216. 000001A2EE982AE9 @ 51 : 29 f9 08 LdaNamedPropertyNoFeedback r2, [8]17. 000001A2EE982AEC @ 54 : 26 fa Star r118. 000001A2EE982AEE @ 56 : 13 06 02 LdaGlobal [6], [2]19. 000001A2EE982AF1 @ 59 : 26 f8 Star r320. 000001A2EE982AF3 @ 61 : 5f fa f9 02 CallNoFeedback r1, r2-r321. 000001A2EE982AF7 @ 65 : 26 fb Star r022. 000001A2EE982AF9 @ 67 : ab Return23. Constant pool (size = 9)24. 000001A2EE982A29: [FixedArray] in OldSpace25. - map: 0x022d5e100169 <Map>26. - length: 927. 0: 0x01a2ee9829c9 <FixedArray[8]>28. 1: 0x01a2ee9828c1 <String[#32]: Visit Microsoft!Visit Microsoft!>29. 2: 0x01a2ee9828a9 <String[#3]: str>30. 3: 0x02749a2ab821 <String[#7]: replace>31. 4: 0x01a2ee982909 <String[#9]: Microsoft>32. 5: 0x01a2ee982929 <String[#6]: Runoob>33. 6: 0x01a2ee9828f1 <String[#3]: res>34. 7: 0x02749a2b3699 <String[#7]: console>35. 8: 0x02749a2b2cd9 <String[#3]: log>
上述代码中,第 2-5 行读取字符串 “Visit Microsoft!Visit Microsoft!” 并保存到 r2 寄存器中;
第 6-7 行获取 replace 函数地址并保存到 r1 寄存器中;
第 8-11 行读取 “Microsoft” 和 “Runoob” 并分别保存到 r3 和 r4 寄存器中;
第 12 行调用 repalce 方法;
图 2 给出了 CallNoFeedback 的调用堆栈。
技术总结
(1) receiver 长度大于 0xFF、search 长度大于 1 以及 replace为ConsString,三个条件同时成立时采用低效率的 runtime 方式处理;
(2) replace 的原理是计算前、后缀,并与 newvalue 拼接组成最终结果。
好了,今天到这里,下次见。
个人能力有限,有不足与纰漏,欢迎批评指正