由react效能優化擴充套件出來的bind與閉包的比較(效能)

sundjly發表於2019-03-15

github的地址 歡迎star

前言

最近由於在看一些react 優化的方面,注意到了之前沒有注意到的東西,都知道在react DOM事件的繫結方式有幾種方式:

  1. 在constructor中進行繫結。

  2. 或者用箭頭函式(無this)。

  3. 對於需要動態傳參的形式:

    • 可以使用閉包的方式

    • 或者可以直接把處理函式傳入子元件,子組建時可以拿到引數,再執行父元件的處理函式就可以了

    class App extends Component {
    removeCharacter = index => () => {
        const {list} = this.state;
        list.splice(index, 1);
        this.setState({
            list
        })
    }

    render() {
        return (
            <div>
               {
                    this.state.list.map((value, index) =>(
                        <div 
                            onClick={this.removeCharacter(index)}
                            key={value.id}
                            data={value}
                        >
                            點選我
                        </div>
                    ))
               }
            </div>
        )
    }
}

// 子元件處理的方式
class OtherApp extends Component {
    removeCharacter = index => {
        const {list} = this.state;
        list.splice(index, 1);
        this.setState({
            list
        })
    }
    render() {
        return (
            <div>
                {
                    this.state.list.map((value, index) =>(
                        <Child 
                            onClick={this.removeCharacter}
                            index={index}
                            key={value.id}
                            data={value}
                        />
                    ))
                }
            </div>
        )
    }
}

class Child extends Component {
    handleClick = () => {
        const { index, onClick} = this.props;
        onClick(index)
    }
    render() {
        return (
            <div onClick={this.handleClick}>
                {this.props.data}
            </div>
        )
    }
}
複製程式碼

重點介紹了需要傳參的方式,對於效能比較是要看具體的環境(和瀏覽器,平臺的優化都有關係),沒有什麼是絕對效能好的。

看到了這篇部落格的結論,就想具體實際比較一下:

如果每次都在 render 裡面的 jsx 去 bind 這個方法,會消耗效能,因為每次bind都會返回一個新函式,重複建立靜態函式肯定是不合適的(閉包也是這樣,但bind內部有一系列的演算法,比閉包複雜多了)
複製程式碼

對於react中使用bind和閉包傳參效能的比較

Chrome Version 72.0.3626.119 (Official Build) (64-bit)

react version "16.8.3"

node version v10.15.1

通過chrome的JavaScript Profiler進行效能分析,發現渲染1千次事件的渲染時間是差不多的(均採用首次重新整理渲染)

  1. 首先bind方式:

    bind方式

  2. 採用閉包的形式:

    clipboard.png

  3. 採用不帶引數的模式:

clipboard.png

可以看到效能上雙方相差不多,而Stack Overflow上的 stackoverflow.com/questions/1… 以及在chrome中各個版本bind和closure以及proxy效能測試(點選)發現bind是要比closure慢的多, 對於bind的實現如下:

Function.prototype.bind = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
複製程式碼

發現內部的實現也是閉包!當然這個不是完整的處理,從實現上bind是對閉包的封裝,可讀性來說bind好,因此bind是要比closure慢的,但是 v8做了優化,導致在react中差異也不是很大。

v8裡面的實現(來自stackoverflow.com/questions/1…),還沒有檢視最新的實現。

function FunctionBind(this_arg) { // Length is 1.
  if (!IS_SPEC_FUNCTION(this)) {
    throw new $TypeError('Bind must be called on a function');
  }
  var boundFunction = function () {
    // Poison .arguments and .caller, but is otherwise not detectable.
    "use strict";
    // This function must not use any object literals (Object, Array, RegExp),
    // since the literals-array is being used to store the bound data.
    if (%_IsConstructCall()) {
      return %NewObjectFromBound(boundFunction);
    }
    var bindings = %BoundFunctionGetBindings(boundFunction);

    var argc = %_ArgumentsLength();
    if (argc == 0) {
      return %Apply(bindings[0], bindings[1], bindings, 2, bindings.length - 2);
    }
    if (bindings.length === 2) {
      return %Apply(bindings[0], bindings[1], arguments, 0, argc);
    }
    var bound_argc = bindings.length - 2;
    var argv = new InternalArray(bound_argc + argc);
    for (var i = 0; i < bound_argc; i++) {
      argv[i] = bindings[i + 2];
    }
    for (var j = 0; j < argc; j++) {
      argv[i++] = %_Arguments(j);
    }
    return %Apply(bindings[0], bindings[1], argv, 0, bound_argc + argc);
  };

  %FunctionRemovePrototype(boundFunction);
  var new_length = 0;
  if (%_ClassOf(this) == "Function") {
    // Function or FunctionProxy.
    var old_length = this.length;
    // FunctionProxies might provide a non-UInt32 value. If so, ignore it.
    if ((typeof old_length === "number") &&
        ((old_length >>> 0) === old_length)) {
      var argc = %_ArgumentsLength();
      if (argc > 0) argc--;  // Don't count the thisArg as parameter.
      new_length = old_length - argc;
      if (new_length < 0) new_length = 0;
    }
  }
  // This runtime function finds any remaining arguments on the stack,
  // so we don't pass the arguments object.
  var result = %FunctionBindArguments(boundFunction, this,
                                      this_arg, new_length);

  // We already have caller and arguments properties on functions,
  // which are non-configurable. It therefore makes no sence to
  // try to redefine these as defined by the spec. The spec says
  // that bind should make these throw a TypeError if get or set
  // is called and make them non-enumerable and non-configurable.
  // To be consistent with our normal functions we leave this as it is.
  // TODO(lrn): Do set these to be thrower.
  return result;
複製程式碼

我們在bind的實現方法裡面可以看到一些額外的開銷,如'%_IsConstructCall()',這些額外的東西是為了實現bind的規範,這也造成了在大多數情況下bind比簡單的閉包慢的情況。 另一方面,bind方法也有稍微的不同,使用Function.prototype.bind創造的函式沒有原型屬性或者是[[Code]], [[FormalParameters]], 和 [[Scope]] 內部屬性。

總之,在bind和closure不成為效能瓶頸的時候,優先考慮可讀性,儘量保證程式碼的簡潔

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!

參考

  1. wulv.site/2017-07-02/…
  2. stackoverflow.com/questions/1…
  3. github.com/mqyqingfeng…
  4. developers.google.com/web/tools/c…

相關文章