美文网首页VuevueKagashino的Vue.js提高班
【Vue.js】 那些相似的 API,我该用哪个? Vue AP

【Vue.js】 那些相似的 API,我该用哪个? Vue AP

作者: Kagashino | 来源:发表于2020-09-03 21:31 被阅读0次

    前言

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

    props vs $attrs

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

    export default {
        props: {
            uid: Number,
            name: {
                type: String,s
                default: 'Zhangsan' 
            },
        }
    }
    
    <personal-info :uid="1" name="Xiaoming" mobile="123456789" />
    

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

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

    <template>
        <button v-bind="$attrs" :disabled="loading">
            <slot><slot>
        </button>
    </template>
    <script type="text/javascript">
        export default {
            props: {
                loading: Boolean // 除了 loading 其余的属性都被打入 $attrs 中了
            }
        }
    </script>
    

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

    <my-button 
        id="foo" 
        class="btn normal" 
        :style="buttonStyle"
        :loading="loading"
        @click="$alert('你在想🍑')">点击获得 100 块</my-button>
    

    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 需要多个函数转换时,可能会出现嵌套现象:

    <span>价格 {{ method1(method2(value || 0)) }}</span>
    

    filters 可以避免这个问题:

    <span>年龄: {{ value | filter1 | filter2 }}</span>
    

    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 事件,也可以完成“双向绑定”效果:

    <template>
        <div>
            <!-- 拼凑 value -->
            <span>{{value}}</span>
            <!-- 拼凑 @change -->
            <button @click="$emit('change', value + 1)">+</button>
            <button @click="$emit('change', value - 1)">-</button>
        </div>
    </template>
    
    <script type="text/javascript">
    export default {
        props: {
            value: Number
        }
    }
    </script>
    

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

    <my-counter v-model="myCount" />
    

    等价于:

    <my-counter :value="myCount" @change="v => myCount = v" />
    

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

    <template>
        <div>
            <!-- 自定义属性 count -->
            <span>{{count}}</span>
            <!-- 自定义事件 setCount -->
            <button @click="$emit('setCount', value + 1)">+</button>
            <button @click="$emit('setCount', value - 1)">-</button>
        </div>
    </template>
    export default {
        model: {
            prop: 'count',
            event: 'setCount'
        },
        props: {
            count: Number
        }
    }
    

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

    .sync

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

    <template>
        <div>
            <span>{{count}}</span>
            <button @click="$emit('update:count', count + 1)">+</button>
            <button @click="$emit('update:count', count - 1)">-</button>
        </div>
    </template>
    
    <script type="text/javascript">
    export default {
        props: {
            count: Number
        }
    }
    </script>
    

    外层使用:

    <my-counter :count.sync="myCount" />
    

    等价于:

    <my-counter :count="myCount" @update:count="c => myCount = c" />
    

    与 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}`)
        }
    }
    

    最终生成:

    <font color="">Hello World</font>

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

    <div><span>{{ msg }}</span></div>
    

    经过 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)
        }
    }
    

    使用:

    <my-title :level="1" style="color:#333">这是 h1</my-title>
    <my-title :level="2" style="color:#666">这是 h2</my-title>
    <my-title :level="3" style="color:#999">这是 h3</my-title>
    

    试想一下:如果我们使用 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 functional>
        <button class="my-button" v-bind="data.attrs" v-on="listeners">
            <slot></slot>
        </button>
    </template>
    

    正常组件 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 AP

        本文链接:https://www.haomeiwen.com/subject/oizosktx.html