Vue的一些开发技巧

父子组件的生命周期

父组件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,达到解耦。它支持三种配置方式:

  • props: true 对应组件内可以通过 props 接收 params 参数。
// 动态路由的 :xxx 其实会作为 params 参数
const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User,
        props: true
    }]
})
// 组件User
export default {
    props: ['id'],
}
  • 对象
routes: [{
    // ...
    props: { newsletterPopup: false }
}]
  • 函数:回调的第一个参数就是$route
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

  • 父组件
    <checkbox v-model="chkStatus"  @change="onChangedListener" />
    // 可以显示注册 change 事件,做一些额外处理
    // 但 change 事件会优先修改 v-model 绑定的变量,不需要手动修改chkStatus
    
  • 子组件Checkbox .vue
    <template>
       <input type="checkbox" v-bind:checked="state"  v-on:change="setChange" />
    </template>
    <script>
       export default {
          model: {
             prop: 'state',   // 默认是 value
             event: 'change',
          },
          props: {
              state: Boolean  // 默认是value,model中修改为state
          },
          methods: {
             setChange(evt) {
                this.$emit("change", evt.target.checked);
             }
          }
       }
    

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

<FormItem>
    <KMInput />
</FormItem>
  • 子组件 KMInput
    this.$parent.$emit('xxx', xxx) //通过父组件派发事件
    
  • 父组件 FormItem
    mounted() {
       // 注册事件
       this.$on('xxx', () => {
          // ...
       })
    }
    

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

修饰符sync

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

  • 父组件::xxx.sync="myProp"
     <Child :visible.sync="showDlog" />
    
  • 子组件:this.$emit('update:xxx', value);
     <button @click="updeShow">更新</button>
    
     props: {
         visible: {
             type: Boolean,
             default: false
         }
     }
     updeShow() {
         this.$emit('update:visible',false);  // 分发以 update: 为前缀的事件
     }
    

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

函数式组件

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

  • 模板上标记
    <template functional>
    </template>
    
  • 属性标记
    export default {
        name: 'MenuItem',
        functional: true,
        props: {
            icon: {
                type: String,
                default: ''
            },
            title: {
                type: String,
                default: ''
            }
        },
        render(h, context) {  // 返回一个VNode
            const { icon, title } = context.props
            // 支持JSX语法
            const vnodes = []
            if (icon) {
                vnodes.push(<svg-icon icon-class={icon}/>)
            }
            if (title) {
                vnodes.push(<span slot='title'>{(title)}</span>)
            }
            return vnodes
        }
    }
    

样式穿透

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

  • less
    <style scoped lang="less">
    .content /deep/ .el-button {
        height: 60px;
    }
    </style>
    
  • scss
    <style scoped lang="scss">
    .content ::v-deep .el-button {
        height: 60px;
    }
    </style>
    
  • stylus
    <style scoped ang="stylus">
    外层 >>> .el-checkbox{
       font-size: 20px;
    }
    </style>
    

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 传递过来的参数值)。

  • 该组件被多个地方使用
  • 每个父组件中对该组件的内部有一部分需要特殊定制
  • slot可以让更好地复用组件的同时并对其定制化处理
  • 可以理解为父组件向子组件传递了一段 html 文本
    要求:
    1.子组件模板包含至少一个 插槽 <slot></slot>
    2.父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身

provide / inject

  • 父组件向所有子孙组件传递数据,不管孙组件有多深。
  • 但却难以实现响应式,这是设计者刻意为之。
  • 适合传递方法、初始化的值。

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

把所有props传到子组件

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

attrs 与listeners

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

  • $attrs
    父组件在使用子组件时,通常会在子组件上声明一些属性,而子组件会使用 prop 接收这些属性。
    子组件中的 this.$attrs 就是在子组件上声明的静态属性(attr="xxx") 和动态属性(:attr="xxx") 的对象集合,其中不包括 class 和 style 两个属性
    最重要的是,子组件可以通过 v-bind="$attrs" 向下继续传递!这在父组件与孙组件通信时非常有用!
    但如果子组件中声明了propthis.$attrs 将去除 prop 中声明的属性。
  • $listeners
    包含了父组件中的 v-on (@xxx="xxx") 事件监听器,但不包括 .native 修饰器的。它可以通过 v-on="$listeners" 继续向下传给孙组件。
    孙组件通过 this.$emit('xxx', args) 可以直接给父组件发送事件。
  • inheritAttrs
    默认情况下,父组件在子组件上声明的一系列属性,但没有被子组件的 props 声明,这些属性会包含在 this.$attrs 中,但同时也会作为普通字符串应用在子组件的根元素上。
    inheritAttrs 应用在子组件中,默认为 true,设置为 false 会去除默认行为,不再把 this.$attrs 中的属性以普通字符串的形式应用在自己的根节点上!
    注意:不会影响 classstyle 属性
# 子组件: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.component('draggable', draggable)
  • Vue.use() 内部调用的仍是 Vue.component()去注册全局组件/插件,但它可以做更多事情,比如多次调用 Vue.component() 一次性注册多个组件,还可以调用Vue.directive()、Vue.mixins()、Vue.prototype.xxx=xxx 等等,其第二个可选参数又可以传递一些数据。
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 = /^\.\//)

  • directory {String} 读取目录的路径
  • useSubdirectories {Boolean} 是否递归遍历子目录
  • regExp {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

  • resolve 是一个函数,返回被解析后得到的模块id
  • keys 函数,返回一个数组,由所有可能被解析的req对象组成;在批量导入文件时会很有帮助;
        const req = require.context('./svg', false, /\.svg$/)
        function requireAll (r) {
            r.keys().forEach(r);
        }
        requireAll(req)
    
  • id 上下文模块的ID,在使用 module.hot.accept 时会用到。

高精度全局权限处理

权限控制由前端处理时,通常使用 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具备了现代框架应用的高级特性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354