Просмотр исходного кода

perf: optimize Input, TextArea getValueLength judgment times (#2432)

Co-authored-by: shijia.me <[email protected]>
Co-authored-by: pointhalo <[email protected]>
Shi Jia 1 год назад
Родитель
Сommit
a4a95a4935

+ 3 - 14
packages/semi-foundation/input/foundation.ts

@@ -3,6 +3,7 @@ import { strings } from './constants';
 import { noop, set, isNumber, isString, isFunction } from 'lodash';
 
 import { ENTER_KEY } from './../utils/keyCode';
+import truncateValue from './util/truncateValue';
 
 export interface InputDefaultAdapter {
     notifyChange: noopFunction;
@@ -112,6 +113,7 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
                 return value;
             }
         }
+        return value;
     }
 
     /**
@@ -122,20 +124,7 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
      */
     handleTruncateValue(value: any, maxLength: number) {
         const { getValueLength } = this._adapter.getProps();
-        if (isFunction(getValueLength)) {
-            let truncatedValue = '';
-            for (let i = 1, len = value.length; i <= len; i++) {
-                const currentValue = value.slice(0, i);
-                if (getValueLength(currentValue) > maxLength) {
-                    return truncatedValue;
-                } else {
-                    truncatedValue = currentValue;
-                }
-            }
-            return truncatedValue;
-        } else {
-            return value.slice(0, maxLength);
-        }
+        return truncateValue({ value, maxLength, getValueLength });
     }
 
     handleClear(e: any) {

+ 2 - 14
packages/semi-foundation/input/textareaFoundation.ts

@@ -7,6 +7,7 @@ import {
 } from 'lodash';
 import calculateNodeHeight from './util/calculateNodeHeight';
 import getSizingData from './util/getSizingData';
+import truncateValue from './util/truncateValue';
 
 export interface TextAreaDefaultAdapter {
     notifyChange: noopFunction;
@@ -124,20 +125,7 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdapter>
      */
     handleTruncateValue(value: string, maxLength: number) {
         const { getValueLength } = this._adapter.getProps();
-        if (isFunction(getValueLength)) {
-            let truncatedValue = '';
-            for (let i = 1, len = value.length; i <= len; i++) {
-                const currentValue = value.slice(0, i);
-                if (getValueLength(currentValue) > maxLength) {
-                    return truncatedValue;
-                } else {
-                    truncatedValue = currentValue;
-                }
-            }
-            return truncatedValue;
-        } else {
-            return value.slice(0, maxLength);
-        }
+        return truncateValue({ value, maxLength, getValueLength });
     }
 
     handleFocus(e: any) {

+ 25 - 0
packages/semi-foundation/input/util/truncateValue.ts

@@ -0,0 +1,25 @@
+import { isFunction } from 'lodash';
+
+export default function truncateValue(options: {
+    value: string;
+    maxLength: number;
+    getValueLength?: (value: string) => number
+}): string {
+    const { value, maxLength, getValueLength } = options;
+    if (isFunction(getValueLength)) {
+        let left = 0;
+        let right = value.length;
+        while (left < right) {
+            const mid = left + Math.floor((right - left) / 2);
+            const currentValue = value.slice(0, mid + 1);
+            if (getValueLength(currentValue) > maxLength) {
+                right = mid;
+            } else {
+                left = mid + 1;
+            }
+        }
+        return value.slice(0, left);
+    } else {
+        return value.slice(0, maxLength);
+    }
+}

+ 78 - 23
packages/semi-ui/input/__test__/textArea.test.js

@@ -1,22 +1,22 @@
 import TextArea from '../textarea';
 import Icon from '../../icons/index';
 import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
+import truncateValue from '../../../semi-foundation/input/util/truncateValue';
 import GraphemeSplitter from 'grapheme-splitter';
 import { isString } from 'lodash';
 
 function getValueLength(str) {
-  if (isString(str)) {
-    const splitter = new GraphemeSplitter();
-    return splitter.countGraphemes(str);
-  } else {
-    return -1;
-  }
+    if (isString(str)) {
+        const splitter = new GraphemeSplitter();
+        return splitter.countGraphemes(str);
+    } else {
+        return -1;
+    }
 }
 
 describe('TextArea', () => {
-
     it('TextArea with custom className & style', () => {
-        const wrapper = mount(<TextArea className='test' style={{ color: 'red' }} />);
+        const wrapper = mount(<TextArea className="test" style={{ color: 'red' }} />);
         expect(wrapper.hasClass('test')).toEqual(true);
         expect(wrapper.find('div.test')).toHaveStyle('color', 'red');
     });
@@ -39,7 +39,7 @@ describe('TextArea', () => {
         textArea.find('textarea').simulate('change', event);
         expect(spyOnChange.calledOnce).toBe(true);
         expect(spyOnChange.calledWithMatch(textAreaValue)).toBe(true);
-    })
+    });
 
     it('TextArea show maxCount', () => {
         const textarea = mount(<TextArea maxCount={10} />);
@@ -54,21 +54,24 @@ describe('TextArea', () => {
         const textarea = mount(<TextArea placeholder={placeholderText} />);
         let textareaDom = textarea.find('textarea');
         expect(textareaDom.props().placeholder).toEqual(placeholderText);
-    })
+    });
 
     it('TextArea disabled', () => {
         const textarea = mount(<TextArea disabled />);
         let textareaDom = textarea.find(`textarea.${BASE_CLASS_PREFIX}-input-textarea-disabled`);
         expect(textareaDom.props().disabled).toEqual(true);
-    })
+    });
 
     it('TextArea showClear / onClear', () => {
-        const spyOnClear = sinon.spy(()=>{});
-        const textarea = mount(<TextArea showClear defaultValue='123' onClear={spyOnClear}/>);
-        textarea.simulate('mouseEnter', {}).find(`.${BASE_CLASS_PREFIX}-input-clearbtn`).simulate('click');
+        const spyOnClear = sinon.spy(() => {});
+        const textarea = mount(<TextArea showClear defaultValue="123" onClear={spyOnClear} />);
+        textarea
+            .simulate('mouseEnter', {})
+            .find(`.${BASE_CLASS_PREFIX}-input-clearbtn`)
+            .simulate('click');
         expect(spyOnClear.calledOnce).toBe(true);
         expect(textarea.find(`.${BASE_CLASS_PREFIX}-input-textarea`).getDOMNode().textContent).toEqual('');
-    })
+    });
 
     // TODO
     // it('TextArea autosize', () => {
@@ -95,7 +98,7 @@ describe('TextArea', () => {
             console.log(e);
         };
         let spyOnChange = sinon.spy(onChange);
-        const textArea = mount(<TextArea onChange={spyOnChange} value='semi' />);
+        const textArea = mount(<TextArea onChange={spyOnChange} value="semi" />);
         const textareaDom = textArea.find('textarea');
         expect(textareaDom.instance().value).toEqual('semi');
         let newValue = 'vita lemon';
@@ -125,10 +128,12 @@ describe('TextArea', () => {
         let event1 = { target: { value: inputValue1 } };
 
         let onChange = value => {
-        console.log(value);
+            console.log(value);
         };
         let spyOnChange = sinon.spy(onChange);
-        const textArea = mount(<TextArea onChange={spyOnChange} minLength={minLength} getValueLength={getValueLength} />);
+        const textArea = mount(
+            <TextArea onChange={spyOnChange} minLength={minLength} getValueLength={getValueLength} />
+        );
         const textAreaDom = textArea.find('textarea');
 
         textAreaDom.simulate('change', event);
@@ -138,7 +143,7 @@ describe('TextArea', () => {
 
         textAreaDom.simulate('change', event1);
         expect(spyOnChange.calledWithMatch(textAreaDom)).toBe(true);
-        expect(textAreaDom.instance().minLength).toEqual(minLength)
+        expect(textAreaDom.instance().minLength).toEqual(minLength);
     });
 
     it('test maxLength + truncateValue', () => {
@@ -149,7 +154,9 @@ describe('TextArea', () => {
             };
 
             let spyOnChange = sinon.spy(onChange);
-            const textArea = mount(<TextArea onChange={spyOnChange} maxLength={maxLength} getValueLength={getValueLength} />);
+            const textArea = mount(
+                <TextArea onChange={spyOnChange} maxLength={maxLength} getValueLength={getValueLength} />
+            );
             const textAreaDom = textArea.find('textarea');
             textAreaDom.simulate('change', event);
             expect(spyOnChange.calledOnce).toBe(true);
@@ -157,7 +164,7 @@ describe('TextArea', () => {
         }
 
         const testCases = [
-        // 自定义valueLength
+            // 自定义valueLength
             ['Semi', 5, getValueLength, 'Semi'],
             ['Semi Design', 4, getValueLength, 'Semi'],
             ['💖💖💖💖💖💖💖💖💖💖👨👩👧👦', 10, getValueLength, '💖💖💖💖💖💖💖💖💖💖'],
@@ -168,5 +175,53 @@ describe('TextArea', () => {
         for (let [value, length, fc, result] of testCases) {
             expect(truncateValue(value, length, fc)).toBe(result);
         }
-  })
-})
+    });
+
+    it('test truncateValue', () => {
+        expect(truncateValue({ value: 'Semi Design', getValueLength, maxLength: 4 })).toBe('Semi');
+        expect(truncateValue({ value: 'Semi', getValueLength, maxLength: 4 })).toBe('Semi');
+        expect(truncateValue({ value: 'Se', getValueLength, maxLength: 1 })).toBe('S');
+        expect(truncateValue({ value: 'S', getValueLength, maxLength: 2 })).toBe('S');
+        expect(truncateValue({ value: '', getValueLength, maxLength: 2 })).toBe('');
+
+        expect(truncateValue({ value: '💖💖💖💖💖', getValueLength, maxLength: 4 })).toBe('💖💖💖💖');
+        expect(truncateValue({ value: '💖💖💖💖', getValueLength, maxLength: 4 })).toBe('💖💖💖💖');
+        expect(truncateValue({ value: '💖', getValueLength, maxLength: 1 })).toBe('💖');
+    });
+
+    it('test truncateValue function call time', () => {
+        function truncateValue(inputValue, maxLength) {
+            let event = { target: { value: inputValue } };
+
+            let spyTruncateValue = sinon.spy((str) => {
+                console.log('call getValueLength', str);
+                if (isString(str)) {
+                    const splitter = new GraphemeSplitter();
+                    return splitter.countGraphemes(str);
+                } else {
+                    return 0;
+                }
+            });
+            
+            const textArea = mount(
+                <TextArea maxLength={maxLength} getValueLength={spyTruncateValue} />
+            );
+            const textAreaDom = textArea.find('textarea');
+            textAreaDom.simulate('change', event);
+            // 超出判断一次,截断判断 LogN 次
+            const expectedValue = 1 + Math.ceil(Math.log2(inputValue.length));
+            console.log('expectedValue', expectedValue);
+            expect(spyTruncateValue.callCount).toBeLessThanOrEqual(expectedValue);
+            return textAreaDom.instance().value;
+        }
+
+        const testCases = [
+            ['Semi Design', 4],
+            [Array.from({ length: 1000 }).fill('👨‍👩‍👧‍👦').join(''), 500],
+        ];
+
+        for (let [value, length, expectedCalcTimes] of testCases) {
+            truncateValue(value, length, expectedCalcTimes);
+        }
+    });
+});