【Vue.js】 那些相似的 API,我该用哪个? Vue API 用法大比拼

前言

Vue.js 的 API 比 React 稍多,某些 API 的功能有重叠的部分,在实际使用的时候,多少会令人造成困惑。遗憾的是,各种文章、博客理对这些 API 的介绍,都是单独挑出或者泛泛而谈(点名吐槽什么 Vue 传参的 X 种方式)。本文将从对比的角度出发,分析这些功能相似的 API 各自适合那些特定场景。
如果觉得文章太长,可以直接跳到【小结】部分。

props vs $attrs

同样是父组件传入的属性,区别在于: props 中声明的属性会被子组件捕获,并代理到组件实例上,未被捕获的属性会被放入 $attrs 中

export default {
    props: {
        uid: Number,
        name: {
            type: String,
            default: 'Zhangsan' 
        },
    }
}

props 捕获了 uid name ,所以组件内可以通过 this.uid this.name 访问。而 mobile 没有被翻牌,只好乖乖地排进 this.$attrs 里了。

在某些场景下,需要声明一个高阶组件,可以使用 v-bind="$attrs" 将当前组件未捕获的属性透传,比如定义一个 button 组件:



这样,我们就完成了一个除了 loading 以外其他属性和原生 button 一样的组件:

点击获得 100 块

data vs computed

data 直接意思是数据,确切地说是组件自身的状态,可以在组件内进行修改。而 computed 起到一个“归纳” 的作用,用来合并多个响应式数据,或者对响应式数据做一些逻辑计算,减少模板表达式的长度。

{
    props: {
        gradeAverage: Number, // 年级平均成绩
    },
    data () {
        return {
            students: [
                { name: 'xiaoming', score: 93 },
                { name: 'xiaohong', score: 96 },
                { name: 'zhang3', score: 77 },
                { name: 'li4', score: 89 },
                { name: 'wang5', score: 91 }
            ]
        }
    },
    computed: {
        // 计算平均分
        average() {
            const headCount = this.students.length;
            if (!headCount) {
                return 0;
            }
            const total = this.students.reduce((acc, student) => acc.score + student.score, 0);
            return (total / headCount).toFixed(4)
        },
        // 计算与年级平均分差值
        compareToGrade() {
            const delta = this.average - this.gradeAverage;
            const result = delta > 0 ? '高于' : '低于'
            return `${result} 年级平均 ${Math.abs(delta) 分}`
        }
    }
}

如果 computed 属性同时提供 get / set 方法,这个属性也能被赋值:

{
    data () {
        return {
            cash: 50
        }   
    },
    computed: {
        yuan: {
            get() {
                return `${this.cash} 元`
            },
            // v 参数就是等号右边的值
            set(v) {
                this.cash = v
            }
        }
    }
}

有了 set 方法,我们直接对 yuan 进行赋值操作,如 yuan = 168 ,就能触发 yuanset 方法,168 会作为参数传入。

watch 与 computed

告诉你们一个秘密:之所以放在一起说,是因为这两个 API 是兄弟关系,它们在源码中有相同的老爹 —— Watcher,且看:

export default {
    data() {
        return {
            params: {
                name: ''
            }
        }
    },
    watch: {
        'params.name' (newVal) {
            if (newVal.length > 20) {
                alert('名称不得超过 20 个字');
                this.params.name = newVal.slice(20);
            }
        }
    },
    computed: {
        inputName: {
            get() {
                return this.params.name;
            },
            set(newVal) {
                if (newVal.length > 20) {
                    alert('名称不得超过 20 个字');
                }
                this.params.name = newVal.slice(0, 20);
            }
        }
    }
}

当 vue 实例中的 params.name 改变,就会触发这个 watch 函数执行,当对inputName 进行赋值操作,如 this.inputName = 'xxx',就会触发 set 函数执行。
从语义上来说: watch 强调过程,当你的数据变更时可以用 watcher 处理的副作用。computed 强调的是结果,不管你用什么方法,只要返回值符合你的预期即可。

methods vs computed

(三英战 computed)

computed 与 method 不同的是,computed 会把计算结果缓存起来,当内部依赖改变并且直到下一次访问时,才会重新执行 get 函数。
来看例子:一个个人信息表单组件,初始化时从服务端获取数据,现在需要判断 params 的内容比较初始数据是否改过,使用 methodcomputed 两种方法实现:

{
    data() {
        return {
            oldParams: null,
            params: {
                name: '',
                age: '',
            }
        }
    },
    async created () {
        const info = await fetch('/user/info');
        Object.assign(this.params, info);
        // 原版数据因为不需要响应式,所以将它冻结起来
        oldParams = Object.freeze(info);
    },
    methods: {
        hasChanged() {
            if (!this.oldParams) {
                return false;
            }
            return (
                oldParams.name === params.name 
                && oldParams.age === params.age
            )
        },
    },
    computed: {
        computeChanged () {
            if (!this.oldParams) {
                return false;
            }
            return (
                oldParams.name === params.name 
                && oldParams.age === params.age
            )
        }
    }
}

两种方式的代码一模一样,区别在于执行时机: 每次进行 hasChanged() 调用时 method 方法都会执行,这点没有疑问。而对于 computeChanged:当 oldParamsoldParams.nameoldParams.ageoldParamsoldParams 其中任何一项改变,computeChanged 会被标记为 dirty ,再次访问 computeChanged,才会重新调用这个求值函数,写段代码:

export default {
    created() {
        this.computeChanged // computed 第一次调用
        for (let i = 0; i < 100; i++) {
            this.computedChanged // 依赖项没有改变,computed 不会再次调用
        }

        this.params.name = 'xxxx' // 依赖项改变, computed 标记为 dirty

        this.computedChanged // 依赖项改变以后的求值, computed 更新调用
    }
}

不得不说 computed 实乃响应式 API 的精髓,如果希望你的 template 代码变薄,请务必利用好 computed 。

methods vs filters

相对于 methods, filter 有以下几个特点:

  • filters 只能在 template 中使用
  • filters 相当于管道操作符(熟悉 shell 的程序员非常容易理解),可以将输入数据从左往右传递
  • filters 是上下文无关的,无法在内部访问 this
  • filters 支持全局注入,注入以后无需 import 代码就能在每个实例引用

当你的 value 需要多个函数转换时,可能会出现嵌套现象:

价格 {{ method1(method2(value || 0)) }}

filters 可以避免这个问题:

年龄: {{ value | filter1 | filter2 }}

watch 与 $watch

上文中的 watch 监听是声明式的选项,跟随组件创建和销毁。如果你需要随时撤销监听操作,可以调用组件实例上的 $watch ,它的返回值是一个撤销函数,调用即撤销:

export default {
    data() {
        return: {
            params: {
                name: '',
            },
            cancel: null
        }
    },
    methods: {
        startWatching() {
            // 将取消函数赋给 data
            this.cancel = this.$watch('params.name', ()=>{
                // do sth
            })
        },
        stopWatching() {
            if (!this.cancel) {
                return;
            }
            this.cancel();
        }
    }
}

另外,$watch 的第一个参数支持传入函数,以便实现更复杂的触发条件,比如:

this.$watch(
    // 触发条件
    ()=> {
        const { name } = this.params;
        if (!name) {
            return '名称不能为空'
        }
        if (name.length > 20) {
            return '名称不能超过 20 个字'
        }
    },
    // 触发回调
    (message) => {
        if (message) {
            alert(message)
        }
    },
    // 延迟执行
    {
        immediate: false
    }
)

v-model vs .sync

二者都是模板的语法糖,其中 v-model 就是各路文章鼓吹的“双向绑定”,其实没那么玄乎,vue 在模板编译期会把这个指令拆成 value 和 change(或者 input) 事件罢了。由于单向数据流的关系,子组件不能直接修改 props ,需要通过发送事件实现,而 v-model / .sync 在模板层面帮你做了这层封装:

v-model

v-model 一般用于原生的 input 、 checkbox 、 select 表单作为“双向绑定”指令,当普通组件引用中出现 v-model 指令,会自动推导为 :value@change。利用这个特性,如果在组件内部拼凑出 value 属性和 change 事件,也可以完成“双向绑定”效果:




外层引用组件时,传入 v-model ,就可以像普通输入框一样使用了!


等价于:


在 2.2.0+ 版本,vue 支持指定 model 选项,自定义字段名和 event 事件名:


export default {
    model: {
        prop: 'count',
        event: 'setCount'
    },
    props: {
        count: Number
    }
}

注意,一个组件只能定义一个 model,如果希望定义多个,请看下文的 .sync 修饰符:

.sync

同步属性属性修饰符,通过约定的事件格式,通知父组件同步更新,原理跟 v-model 相似。不同的是,事件名称要遵循 update:属性 格式:




外层使用:


等价于:


与 v-model 不同,.sync 可以修饰多个属性

render vs template

我们平时编写的 template 并不会马上转化为虚拟 DOM 节点,而是先编译成 render 函数。且看官方文档中 render 函数的引子:

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

说得再直白一点:render 函数是真正创建虚拟 DOM 的函数:

export default {
    render (createElement) {
        const { msg } = this;
        return createElement('h3', { style: { color: '#66ccff' } }, `Hello ${msg}`)
    }
}

最终生成:

Hello World

举个例子:如果提供这么一个 template 模板:

{{ msg }}

经过 Vue 的编译,它将变成:

function () {
    with(this){
        return createElement(
            'div',
            [
                createElement('span', [ createTextVNode( toString(msg) ) ])
            ]
        )
    }
}

如文档所言,模板可以更直观地映射 HTML ,但某些特定场景下,直接使用 render 函数会比模板更灵活,官方文档中介绍了一种动态标题的示例,根据 level 属性决定渲染 h1 ~ h6 标签,使用 render 函数可以这么写:

export default {
    name: 'my-title'
    props: {
        level: {
            type: Number
        },
    },
    render(createElement) {
        // 获取传入的属性、和插槽(子模板)
        const { $attrs, level, $slot } = this;
        const children = $slot.default;
        // 标签名称 h1 ~ h6
        const tag = `h${level}`; 
        return createElement(tag, $attrs, children)
    }
}

使用:

这是 h1
这是 h2
这是 h3

试想一下:如果我们使用 template 语法来实现的话就要声明6个 v-if 模板,非常啰嗦。不过 render 函数也是一把双刃剑:使用 render 函数无法使用 v-model 这个模板专属的语法糖,同时也失去了静态模板优化的可能性(有得必有失)。

也许你已经发现: createElement 的思路与 React.createElement 一脉相承。既然可以通过 JSX 编写 ReactElement ,能否使用 JSX 编写 render 函数呢?答案是肯定的:根据官方文档介绍,通过这个 Babel 插件,就能在 Vue 文件中愉快地使用JSX了,详情移步官方文档

functional vs stateful

假如你的组件本身不需要响应式数据,所有的行为都受控父组件的输入,不妨对这个组件添加 functional 属性:

export default {
    // 标记为函数式
    functional: true,
    name: 'my-button',
    props: {
        type: String,
        style: [Object, String]
    },
    render (h, context) {

        console.log(this); // 函数式组件没有 this,为 undefined
        // 函数式组件的上下文 context
        // 注意因为没有 this 所以上下文属性就不能叫做实例属性了,名称开头不带 $
        const { props, slot, listeners } = context; 
        const { type = 'info', style } = props;
        // 根据 type 决定背景颜色
        const backgroundColor = {
            danger: 'red',
            success: 'green',
            info: 'blue',
            disabled: 'gray'
        }[type];

        return h(
            'button', 
            {
                'class': 'my-btn',
                style: {
                    color: 'white',
                    outline: 'none',
                    backgroundColor,
                    ...style,
                },
                on: listeners
            },
            slot.default
        )
    }

}

上面的代码是为了实现一个简单的样式 button , 因为添加了 functional: true 属性,这个组件就变成了一个函数式组件,与普通的组件(我称之为 stateful 组件)不同,它是无状态的,不能访问 this ,需要借助 context 对象,同时 datacomputedwatch 等响应式 API 无法使用(只能响应父组件的)。但是它更为轻量,比 stateful 组件创建减少了不必要的开销。

另外,如果需要使用函数式模板,在 template 标签中添加 functional 属性即可,这里又偷懒直接用官方示例:


正常组件 template 被包裹在 this 中,而函数式组件就被包裹为 context 中,可以直接引用 context 上的属性。

$set vs assignment 赋值

export default {
    data () {
        params: {
            name: ''
        }
    },
    methods: {
        setAge (age) {

            if (!age) {
                delete this.params.age;
                return;
            }

            this.params.age = age;
        }
    }
}

由于 Object.defineProperty 的局限,直接对对象进行添加/删除属性操作是无法被监听的,需要换成 vm.$setvm.$delete

setAge(age) {
    if (!age) {
        this.$delete(this.params, 'age');
        return;
    }
    this.$set(this.params, 'age', age);
}

小结

  • props 中声明的属性会被捕获并挂在组件实例上,否则会挂进 $attrs
  • 使用 v-bind="$attrs" 可以实现属性透传
  • data 可以理解为组件中的响应式状态
  • watchcomputed 都是 Watcher 的实例。 watch 注重过程和副作用,computed 注重结果
  • computed 具有缓存响应式依赖的效果
  • filters 可以理解为模板的管道操作符,与上下文(组件实例)无关
  • $watch 是挂在组件实例上的方法,可以随时调用和撤销,支持复杂的触发条件
  • v-modelvalue@change 的合体,组件中唯一
  • .sync 是约定化的父子组件通信,组件中可以定义多个
  • render 是真正生成虚拟 DOM 的方法,template 最终会被编译成 render 函数。直接使用 render 函数可以更灵活地处理组件表现逻辑。
  • functional 是无状态的组件
  • 如果需要对响应式数据添加/删除属性,需要借助 $set/$delete 方法。

你可能感兴趣的:(【Vue.js】 那些相似的 API,我该用哪个? Vue API 用法大比拼)