深入剖析 Vue 过滤器模块(十三)

深入剖析 Vue 过滤器模块

本人掘金号,欢迎点击关注:掘金号地址

本人公众号,欢迎点击关注:公众号地址

一、引言

在 Vue.js 的开发中,过滤器是一个实用且强大的功能。它允许开发者对数据进行格式化处理,以更友好的方式展示给用户。例如,将日期格式化为特定的样式,将数字进行货币格式化等。虽然在 Vue 3 中,过滤器已被废弃,推荐使用计算属性或方法来替代,但在大量的 Vue 2 项目中,过滤器仍然被广泛使用。理解过滤器的工作原理,不仅有助于开发者更好地使用它,还能为理解 Vue 的整体架构提供帮助。本文将从源码级别深入分析 Vue 过滤器模块,探究其内部实现机制。

二、过滤器的基本概念与使用

2.1 过滤器的定义

过滤器是 Vue.js 提供的一种数据格式化工具,它本质上是一个函数,接收一个值作为输入,并返回一个经过处理后的值。过滤器可以在插值表达式或 v - bind 指令中使用,通过管道符号 | 来调用。

2.2 全局过滤器的使用示例

javascript

javascript 复制代码
// 定义一个全局过滤器,用于将字符串转换为大写
// Vue.filter 方法用于注册全局过滤器,第一个参数是过滤器的名称,第二个参数是过滤器函数
Vue.filter('uppercase', function (value) {
    // 如果传入的值不是字符串,直接返回该值
    if (!value) return '';
    // 将字符串转换为大写并返回
    return value.toString().toUpperCase();
});

// 创建一个 Vue 实例
new Vue({
    el: '#app',
    data: {
        // 定义一个字符串数据
        message: 'hello world'
    }
});

html

javascript 复制代码
<!-- 在 HTML 模板中使用过滤器 -->
<div id="app">
    <!-- 使用 uppercase 过滤器将 message 转换为大写 -->
    {{ message | uppercase }}
</div>

2.3 局部过滤器的使用示例

javascript

javascript 复制代码
new Vue({
    el: '#app',
    data: {
        // 定义一个数字数据
        price: 123.45
    },
    filters: {
        // 定义一个局部过滤器,用于将数字格式化为货币形式
        currency: function (value) {
            // 如果传入的值不是数字,直接返回该值
            if (typeof value!== 'number') {
                return value;
            }
            // 使用 toFixed(2) 方法将数字保留两位小数,并添加货币符号
            return '$' + value.toFixed(2);
        }
    }
});

html

javascript 复制代码
<div id="app">
    <!-- 使用 currency 过滤器将 price 格式化为货币形式 -->
    {{ price | currency }}
</div>

2.4 过滤器链的使用

过滤器可以串联使用,即一个过滤器的输出可以作为另一个过滤器的输入。

javascript

javascript 复制代码
Vue.filter('uppercase', function (value) {
    // 如果传入的值不是字符串,直接返回该值
    if (!value) return '';
    // 将字符串转换为大写并返回
    return value.toString().toUpperCase();
});

Vue.filter('reverse', function (value) {
    // 如果传入的值不是字符串,直接返回该值
    if (!value) return '';
    // 将字符串反转并返回
    return value.toString().split('').reverse().join('');
});

new Vue({
    el: '#app',
    data: {
        // 定义一个字符串数据
        message: 'hello'
    }
});

html

javascript 复制代码
<div id="app">
    <!-- 先使用 reverse 过滤器将字符串反转,再使用 uppercase 过滤器将结果转换为大写 -->
    {{ message | reverse | uppercase }}
</div>

三、过滤器的注册机制

3.1 全局过滤器的注册源码分析

在 Vue 源码中,全局过滤器的注册是通过 Vue.filter 方法实现的。以下是简化后的源码分析:

javascript

javascript 复制代码
// 定义一个全局的 filters 对象,用于存储所有的全局过滤器
Vue.options.filters = Object.create(null);

// 实现 Vue.filter 方法
Vue.filter = function (id, definition) {
    // 如果只传入了一个参数,说明是获取过滤器
    if (!definition) {
        return this.options.filters[id];
    } else {
        // 如果传入了两个参数,说明是注册过滤器
        // 将过滤器函数存储到全局的 filters 对象中
        this.options.filters[id] = definition;
        return definition;
    }
};

上述代码中,Vue.options.filters 是一个全局的对象,用于存储所有的全局过滤器。Vue.filter 方法接收两个参数,id 是过滤器的名称,definition 是过滤器的函数定义。如果只传入一个参数,则表示获取该名称的过滤器;如果传入两个参数,则将该过滤器注册到全局的 filters 对象中。

3.2 局部过滤器的注册源码分析

局部过滤器是在组件的 filters 选项中定义的。在组件实例化的过程中,会将局部过滤器合并到组件的选项中。以下是简化后的源码分析:

javascript

javascript 复制代码
// 定义一个合并策略函数,用于合并过滤器选项
function mergeFilters(parentVal, childVal) {
    // 如果父级和子级都没有过滤器选项,返回空对象
    const res = Object.create(parentVal || null);
    if (childVal) {
        // 如果子级有过滤器选项,将子级的过滤器合并到结果对象中
        for (const key in childVal) {
            res[key] = childVal[key];
        }
    }
    return res;
}

// 在合并选项时,使用 mergeFilters 函数来合并 filters 选项
const strats = Vue.config.optionMergeStrategies;
strats.filters = mergeFilters;

// 组件实例化时,会调用合并选项的方法
function initOptions(vm) {
    // 合并选项
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        vm.$options || {},
        vm
    );
}

上述代码中,mergeFilters 函数用于合并父级和子级的过滤器选项。在组件实例化时,会调用 mergeOptions 方法来合并选项,其中 filters 选项会使用 mergeFilters 函数进行合并。

四、过滤器的解析与调用

4.1 模板解析阶段的过滤器解析

在 Vue 的模板解析阶段,会对插值表达式和指令中的过滤器进行解析。以下是简化后的源码分析:

javascript

javascript 复制代码
// 定义一个解析过滤器的函数
function parseFilters(exp) {
    // 定义一个正则表达式,用于匹配过滤器
    const filterRe = /|([^|]+)/g;
    let match;
    let filters = [];
    let index = 0;
    // 循环匹配过滤器
    while ((match = filterRe.exec(exp))) {
        // 获取过滤器的名称和参数
        const filterName = match[1].trim();
        const filterArgs = [];
        const argsIndex = filterName.indexOf(':');
        if (argsIndex > -1) {
            // 如果有参数,将参数提取出来
            const argStr = filterName.slice(argsIndex + 1);
            filterArgs.push(argStr);
            filterName = filterName.slice(0, argsIndex);
        }
        // 将过滤器名称和参数添加到过滤器数组中
        filters.push({
            name: filterName,
            args: filterArgs
        });
        index = match.index;
    }
    // 获取表达式的原始值
    const expValue = exp.slice(0, index);
    return {
        exp: expValue,
        filters: filters
    };
}

上述代码中,parseFilters 函数用于解析插值表达式或指令中的过滤器。它使用正则表达式匹配过滤器,提取过滤器的名称和参数,并将其存储在一个数组中。最后返回一个对象,包含表达式的原始值和过滤器数组。

4.2 过滤器的调用源码分析

在渲染函数执行时,会调用解析后的过滤器。以下是简化后的源码分析:

javascript

javascript 复制代码
// 定义一个应用过滤器的函数
function applyFilters(value, filters, vm) {
    // 遍历过滤器数组
    for (let i = 0; i < filters.length; i++) {
        const filter = filters[i];
        // 获取过滤器的函数定义
        const filterFn = vm.$options.filters[filter.name];
        if (filterFn) {
            // 如果过滤器函数存在,调用过滤器函数
            if (filter.args.length > 0) {
                // 如果有参数,将参数传递给过滤器函数
                value = filterFn.apply(vm, [value].concat(filter.args));
            } else {
                // 如果没有参数,直接调用过滤器函数
                value = filterFn.call(vm, value);
            }
        }
    }
    return value;
}

// 在渲染函数中调用过滤器
function render() {
    // 解析过滤器
    const { exp, filters } = parseFilters('message | uppercase');
    // 获取表达式的值
    const value = this[exp];
    // 应用过滤器
    const filteredValue = applyFilters(value, filters, this);
    // 返回渲染结果
    return createVNode('div', null, filteredValue);
}

上述代码中,applyFilters 函数用于应用过滤器。它遍历过滤器数组,依次调用每个过滤器函数,并将上一个过滤器的输出作为下一个过滤器的输入。在渲染函数中,先解析过滤器,然后获取表达式的值,最后调用 applyFilters 函数应用过滤器,并返回渲染结果。

五、过滤器的参数传递

5.1 过滤器参数的使用示例

过滤器可以接受参数,参数通过冒号 : 分隔。

javascript

javascript 复制代码
Vue.filter('formatDate', function (value, format) {
    // 如果传入的值不是日期类型,直接返回该值
    if (!value) return '';
    // 根据传入的格式参数对日期进行格式化
    if (format === 'yyyy - MM - dd') {
        return value.toISOString().split('T')[0];
    } else if (format === 'dd/MM/yyyy') {
        const date = value.getDate();
        const month = value.getMonth() + 1;
        const year = value.getFullYear();
        return `${date}/${month}/${year}`;
    }
    return value;
});

new Vue({
    el: '#app',
    data: {
        // 定义一个日期数据
        date: new Date()
    }
});

html

javascript 复制代码
<div id="app">
    <!-- 使用 formatDate 过滤器,并传递格式参数 -->
    {{ date | formatDate('yyyy - MM - dd') }}
</div>

5.2 过滤器参数的源码解析

在过滤器解析阶段,会对过滤器的参数进行解析。以下是简化后的源码分析:

javascript

javascript 复制代码
function parseFilters(exp) {
    const filterRe = /|([^|]+)/g;
    let match;
    let filters = [];
    let index = 0;
    while ((match = filterRe.exec(exp))) {
        const filterName = match[1].trim();
        const filterArgs = [];
        const argsIndex = filterName.indexOf(':');
        if (argsIndex > -1) {
            // 提取过滤器的参数
            const argStr = filterName.slice(argsIndex + 1);
            // 解析参数,这里简单处理,实际可能需要更复杂的解析
            const args = argStr.split(',').map(arg => arg.trim());
            filterArgs.push(...args);
            filterName = filterName.slice(0, argsIndex);
        }
        filters.push({
            name: filterName,
            args: filterArgs
        });
        index = match.index;
    }
    const expValue = exp.slice(0, index);
    return {
        exp: expValue,
        filters: filters
    };
}

上述代码中,在解析过滤器时,如果发现过滤器名称后面有冒号 :,则提取冒号后面的参数,并将其存储在 filterArgs 数组中。在调用过滤器时,会将参数传递给过滤器函数。

六、过滤器与响应式数据的交互

6.1 过滤器对响应式数据的处理

过滤器可以处理响应式数据,当响应式数据发生变化时,过滤器会重新计算。以下是一个示例:

javascript

javascript 复制代码
new Vue({
    el: '#app',
    data: {
        // 定义一个响应式的数字数据
        number: 10
    },
    filters: {
        // 定义一个过滤器,用于将数字乘以 2
        double: function (value) {
            return value * 2;
        }
    }
});

html

javascript 复制代码
<div id="app">
    <!-- 使用 double 过滤器处理响应式数据 -->
    {{ number | double }}
    <button @click="number++">增加数字</button>
</div>

当点击按钮增加 number 的值时,过滤器会重新计算,页面上显示的值也会相应更新。

6.2 源码层面的响应式处理分析

在 Vue 的响应式系统中,当响应式数据发生变化时,会触发 setter 方法,进而通知所有依赖该数据的 watcher 进行更新。过滤器的计算也依赖于响应式数据,因此当数据变化时,过滤器会重新计算。以下是简化后的源码分析:

javascript

javascript 复制代码
// 定义一个响应式对象
function defineReactive(obj, key, val) {
    // 创建一个 Dep 对象,用于收集依赖
    const dep = new Dep();
    // 获取对象的属性描述符
    const property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return;
    }
    // 获取属性的 getter 和 setter
    const getter = property && property.get;
    const setter = property && property.set;
    // 递归地将对象的属性转换为响应式
    let childOb = observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter? getter.call(obj) : val;
            if (Dep.target) {
                // 收集依赖
                dep.depend();
                if (childOb) {
                    childOb.dep.depend();
                }
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter? getter.call(obj) : val;
            if (newVal === value || (newVal!== newVal && value!== value)) {
                return;
            }
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            // 递归地将新值转换为响应式
            childOb = observe(newVal);
            // 通知所有依赖更新
            dep.notify();
        }
    });
}

// 定义一个 Watcher 对象,用于监听数据变化
class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm;
        this.cb = cb;
        // 将当前 Watcher 对象赋值给 Dep.target
        Dep.target = this;
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        // 获取表达式的值,触发依赖收集
        this.value = this.get();
        Dep.target = null;
    }
    get() {
        const vm = this.vm;
        // 调用表达式的 getter 方法,触发依赖收集
        let value = this.getter.call(vm, vm);
        return value;
    }
    update() {
        // 当数据变化时,调用回调函数
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

// 在渲染函数中创建 Watcher 对象
function render() {
    const vm = this;
    // 创建一个 Watcher 对象,监听数据变化
    new Watcher(vm, 'number', function (newVal, oldVal) {
        // 当数据变化时,重新计算过滤器
        const { exp, filters } = parseFilters('number | double');
        const value = vm[exp];
        const filteredValue = applyFilters(value, filters, vm);
        // 更新 DOM
        updateDOM(filteredValue);
    });
    const { exp, filters } = parseFilters('number | double');
    const value = vm[exp];
    const filteredValue = applyFilters(value, filters, vm);
    return createVNode('div', null, filteredValue);
}

上述代码中,defineReactive 函数用于将对象的属性转换为响应式,当属性值发生变化时,会通知所有依赖该属性的 Watcher 对象进行更新。Watcher 对象在创建时会触发依赖收集,当数据变化时,会调用 update 方法,重新计算过滤器并更新 DOM。

七、过滤器的性能优化

7.1 避免在过滤器中进行复杂计算

过滤器应该尽量保持简单,避免在过滤器中进行复杂的计算。如果需要进行复杂的计算,建议使用计算属性或方法。例如,以下是一个在过滤器中进行复杂计算的示例:

javascript

javascript 复制代码
Vue.filter('complexCalculation', function (value) {
    // 进行复杂的计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
        result += i;
    }
    return result + value;
});

new Vue({
    el: '#app',
    data: {
        number: 10
    }
});

html

javascript 复制代码
<div id="app">
    {{ number | complexCalculation }}
</div>

上述代码中,complexCalculation 过滤器进行了大量的循环计算,这会影响性能。可以将复杂计算提取到计算属性中:

javascript

javascript 复制代码
new Vue({
    el: '#app',
    data: {
        number: 10
    },
    computed: {
        complexResult: function () {
            let result = 0;
            for (let i = 0; i < 1000000; i++) {
                result += i;
            }
            return result + this.number;
        }
    }
});

html

javascript 复制代码
<div id="app">
    {{ complexResult }}
</div>

7.2 缓存过滤器的计算结果

如果过滤器的计算结果是固定的,可以考虑缓存计算结果,避免重复计算。以下是一个缓存过滤器计算结果的示例:

javascript

javascript 复制代码
Vue.filter('cachedFilter', function (value) {
    // 定义一个缓存对象
    const cache = {};
    if (cache[value]) {
        // 如果缓存中存在结果,直接返回
        return cache[value];
    }
    // 进行计算
    const result = value * 2;
    // 将结果存入缓存
    cache[value] = result;
    return result;
});

new Vue({
    el: '#app',
    data: {
        number: 10
    }
});

html

javascript 复制代码
<div id="app">
    {{ number | cachedFilter }}
</div>

上述代码中,cachedFilter 过滤器使用一个缓存对象来存储计算结果,如果缓存中已经存在该值的计算结果,则直接返回缓存中的结果,避免了重复计算。

八、过滤器在不同场景下的应用

8.1 日期格式化

在前端开发中,日期格式化是一个常见的需求。可以使用过滤器来实现日期的格式化。

javascript

javascript 复制代码
Vue.filter('formatDate', function (value, format) {
    if (!value) return '';
    const date = new Date(value);
    if (format === 'yyyy - MM - dd') {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
    } else if (format === 'dd/MM/yyyy') {
        const date = date.getDate();
        const month = date.getMonth() + 1;
        const year = date.getFullYear();
        return `${date}/${month}/${year}`;
    }
    return value;
});

new Vue({
    el: '#app',
    data: {
        date: new Date()
    }
});

html

javascript 复制代码
<div id="app">
    {{ date | formatDate('yyyy - MM - dd') }}
</div>

8.2 货币格式化

货币格式化也是一个常见的需求,可以使用过滤器来实现货币的格式化。

javascript

javascript 复制代码
Vue.filter('currency', function (value, symbol = '$') {
    if (typeof value!== 'number') {
        return value;
    }
    return `${symbol}${value.toFixed(2)}`;
});

new Vue({
    el: '#app',
    data: {
        price: 123.45
    }
});

html

javascript 复制代码
<div id="app">
    {{ price | currency }}
</div>

8.3 字符串截断

在显示长字符串时,可能需要将字符串截断并添加省略号。可以使用过滤器来实现字符串的截断。

javascript

javascript 复制代码
Vue.filter('truncate', function (value, length = 10) {
    if (!value) return '';
    if (value.length <= length) {
        return value;
    }
    return value.slice(0, length) + '...';
});

new Vue({
    el: '#app',
    data: {
        longText: 'This is a very long text that needs to be truncated.'
    }
});

html

javascript 复制代码
<div id="app">
    {{ longText | truncate(20) }}
</div>

九、过滤器与其他 Vue 特性的结合使用

9.1 过滤器与计算属性的结合

过滤器可以与计算属性结合使用,先通过计算属性进行数据处理,再使用过滤器进行格式化。

javascript

javascript 复制代码
new Vue({
    el: '#app',
    data: {
        numbers: [1, 2, 3, 4, 5]
    },
    computed: {
        // 计算数组中所有数字的总和
        sum: function () {
            return this.numbers.reduce((acc, val) => acc + val, 0);
        }
    },
    filters: {
        // 定义一个过滤器,用于将数字格式化为货币形式
        currency: function (value) {
            return '$' + value.toFixed(2);
        }
    }
});

html

javascript 复制代码
<div id="app">
    <!-- 先计算总和,再使用 currency 过滤器进行格式化 -->
    {{ sum | currency }}
</div>

9.2 过滤器与指令的结合

过滤器可以与指令结合使用,对指令绑定的值进行格式化。

javascript

javascript 复制代码
Vue.filter('uppercase', function (value) {
    if (!value) return '';
    return value.toString().toUpperCase();
});

new Vue({
    el: '#app',
    data: {
        message: 'hello world'
    }
});

html

javascript 复制代码
<div id="app">
    <!-- 使用 v - bind 指令绑定值,并使用 uppercase 过滤器进行格式化 -->
    <input v - bind:value="message | uppercase">
</div>

十、总结与展望

10.1 总结

通过对 Vue 过滤器模块的深入分析,我们了解了过滤器的基本概念、注册机制、解析与调用过程、参数传递、与响应式数据的交互、性能优化、不同场景下的应用以及与其他 Vue 特性的结合使用。过滤器是一个实用的工具,它可以帮助我们对数据进行格式化处理,提高代码的可读性和可维护性。然而,在 Vue 3 中,过滤器已被废弃,推荐使用计算属性或方法来替代。这是因为过滤器的功能可以通过计算属性或方法更清晰地实现,并且避免了一些潜在的问题。

10.2 展望

虽然过滤器在 Vue 3 中被废弃,但在大量的 Vue 2 项目中仍然被广泛使用。对于 Vue 2 开发者来说,深入理解过滤器的原理可以更好地维护和优化现有项目。对于 Vue 3 开发者来说,虽然不再使用过滤器,但可以借鉴过滤器的思想,使用计算属性或方法来实现数据的格式化和处理。未来,随着 Vue 生态的不断发展,可能会有更强大和灵活的数据处理工具出现,帮助开发者更高效地进行前端开发。

相关推荐
冬阳春晖17 分钟前
web animation API 锋利的css动画控制器 (更新中)
前端·javascript·css
Python私教1 小时前
使用FastAPI和React以及MongoDB构建全栈Web应用05 FastAPI快速入门
前端·react.js·fastapi
浪裡遊1 小时前
Typescript中的对象类型
开发语言·前端·javascript·vue.js·typescript·ecmascript
杨-羊羊羊2 小时前
什么是深拷贝什么是浅拷贝,两者区别
开发语言·前端·javascript
发呆的薇薇°2 小时前
在vue里,使用dayjs格式化时间并实现日期时间的实时更新
前端·javascript·vue.js
m0_627827522 小时前
vue3中 input 中放大镜在后面
javascript·vue.js·elementui
七冬与小糖2 小时前
【本地搭建npm私服】使用Verdaccio
前端·npm·node.js
从味书2 小时前
安装typescript时,npm install -g typescript报错
javascript·typescript·npm
lally.2 小时前
2025御网杯wp(web,misc,crypto)
前端·ctf
海绵不是宝宝8172 小时前
React+Springboot项目部署ESC服务器
前端·react.js·前端框架