Vue原始碼學習(二十):$emit、$on實現原理

养肥胖虎發表於2024-07-09

好傢伙,

0、一個例子

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>Vue 父子元件通訊示例</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>

<body>
    <div id="app">
        <parent-component></parent-component>
    </div>

    <script>
        // 子元件
        Vue.component('child-component', {
            template: `
        <div>
          <button @click="sendDataToParent">傳送資料給父元件</button>
        </div>
      `,
            methods: {
                sendDataToParent() {
                    this.$emit('data-sent', '這是從子元件傳送的資料');
                }
            }
        });

        // 父元件
        Vue.component('parent-component', {
            template: `
        <div>
          <child-component @data-sent="handleDataReceived"></child-component>
          <p>從子元件接收到的資料:{{ receivedData }}</p>
        </div>
      `,
            data() {
                return {
                    receivedData: ''
                };
            },
            methods: {
                handleDataReceived(data) {
                    this.receivedData = data;
                }
            }
        });

        // 建立Vue例項
        let vm = new Vue({
            el: '#app'
        });
    </script>
</body>

</html>

1、$emit、$on原始碼

原始碼實現,我們來看$emit、$on的原始碼實現部分

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (isArray(event)) {
        for (var i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn);
        }
    }
    else {
        (vm._events[event] || (vm._events[event] = [])).push(fn);
        // optimize hook:event cost by using a boolean flag marked at registration
        // instead of a hash lookup
        if (hookRE.test(event)) {
            vm._hasHookEvent = true;
        }
    }
    return vm;
};

Vue.prototype.$emit = function (event) {
    var vm = this;
    // 處理大小寫
    {
        var lowerCaseEvent = event.toLowerCase();
        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
            tip("Event \"".concat(lowerCaseEvent, "\" is emitted in component ") +
                "".concat(formatComponentName(vm), " but the handler is registered for \"").concat(event, "\". ") +
                "Note that HTML attributes are case-insensitive and you cannot use " +
                "v-on to listen to camelCase events when using in-DOM templates. " +
                "You should probably use \"".concat(hyphenate(event), "\" instead of \"").concat(event, "\"."));
        }
    }
    var cbs = vm._events[event];
    if (cbs) {
        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
        var args = toArray(arguments, 1);
        var info = "event handler for \"".concat(event, "\"");
        for (var i = 0, l = cbs.length; i < l; i++) {
            invokeWithErrorHandling(cbs[i], vm, args, vm, info);
        }
    }
    return vm;
};

function invokeWithErrorHandling(handler, context, args, vm, info) {
    var res;
    try {
        res = args ? handler.apply(context, args) : handler.call(context);
        if (res && !res._isVue && isPromise(res) && !res._handled) {
            res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
            res._handled = true;
        }
    }
    catch (e) {
        handleError(e, vm, info);
    }
    return res;
}

2.程式碼解釋

看著比較複雜,所以我們精簡一下,去掉效能最佳化和一些正規表示式還有一些陣列處理

精簡下來無非幾句程式碼

$on

(vm._events[event] || (vm._events[event] = [])).push(fn);

$emit

var cbs = vm._events[event];

invokeWithErrorHandling(cbs[i], vm, args, vm, info);

function invokeWithErrorHandling(handler, context, args, vm, info) {
   
        res = args ? handler.apply(context, args) : handler.call(context);

        return res;

}

分析:

$emit、$on的實現使用了觀察者模式的設計思想

$on方法用於在當前Vue例項上註冊事件監聽器。

vm._events:維護一個事件與其處理函式的對映。每個事件對應一個陣列,陣列中存放了所有註冊的處理函式。

$emit方法用於觸發事件,當事件被觸發時,呼叫所有註冊在該事件上的處理函式。

非常簡單

3.原始碼註釋版本

// 在Vue的原型上定義一個方法$on
Vue.prototype.$on = function (event, fn) {
    // vm指的是Vue的例項
    var vm = this;
    // 如果event是一個陣列,那麼對每個事件遞迴呼叫$on方法
    if (isArray(event)) {
        for (var i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn);
        }
    }
    // 如果event不是一個陣列,那麼將函式fn新增到vm._events[event]中
    else {
        (vm._events[event] || (vm._events[event] = [])).push(fn);
        // 如果event是一個鉤子事件,那麼設定vm._hasHookEvent為true
        if (hookRE.test(event)) {
            vm._hasHookEvent = true;
        }
    }
    // 返回Vue的例項
    return vm;
};

// 在Vue的原型上定義一個方法$emit
Vue.prototype.$emit = function (event) {
    // vm指的是Vue的例項
    var vm = this;
    // 處理事件名的大小寫
    {
        var lowerCaseEvent = event.toLowerCase();
        // 如果事件名的小寫形式和原事件名不同,並且vm._events中有註冊過小寫的事件名
        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
            // 那麼提示使用者事件名的大小寫問題
            tip("Event \"".concat(lowerCaseEvent, "\" is emitted in component ") +
                "".concat(formatComponentName(vm), " but the handler is registered for \"").concat(event, "\". ") +
                "Note that HTML attributes are case-insensitive and you cannot use " +
                "v-on to listen to camelCase events when using in-DOM templates. " +
                "You should probably use \"".concat(hyphenate(event), "\" instead of \"").concat(event, "\"."));
        }
    }
    // 獲取vm._events[event]中的所有回撥函式
    var cbs = vm._events[event];
    // 如果存在回撥函式
    if (cbs) {
        // 如果回撥函式的數量大於1,那麼將其轉換為陣列
        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
        // 獲取除event外的其他引數
        var args = toArray(arguments, 1);
        // 定義錯誤處理資訊
        var info = "event handler for \"".concat(event, "\"");
        // 對每個回撥函式進行錯誤處理
        for (var i = 0, l = cbs.length; i < l; i++) {
            invokeWithErrorHandling(cbs[i], vm, args, vm, info);
        }
    }
    // 返回Vue的例項
    return vm;
};

// 定義一個錯誤處理函式
function invokeWithErrorHandling(handler, context, args, vm, info) {
    var res;
    try {
        // 如果存在引數args,那麼使用apply方法呼叫handler,否則使用call方法呼叫handler
        res = args ? handler.apply(context, args) : handler.call(context);
        // 如果返回結果res存在,且res不是Vue例項,且res是一個Promise,且res沒有被處理過
        if (res && !res._isVue && isPromise(res) && !res._handled) {
            // 那麼對res進行錯誤處理,並標記res已經被處理過
            res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
            res._handled = true;
        }
    }
    // 如果在執行過程中丟擲錯誤,那麼進行錯誤處理
    catch (e) {
        handleError(e, vm, info);
    }
    // 返回結果res
    return res;
}

相關文章