vuejs 中的 computed,watch(和 filter)

本文深入探讨Vue.js中的计算属性(computed)和监视器(watch),讲解它们的使用场景、区别及如何优化视图数据处理。通过实例演示,帮助读者理解何时使用计算属性进行高效的数据转换,何时采用监视器实现数据变化的实时响应。

我们现在已经知道如何把模型中的数据放到视图里,但这里有一个问题,如果我们模型里的数据要经过处理才能放到视图上的话,我们就需要用到方法。比如说我们在模型里有一个 username 属性,它的值是小写的,但是我们想把它转换成大写,我们就要对数据进行处理,如下面的代码所示

<div id="box">
   <h3>{{ name }}</h3>
   <h3>{{ name.toUpperCase() }}</h3>
</div>
<script>
    var vm = new Vue({
        el: "#box",
        data: {
            name: 'lili',
        }
    })
</script>

上面的方法是直接在插值表达式里使用方法,我们知道插值表达式是支持方法的,所以没问题,能得到我们想要的结果。但是这还只是简单的一个方法,如果它很复杂的话,我们直接写在插值表达式里就很不好了,不好看,也会让页面加载负荷变大。那我们是不是可以在别的地方先把这个操作做完,然后在差值表达式里调用,这样不就可以了吗。而在vuejs 里也提供了这么一个空间(computed),让我们可以再里面写逻辑业务

computed:计算属性

computed 翻译过来叫做计算出来,意思是某个属性是依靠其他的模型变量进行计算得来的。它其实也是一个对象,用来存放方法的

特点:它里面定义属性是依赖于其他的属性的,并且计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。注意,如果某个依赖 (比如非响应式属性) 在该实例范畴之外,则计算属性是不会被更新的。

注意:1:computed 和data 一样是 Vue 里面的一个属性,它和 data 还有 el 是同级的
2:我们一般用 es6 语法去写函数,更便利
3:计算属性将被混入到 Vue 实例中,里面的所有 this 都是指向 Vue
4:所以在里面最好不要使用箭头函数,因为如果你使用了箭头函数,那么 this 就会指向 window
5:我们在用它的时候不要加(),在 computed 里面,是属性方法调用,函数方法书写,所以不要加()!不要加()!不要加()!

我们来看上面的例子,上面是直接在插值表达式里进行运算,现在我们可以在 computed 里先执行,然后把值返回到视图就可以了

computed 演示
<div id="box">
   <h3>{{ name }}</h3>
   <h3>{{ upperName }}</h3>
</div>
<script>
    var vm = new Vue({
        el: "#box",
        data: {
            name: 'lili',
        },
        computed: {
        //es6 语法
        upperName (){
        	return this.name.toUpperCase();
        	}
        }
    })
</script>

从上面可以看出,computed 计算属性的实质是 封装一个函数,然后在需要的地方去调用它,我们想到,之前我们说过 methods 这个属性就是用来存放方法的,那为什么我们不把这个方法放到 methods 里呢?我们用 methods 试一下

methods 演示
<div id="box">
   <h3>{{ name }}</h3>
   
   <h3>{{ upperName }}</h3>
   <h3>{{ upperName }}</h3>
   
   <h3>{{ mName() }}</h3>
   <h3>{{ mName() }}</h3>
</div>
<script>
    var vm = new Vue({
        el: "#box",
        data: {
            name: 'lili',
        },
        computed:{
            upperName ( ){
                return this.name.toUpperCase();
            }
        },
        methods: {
            mName ( ){
                return this.name.toUpperCase();
            }
        }
    })
</script>

我们可以看到,methods 也可以实现效果,那他们之间有什么不同呢?

computed 和 methods 的不同

1:computed 是属性方法调用,函数方法书写,在使用它的时候不需要加();而 methods 是函数方法调用,要加();
2:当 属性值不发生变化的时候,computed 只执行一次,而 methods 则是有几个地方用到它,就执行几次,如上面的例子,只要 name 的属性值不发生变化,computed 就是执行一次,而 methods 会执行两次

watch:监视,监视某个模型的变化

在表单里,常常有这样的需求,就是在你输入用户名或者密码的时候,在后面会给出提示,如输入字符不能小于2个,输入字符不能大于20个,并且提示是随着你实时输入的东西进行改变的。我们之前实现这种功能是用了 onchange 事件来实时监测输入框里内容的变化,而在 vuejs 里,它给出了我们一个属性(watch),就是用来监测某个模型的变化的,我们来看下面的列子

<div id="box">
	username: <input type="text" v-model.trim="username" /> <span style="color: red">{{ tip }}</span>
</div>
<script>
    var vm = new Vue({
        el: "#box",
        data: {
            username: '',
            tip: '',
        },
        // 这里用watch,监视的是 username
        watch: {
            username ( newVal, oldVal ) {
                if(!newVal){
                    this.tip = "用户名不能为空";
                    return;
                }
                if(newVal.length <= 2){
                    this.tip = "用户名长度不能小于2";
                    return;
                }
                if(newVal.length >= 20){
                    this.tip = "用户名长度不能大于10";
                    return;
                }
                this.tip = '';
            }
        }

    })
</script>

从上面的例子我们可以看出,watch 是和 data 同级的一个属性,也是一个对象,它的属性名就是我们要监控的模型(username ),同时它有两个参数 newVal(新的值),oldVal (旧的值),监控的模型只要发生变化,新的值就会传给 newVal,我们就能实时监控模型的变化了
我们想到 computed 是依赖于其他的属性的,那么他所依赖的属性如果发生变化,那么 computed 也会发生改变,这样也是实时监控了模型,那 computed 和 watch 之间有什么区别呢?

computed 和 watch 的不同

其实他们的差别还是很大的
1:watch 是用来监控模型变化的,而 computed 用计算属性的,只不过他随着依赖属性的变化而变化,所以我们可以用它做来监控
2:如果我们的业务代理里面存在异步操作,则 computed 则不可以使用,则必须使用 watch 实现
但是就功能而言,他们的差别除了在异步里面之外,其他也没有什么了
对于目前的业务来说,我们只是实现前端的用户名的合法性校验, 真正要检验的应该是用户名是否在数据库里面重复,则这个时候需要使用ajax 进行异步请求后,得到响应的结果后进行消息的提示,发现 computed 里面的信息都是return出去的,但是如果有异步请求,则肯定会出现回调函数。则这个时候的 return 没有意义了。所以我们要用 watch

filter:过滤器

filter:过滤器,负责数据进行处理
他在2.x 中已经被移除了,虽然被移除,但是功能还在,所以我们还是要学习他。

vuejs 中自带了一些过滤器:
capitalize 首字母大写
uppercase 全部大写
lowercase 全部小写
currency 输出金钱以及小数点…

也可以自定义我们自己的过滤器

使用方法:

我们知道在操作系统里面有操作系统管道的使用方式,就是可以把前面一个命令的输出作为下一个命令的输入,(可以叫做 管道,或者叫做过滤器),在 vuejs 里面也提供了类似操作系统管道的使用方式,在vuejs里面可以叫做过滤器,有的地方(程序员)也叫作变量调节器,来看下面这个例子

<div id="box">
//capitalize 是vuejs 1.x 自带的过滤器,所以要使用的话,我们引进的 vuejs 版本要是 1.x 的
    <p>{{ name | capitalize }}</p>
    //运行后我们可以知道他的表现是:Lili
    //执行方式就是 name 作为 capitalize 的输入,经过这个过滤器的处理,把它转换成首字母大写的形式输出
</div>

<script>
    var vm = new Vue({
        el: "#box",
        data: {
            name: "lili"
        }
    })
</script>

由于过滤器在 2.x 版本已经被移除了,所以在这里就不多做解释了(主要是我自己也是只知道一点)

<think>我们正在处理一个关于优化Vue3中el-tree过滤计算属性的问题。用户希望使用computed属性来过滤树形数据,并返回匹配的节点,同时保持树形结构(即如果父节点匹配,则保留其所有子节点;如果子节点匹配,则保留其父节点路径)。 在Element Plus的el-tree组件中,过滤通常有两种方式: 1. 使用el-tree内置的filter-node-method(通过设置filter-method:filter-node-method属性) 2. 通过计算属性返回过滤后的树形数据 用户希望使用计算属性(computed)来返回过滤后的数据。因此,我们需要一个递归函数来遍历树,并根据条件保留匹配的节点及其祖先节点。 优化思路: 1. 使用深度优先遍历(DFS)或广度优先遍历(BFS)来遍历树节点。 2. 对于每个节点,检查其是否匹配(或它的任何子节点匹配),如果匹配则保留该节点,并递归处理其子节点。 3. 如果节点本身不匹配,但子节点有匹配的,则保留该节点(因为要展示子节点,必须保留父节点路径)。 具体步骤: 我们将创建一个递归函数,该函数: - 接收一个节点过滤条件(如关键字) - 检查该节点是否匹配(例如,节点标签包含关键字) - 递归处理子节点,得到新的子节点列表(过滤后的) - 如果该节点匹配,或者其子节点列表不为空(即子节点有匹配的),则保留该节点 注意:我们使用计算属性来返回整个过滤后的树。 示例代码: 假设我们的树形数据结构为: { id: 1, label: '一级 1', children: [ ... ] } 在Vue3的setup语法中: ```javascript import { computed, ref } from 'vue'; // 树形数据 const treeData = ref([...]); // 原始树数据 const filterText = ref(''); // 过滤文本 // 过滤函数,判断节点是否匹配 const filterNode = (value, data) => { if (!value) return true; return data.label.includes(value); // 这里以label为例,实际根据需求 }; // 递归函数,用于过滤树节点 const filterTree = (data, filterText) => { // 如果过滤文本为空,返回原始数据 if (!filterText) { return data; } // 遍历每个节点 return data .map(node => ({ ...node })) // 浅拷贝节点,避免修改原始数据 .filter(node => { // 递归处理子节点 if (node.children) { node.children = filterTree(node.children, filterText); } // 如果节点本身匹配,或者子节点有匹配(即子节点数组不为空),则保留该节点 return filterNode(filterText, node) || (node.children && node.children.length > 0); }); }; // 计算属性,返回过滤后的树 const filteredTree = computed(() => { return filterTree(treeData.value, filterText.value); }); ``` 在模板中使用: <el-tree :data="filteredTree" ... /> 但是,上述方法有一个问题:如果父节点不匹配,但是子节点匹配,那么父节点会被保留(因为子节点被保留后,父节点的children不为空),这符合我们的需求。 然而,上述递归函数在每次计算属性更新时都会遍历整个树,如果树很大,可能会有性能问题。我们可以通过一些优化来减少不必要的遍历,比如使用缓存、减少拷贝等。 优化建议: 1. 避免不必要的拷贝:如果数据是只读的,我们可以不拷贝,但这里我们修改了children,所以需要拷贝。如果数据量很大,可以考虑使用其他方式(如标记)来避免修改原始数据。 2. 使用记忆化(memoization):由于计算属性会依赖filterTexttreeData,我们可以利用缓存,当filterTexttreeData没有变化时,直接返回上一次的结果。但是,Vue的计算属性本身就有缓存,只要依赖项不变,就不会重新计算。 3. 如果树非常大,可以考虑使用非递归遍历(用栈或队列)来避免递归深度过大。 另外,注意:如果树的数据结构层次很深,递归可能导致栈溢出。可以使用迭代(循环)代替递归,但代码会复杂一些。 迭代方法(DFS)的伪代码: function filterTree(data, filterText) { const stack = [...data.map(node => ({ node, parent: null }))]; // 初始节点,以及它们的父节点引用(这里不需要) const result = []; // 最终结果 // 由于需要从叶子节点向上构建,所以可以考虑先收集所有节点,然后从叶子节点开始标记,再构建树。或者使用两次遍历。 } 考虑到递归的简洁性,在数据量不是特别大的情况下,递归是更合适的选择。 完整示例: ```vue <template> <div> <el-input v-model="filterText" placeholder="输入关键字过滤" /> <el-tree :data="filteredTree" :props="defaultProps" /> </div> </template> <script setup> import { ref, computed } from 'vue'; const filterText = ref(''); const treeData = ref([ { id: 1, label: 'Level one 1', children: [ { id: 4, label: 'Level two 1-1', children: [ { id: 9, label: 'Level three 1-1-1' }, { id: 10, label: 'Level three 1-1-2' } ] } ] }, { id: 2, label: 'Level one 2', children: [ { id: 5, label: 'Level two 2-1' }, { id: 6, label: 'Level two 2-2' } ] }, { id: 3, label: 'Level one 3', children: [ { id: 7, label: 'Level two 3-1' }, { id: 8, label: 'Level two 3-2' } ] } ]); const defaultProps = { children: 'children', label: 'label' }; const filterNode = (value, data) => { if (!value) return true; return data.label.includes(value); }; const filterTree = (data, keyword) => { if (!keyword) return data; const _filter = (list) => { return list .map(node => { // 先处理子节点 if (node.children) { const children = _filter(node.children); // 如果子节点有匹配,则保留该节点(即使节点本身不匹配) if (children.length) { return { ...node, children }; } } // 如果节点本身匹配,则保留(此时子节点可能为空,但也要保留节点本身) if (filterNode(keyword, node)) { // 如果节点匹配,我们保留节点,但子节点可能已经被过滤(所以这里不重新赋值children,因为上面已经处理了) // 但是注意:如果节点匹配,但子节点没有匹配,我们仍然要保留该节点(不保留子节点),所以这里直接返回节点,但子节点已经被过滤(为空数组) // 所以我们需要返回一个新的节点,并且子节点是过滤后的(可能为空) return { ...node, children: node.children ? _filter(node.children) : [] }; } // 否则返回null,表示不保留 return null; }) .filter(node => node !== null); }; return _filter(data); }; const filteredTree = computed(() => { return filterTree(treeData.value, filterText.value); }); </script> ``` 注意:上述递归函数中,我们先处理子节点,然后根据子节点的结果节点本身是否匹配来决定是否保留节点。这样保证了如果一个节点有子节点被保留,则该节点也会被保留(即使它本身不匹配)。同时,如果一个节点匹配,那么它也会被保留(即使它的子节点都被过滤掉,那么它的children将为空数组,但节点本身会显示)。 但是,上面的代码在节点匹配时,我们再次调用了_filter来处理子节点,这会导致重复递归。我们可以优化为: ```javascript const filterTree = (data, keyword) => { if (!keyword) return data; const _filter = (list) => { return list .map(node => { // 先复制节点 const newNode = { ...node }; // 如果有子节点,先过滤子节点 if (newNode.children) { newNode.children = _filter(newNode.children); } // 检查:如果节点本身匹配,或者子节点有匹配(过滤后children不为空),则保留 const isMatched = filterNode(keyword, newNode); if (isMatched || (newNode.children && newNode.children.length > 0)) { return newNode; } return null; }) .filter(node => node !== null); }; return _filter(data); }; ``` 这样写更简洁,而且避免了重复递归。但是注意:在节点匹配的情况下,我们并没有改变节点的子节点(因为子节点已经在上面的步骤中过滤了)。所以这个版本更高效。 因此,我们推荐使用这个优化后的递归函数。 总结: 1. 使用计算属性返回过滤后的树。 2. 递归遍历每个节点,先处理子节点,然后判断节点是否保留(节点匹配或子节点有匹配)。 3. 注意浅拷贝节点,避免修改原始数据(如果原始数据是响应式的,直接修改会导致问题)。 注意:如果树的数据量非常大,递归可能导致性能问题。在这种情况下,可以考虑使用非递归遍历,或者使用虚拟滚动(el-tree支持虚拟滚动,但需要手动开启,并且确保节点高度固定)。 另外,如果过滤条件变化频繁,可以使用防抖(debounce)来减少计算次数。 相关问题: 1. 如何在el-tree中实现节点过滤并高亮匹配文本? 2. 如何优化大型树形数据的渲染性能? 3. Vue3中如何使用watchEffect来监听过滤条件的变化并执行过滤? 4. 在el-tree中,如何实现多条件过滤(如同时匹配标签值)? 引用说明: 以上方法参考了Element Plus官方文档中关于树形控件过滤的示例,并结合了Vue3计算属性的特性[^1]。</think># Vue3中优化el-tree过滤计算属性以返回匹配节点 在Vue3中优化`el-tree`的过滤计算属性,使其正确返回匹配节点并保持树形结构,可以通过以下方法实现: ## 优化方案 ```vue <template> <div> <el-input v-model="filterText" placeholder="输入关键字过滤" /> <el-tree ref="treeRef" :data="filteredTreeData" :props="defaultProps" :filter-node-method="filterNode" node-key="id" default-expand-all /> </div> </template> <script setup> import { ref, computed, watch } from 'vue' // 原始树数据 const treeData = ref([ { id: 1, label: '一级 1', value: 'level1', children: [ { id: 4, label: '二级 1-1', value: 'level2-1' }, { id: 5, label: '二级 1-2', value: 'level2-2' } ] }, { id: 2, label: '一级 2', value: 'level1-2', children: [ { id: 6, label: '二级 2-1', value: 'level2-2-1' }, { id: 7, label: '二级 2-2', value: 'level2-2-2' } ] } ]) // 树配置 const defaultProps = { children: 'children', label: 'label' } // 过滤文本 const filterText = ref('') const treeRef = ref(null) // 优化后的计算属性 - 返回匹配节点并保持树结构 const filteredTreeData = computed(() => { if (!filterText.value) return treeData.value const filter = (nodes) => { return nodes.reduce((acc, node) => { // 检查当前节点是否匹配 const isMatch = node.label.includes(filterText.value) || node.value.includes(filterText.value) // 如果有子节点,递归过滤 let children = [] if (node.children) { children = filter(node.children) } // 如果当前节点匹配或子节点有匹配,保留该节点 if (isMatch || children.length > 0) { acc.push({ ...node, children }) } return acc }, []) } return filter(treeData.value) }) // 监听过滤文本变化 watch(filterText, (val) => { treeRef.value?.filter(val) }) // 节点过滤方法 const filterNode = (value, data) => { if (!value) return true return data.label.includes(value) || data.value.includes(value) } </script> ``` ## 关键优化点 1. **递归过滤算法**: - 使用深度优先遍历递归处理树结构 - 保留匹配节点及其所有祖先节点,确保树结构完整 - 时间复杂度为 $O(n)$,其中 $n$ 是节点总数 2. **计算属性优化**: - 使用 `computed` 属性自动缓存结果 - 仅当 `filterText` 或原始数据变化时重新计算 - 返回过滤后的新树结构,不修改原始数据 3. **匹配条件**: - 同时检查节点的 `label` `value` 属性 - 支持部分匹配(包含关系) - 可扩展为多字段匹配 4. **性能考虑**: ```javascript // 优化后的递归函数 const filter = (nodes) => { return nodes.reduce((acc, node) => { const isMatch = /* 匹配条件 */ const children = node.children ? filter(node.children) : [] if (isMatch || children.length) { acc.push({ ...node, children }) } return acc }, []) } ``` ## 完整优化实现 ```vue <template> <div class="tree-container"> <el-input v-model="filterText" placeholder="输入关键字过滤" clearable class="filter-input" /> <el-tree ref="treeRef" :data="filteredTreeData" :props="defaultProps" :filter-node-method="filterNode" node-key="id" default-expand-all highlight-current class="custom-tree" > <template #default="{ node, data }"> <span class="custom-node"> {{ data.label }} <el-tag size="small">{{ data.value }}</el-tag> </span> </template> </el-tree> </div> </template> <script setup> import { ref, computed, watch } from 'vue' // 树数据 const treeData = ref([ { id: 1, label: '技术部门', value: 'tech', children: [ { id: 2, label: '前端开发', value: 'frontend' }, { id: 3, label: '后端开发', value: 'backend' }, { id: 4, label: '运维团队', value: 'devops' } ] }, { id: 5, label: '市场部门', value: 'marketing', children: [ { id: 6, label: '数字营销', value: 'digital' }, { id: 7, label: '品牌推广', value: 'branding' } ] }, { id: 8, label: '人力资源', value: 'hr', children: [ { id: 9, label: '招聘', value: 'recruitment' }, { id: 10, label: '培训', value: 'training' } ] } ]) const defaultProps = { children: 'children', label: 'label' } const filterText = ref('') const treeRef = ref(null) // 优化后的计算属性 const filteredTreeData = computed(() => { if (!filterText.value.trim()) return treeData.value const filter = (nodes) => { return nodes.reduce((result, node) => { // 检查当前节点是否匹配 const isMatch = node.label.toLowerCase().includes(filterText.value.toLowerCase()) || node.value.toLowerCase().includes(filterText.value.toLowerCase()) // 递归处理子节点 let children = [] if (node.children) { children = filter(node.children) } // 保留匹配节点或包含匹配子节点的节点 if (isMatch || children.length > 0) { const newNode = { ...node } if (children.length) { newNode.children = children } result.push(newNode) } return result }, []) } return filter(treeData.value) }) // 监听过滤文本变化 watch(filterText, (val) => { treeRef.value?.filter(val) }) // 节点过滤方法 const filterNode = (value, data) => { if (!value) return true return data.label.toLowerCase().includes(value.toLowerCase()) || data.value.toLowerCase().includes(value.toLowerCase()) } </script> <style> .tree-container { max-width: 600px; margin: 20px auto; padding: 20px; border: 1px solid #ebeef5; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .filter-input { margin-bottom: 20px; } .custom-tree { max-height: 400px; overflow-y: auto; } .custom-node { font-size: 14px; } .el-tag { margin-left: 8px; } </style> ``` ## 性能优化建议 1. **使用虚拟滚动**: 对于大型树结构,启用虚拟滚动: ```vue <el-tree :data="filteredTreeData" :height="400" virtual /> ``` 2. **防抖处理**: 当输入频繁变化时,添加防抖: ```javascript import { debounce } from 'lodash-es' watch(filterText, debounce((val) => { treeRef.value?.filter(val) }, 300)) ``` 3. **缓存计算结果**: 使用 `computed` 属性自动缓存结果,避免不必要的重新计算 4. **避免深度拷贝**: 使用 `{ ...node }` 浅拷贝而非深拷贝,减少内存占用 5. **使用索引优化**: 对于非常大的数据集,可以预先构建索引: ```javascript const nodeIndex = computed(() => { const index = {} const buildIndex = (nodes) => { nodes.forEach(node => { index[node.id] = node if (node.children) buildIndex(node.children) }) } buildIndex(treeData.value) return index }) ``` ## 相关问题 1. 如何在el-tree中实现多条件过滤(如同时匹配标签值)? 2. 如何优化大型树形数据(10000+节点)的渲染性能? 3. Vue3中如何实现el-tree节点的懒加载? 4. 如何在el-tree过滤时高亮显示匹配的文本? 5. 如何实现el-tree的搜索历史记录自动补全功能? > 参考Element Plus官方文档中关于Tree组件过滤自定义节点的部分[^1],以及Vue3计算属性的最佳实践指南[^2]。 [^1]: Element Plus Tree组件文档: https://element-plus.org/en-US/component/tree.html [^^2]: Vue3计算属性指南: https://vuejs.org/guide/essentials/computed.html
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值