Vue的一些开发技巧

2020-06-01  本文已影响0人  hellomyshadow

父子组件的生命周期

父组件Home,子组件List

Home beforeCreate -> Home created -> Home beforeMount -
-> List beforeCreate -> List created -> List beforeMount -> List mounted -
-> Home mounted
Home beforeDestroy -> List beforeDestroy -> List destroyed -> Home destroyed

路由参数解耦

this.$route.params.xxx 的用法会使组件与$route高度耦合,从而使组件只能再某些特定URL上使用,限制了其灵活性;
正确的做法应当是配置路由参数props,达到解耦。它支持三种配置方式:

// 动态路由的 :xxx 其实会作为 params 参数
const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User,
        props: true
    }]
})
// 组件User
export default {
    props: ['id'],
}
routes: [{
    // ...
    props: { newsletterPopup: false }
}]
routes: [{
    // ...
    props: (route) => ({
        id: route.query.id
    })
}]

面包屑

利用 this.$route.matched 可得到路由匹配的数组,按顺序解析可得到路由层次关系

// Breadcrumb.vue 面包屑组件
watch: {
    $route() {
        console.log(this.$route.matched)
    }
}

自定义v-model

v-model 其实只是一个语法糖,默认会向组件传递一个名为 valueprops,并注册一个名为 input 的事件。
这个 value 绑定的值就是 v-model 的值,input 事件用于修改v-model绑定的变量值,从而形成双向绑定,也避免了props的单向传递(子组件不能直接修改props中的变量值)

<input type="text" v-model="price" />
# --> 等效于(语法糖):
<input type="text" :value="price" @input="price=$event.target.value">

当然,可以通过配置 model 属性,修改默认的value 、input,通过 this.$emit('xxx', v) 手动触发事件。

在自定义组件上使用 v-model

$emit()$on() 的本质是:谁监听,谁派发

<FormItem>
    <KMInput />
</FormItem>

校验库:async-validatorelement-ui也是用这个库

修饰符sync

syncv-model 颇为相似,都是为了优雅而不粗鲁的实现父子组件间的双向绑定。但.syncv-model 更灵活,一个组件上可以有多个.sync,但只能有一个v-model

综上可知,:xxx.sync="myProp" 其实就是 @update:xxx="value => myProp = value":xxx="myProp" 的语法糖。
典型应用:Dialog对话框

函数式组件

函数式组件
函数式组件没有管理任何状态(响应式数据),没有实例(this),也没有监听任何传递给它的状态,也没有生命周期方法。
实际上,它只是一个接受一些 prop 的函数。
组件标记为 functional,因为是一个函数,所以渲染效率高于普通组件。

样式穿透

在开发中修改第三方组件的样式时,但由于 scoped 属性的样式隔离,可能需要另起一个<style>。但这种做法都会带来副作用(组件样式污染、不够优雅),样式穿透css预处理器中使用才生效。

watch

  1. 只监听对象的某个或一些属性,而不是用 deep: true 深度监听所有属性
    watch: {
      'obj.a': {
        handler(newV, oldV) {
          console.log('obj.a changed');
        },
        immediate: true, // 立即执行一次handler方法
      }
    }
    
  2. handler指向methods中的方法
    watch: {
        studen: {
            handler: 'sayName',
            immediate: true  // 创建组件后立即执行一次
        }
    },
    methods: {
        sayName() {
            console.log(this.studen)
        }
    }
    
  3. 触发多个监听方法,使用数组可以设置多项,形式包括字符串、函数、对象
    watch: {
        name: [
            'sayName1',
            function(newVal, oldVal) {
                // ...
            },
            {
                handler: 'sayName2',
                immaediate: true
            }
        ]
    },
    methods: {
        sayName1() {
            console.log('sayName1==>', this.name)
        },
        sayName2() {
            console.log('sayName2==>', this.name)
        }
    }
    
  4. 同时监听多个属性的变化:把多个变量包装成compouted
    compouted: {
        msgObj() {
            const { msg1, msg2 } = this
            return { msg1, msg2 }
        }
    },
    watch: {
        msgObj: {
            handler(newVal, oldVal) {
                if (newVal.msg1 != oldVal.msg1) {
                    console.log('msg1 is change')
                }
                if (newVal.msg2 != oldVal.msg2) {
                    console.log('msg2 is change')
                }
            },
            deep: true  // 深度监听
        }
    }
    

监听子组件的生命周期

  1. 通过 $emit 通知父组件
    // 子组件List
    export default {
        mounted() {
            this.$emit('listenMounted')
        }
    }
    
    // 父组件
    <template>
        <div>
            <List @listenMounted="listenMounted" />
        </div>
    </template>
    
  2. 更优雅的方式:使用 @hook: 即可监听子组件生命周期
    // 子组件无需做任何改变,父组件直接监听子组件的生命周期方法
    <template>
        <List @hook:mounted="listenMounted" />
    </template>
    

程序化的事件侦听器

以清除定时器为例:在mounted中注册定时器,在beforeDestroy中清除定时器;
可以使用$on('hook:')$once('hook:')来简化生命周期的注册。

export default {
    mounted() {
        // 连续注册两个定时器,无需额外变量,也不担心忘记清除
        this.creatInterval('hello')
        this.creatInterval('world')
    },
    creatInterval(msg) {
        let timer = setInterval(() => {
            console.log(msg)
        }, 1000)
        // 监听组件的销毁
        this.$once('hook:beforeDestroy', () => {
            clearInterval(timer)
        })
    }
}

mixins

mixins:混入,扩展组件的data属性和方法。

slot

slot:插槽,分为普通插槽,具名插槽(对多个slot进行命名),作用域插槽(父组件可以接收来自子组件的 slot 传递过来的参数值)。

provide / inject

如果传递一个引用型数据,如数组arr,只要父组件不主动修改 arr 的指向,而是通过 push、splice 这些方法去修改数组元素,孙组件是可以响应的!

把所有props传到子组件

<template>
  <childComponent v-bind="$props" />
</template>

attrs 与listeners

$attrs$listeners 的主要应用是实现多层嵌套传递,爷孙组件

# 子组件:KMInput
<div>
    <input :value="value" v-bind="$attrs" @input="$emit('input', e.target.value)">
</div>
export default {
    inheritAttrs: false,   // 根组件上不继承未被 props 声明的属性
    props: {
        value: {
            type: String,
            default: ''
        }
    }
}
# 父组件:
<KMInput v-model="pwd" type="password" class="test-km" />
# 映射到子组件-->
<div class="test-km">
    <input :value="value" type="password" @input="$emit('input', e.target.value)">
</div>

Vue.use 与 Vue.component

它们都是用于注册全局组件/插件的,不同的是,Vue.component() 每次只能注册一个组件,功能很单一。

Vue.use({
    install:function (Vue, options) {
        // 接收传递的参数: { name: 'My-Vue', age: 22 }
        console.log(options.name, options.age)
        Vue.directive('my-directive',{
            inserted(el, binding, vnode) { }
        })
        Vue.mixin({
            mounted() { }
        })
        Vue.component('draggable', draggable)
        Vue.component('Tree', Tree)
    }
}, { name: 'My-Vue', age: 22 })

$createElement

this.$createElement方法用来创建和返回虚拟节点。例如,利用它在可以通过v-html指令传递的方法中使用标记。
在函数组件中,此方法将作为渲染函数render中的第一个参数进行访问。

JSX

Vue CLI 3默认支持JSX。使用JSX可以很方便地编写函数式组件。如果尚未使用Vue CLI 3,则可以使用插件babel-plugin-transform-vue-jsx获得JSX得支持。

require.context

一个Webpack的API,通过 require.context() 获取一个特定的上下文(创建自己的context),主要用来实现自动化导入模块。
它会遍历文件夹中的指定文件,然后自动化导入,而不需要每次都显式使用 import/require 语句导入模块!
在前端工程中,如果需要一个文件夹引入很多模块,则可以使用 require.context()

require.context(directory, useSubdirectories = false, regExp = /^\.\//)

读取home目录下的.vue文件

    const path = require("path");

    const files = require.context("./home", false, /\.vue$/)
    const modules = {}
    files.keys().forEach(item => {
        const name = path.basename(item, ".vue")
        // .default 是因为Vue组件是通过 export default 导出的
        modules[name] = files(item).default
    })
    // ---> 批量引入,或者全局挂载
    export default {
        components: {
            ...modules
        },

不管是.vue文件,还是 .js.json 文件,都是前端项目的一个模块,都可以通过API实现自动化导入。
require-context

context module

require.context 返回值是一个 require function

function webpackContext(req) {
    return __webpack_require__(webpackContextResolve(req));
}
function webpackContextResolve(req) {
    var id = map[req];
    if(!(id + 1)) { // check for number or string
        var e = new Error("Cannot find module '" + req + "'");
        e.code = 'MODULE_NOT_FOUND';
        throw e;
    }
    return id;
}
webpackContext.keys = function webpackContextKeys() {
    return Object.keys(map);
}

这个function有三个属性:resolve、keys、id

高精度全局权限处理

权限控制由前端处理时,通常使用 v-if / v-show 控制元素对不同权限的响应效果。这种情况下,就会导致很多不必要的重复代码,不容易维护,因此可以造一个小车轮,挂在全局上对权限进行处理。

    // 注册全局自定义指令,对底层原生DOM操作
    Vue.directive('permission', {
        // inserted → 元素插入的时候
        inserted(el, binding){
            // 获取到 v-permission 的值
            const { value } = binding
            if(value) {
                // 根据配置的权限,去当前用户的角色权限中校验
                const hasPermission = checkPermission(value)
                if(!hasPermission){
                    // 没有权限,则移除DOM元素
                    el.parentNode && el.parentNode.removeChild(el)
                }
            } else{
                throw new Error(`need key! Like v-permission="['admin','editor']"`)
            }
        }
    })
    // --> 在组件中使用 v-permission
    <button v-permission="['admin']">权限1</button>
    <button v-permission="['admin', 'editor']">权限2</button>

小知识点

  1. data:组件的data必须是一个函数,因为一个组件可能被多次引用,它们之间的data应该是独立的,所以使用函数每次返回一个新的data
  2. <component>:vue中的一个动态组件标签,其 v-bind:is 属性可以绑定不同的组件,实现动态切换组件。
  3. vue-loader.vue文件称为单文件组件,通过webpack的一个loader --- vue-loader将单文件组件转为JavaScript模块。 默认支持ES2015
  4. 模板编译<template>会被编译成AST语法树,再经过generate得到render函数,其返回值为VNode --- Vue的虚拟DOM节点
    • parse过程:将template利用正则转化成AST抽象语法树。
    • optimize过程:标记静态节点,diff过程跳过静态节点,提升性能。
    • generate过程:生成render字符串。
      司徒大佬的文章: 前端模板的原理与实现
  5. Vue采用Virtual DOM的原因
    一方面是出于对性能的考量:
    • 创建真实DOM的代价很高:真实DOM节点实现的属性有很多,而VNode仅仅实现一些必要的属性。
    • 触发多次浏览器重绘及回流:使用VNode相当于加了一个缓冲,让一次数据变动所带来的所有Node变化,先在VNode中修改,再 diff 之后对所有产生差异的节点集中一次对 DOM Tree 进行修改,以减少浏览器的重绘和回流。

然而,性能受场景的影响是非常大的,不同的场景可能造成不同实现方案之间成倍的性能差距,所以依赖细粒度绑定及Virtual DOM哪个性能更好不是一个容易下定论的问题。
更重要的是为了解耦HTML依赖,有两个好处:
1. 不再依赖HTML解析器进行模板解析,可以进行更多的AOT工作提高运行时的效率:通过模板AOT编译,Vue的运行时体积可以进一步压缩,运行时效率可以进一步提升。
2. 可以渲染到DOM以外的平台,实现SSR、同构渲染这些高级特性,Weex等框架应用的就是这一特性。
综上所述,Virtual DOM在性能上的收益并不是最主要的,更重要的是,它使Vue具备了现代框架应用的高级特性。

上一篇 下一篇

猜你喜欢

热点阅读