• 这几个小技巧,让你书写不一样的Vue!


    前言

    最近一直在阅读Vue的源码,发现了几个实战中用得上的小技巧,下面跟大家分享一下。

    同时也可以阅读我之前写的Vue文章

    隐藏在源码中的技巧

    在实例化Vue时,首先调用的是Vue.prototype._init方法,而在此方法中mergeOptions()方法返回的options将运用在所有的初始化函数中。也就是如下代码:

    vm.$options = mergeOptions( resolveConstructorOptions(vm.coustructor), options || {}, vm)

    其中mergeOptions方法在core/util/options.js文件中,最后这个方法返回的是一个options对象。

    return options

    这就说明了mergeOptions方法最终将合并处理后的选项返回,并以该返回值作为vm.$options的值。

    自定义选项的合并方法

    如果我们在options中添加自定义的属性会怎么样?来自官方的例子:

    new Vue({  
        customOption: 'foo',  
        created: function () {    
            console.log(this.$options.customOption) // => 'foo'  
        }
    })

    在创建Vue实例的时候传递了一个自定义选项:customOptions,在created中可以通过this.$options.customOption进行访问。原理其实就是使用mergeOptions函数对自定义选项进行合并处理。对于自定义选项的合并,采用的是默认的合并策略:

    // 当一个选项没有对应的策略函数时,使用默认策略
    const strat = strats[key] || defaultStrat

    defaultStrat函数就定义在options.js文件内,源码如下

    /** * Default strategy. */
    const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal}

    defaultStart作用如其名,它是一个默认的合并策略:只要子选项不是undefined则使用子选项,否则使用父选项。最终的效果就是你初始化了什么,得到的就是什么。

    而且Vue也提供了一个全局配置叫Vue.config.optionMergeStrategies,这个对象就是选项合并中的策略对象,所以可以通过它指定某一个选项的合并策略,常用于指定自定义选项的合并策略,比如可以给customOption选项指定一个合并策略,只需要在config.optionMergeStrategies上添加与选项同名的策略函数即可:

    Vue.config.optionMergeStrategies.customOption = function (parentVal, childVal) { 
        return parentVal ? (parentVal + childVal) : childVal
    }

    自定义customOption的合并策略是:如果没有parentVal则直接返回childVal,否则返回两者的和。

    比如下面例子:

    // 创建子类
    const Sub = Vue.extend({ customOption: 1})
    // 以子类创建实例
    const vm = new Sub({ 
        customOption: 2,    
        created () {     
            console.log(this.$options.customOption) // 3    
        }
    })

    结果是在created方法中打印数字3。自定义合并选项的策略函数,这非常实用。

    vm.$createElement

    initRender中,定义了vm.$createElement()方法,initRendercore/instance/render.js文件。在该函数中有以下一段代码:

    // bind the createElement fn to this instance  
    // so that we get proper render context inside it.  
    // args order: tag, data, children, normalizationType, alwaysNormalize 
     // internal version is used by render functions compiled from templates  
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)  
    // normalization is always applied for the public version, used in  
    // user-written render functions.  
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

    这段代码在Vue实例对象上添加了两个方法:

    • vm._c
    • vm.$createElement

    这两个方法实际上是对内函数createElement的包装。

    render: function (createElement) { return createElement('h2', 'title')}

    总所周知,渲染函数的第一个参数就是createElement函数,该函数用来创建虚拟节点,通过上面的了解,你也可以这样写

    render: function () { return this.$createElement('h2', 'title')}

    上面这两段代码是完全等价的。

    provide & inject

    最后来瞧瞧provide和inject的原理。

    如果一个组件使用了provide选项,那么该选项指定的数据将会被注入到该组件的所有子组件中,在子组件中可以使用inject选项选择性注入,这样子组件就拿到了父组件提供的数据。

    provideinject都是在Vue.prototype._init方法中完成初始化工作的:

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')

    可以发现initInjections函数在initProvide函数之前被调用,这说明对于任何一个组件来说,总是要优先初始化inject选项,再初始化provide选项的。

    先来看看initInjections函数,它定义在src/core/instance/inject.js文件中

    export function initInjections (vm: Component) {  
        const result = resolveInject(vm.$options.inject, vm)  
        if (result) {   
             // 省略...  
        }
    }

    该函数接收组件实例对象作为参数,在函数内部首先定义了result常量,接下来要判断result为真才执行if语句内的代码。

    result常量的值是resolveInject函数的返回值。

    export function resolveInject (inject: any, vm: Component): ?Object {  if (inject) {    // 省略...    return result  }}

    可以看到,resolveInject函数接收两个参数,分别是inject选项以及组件实例对象。如果inject为真,那么将执行if语句块内的代码,最后返回result,否则返回undefined

    if语句块内的代码:

    const result = Object.create(null)
    const keys = hasSymbol  ? Reflect.ownKeys(inject).filter(key => {    /* istanbul ignore next */    return Object.getOwnPropertyDescriptor(inject, key).enumerable  })  : Object.keys(inject)

    首先通过Object.create(null)创建一个空对象并赋值给result。接着定义keys常量,保存inject选项对象的每一个键名。

    首先对hasSymbol进行判断,如果为真,则使用Reflect.ownKeys获取inject对象中所有可枚举的键名,否则使用Object.keys

    使用Reflect.ownKeys的好处是可以支持Symbol类型作为键名。

    接下来使用for循环遍历刚刚获取的keys数组

    for (let i = 0; i < keys.length; i++) {
        const key = keys[i];    
        const provideKey = inject[key].from    
        let source = vm    
        while (source) {      
            if (source._provided && hasOwn(source._provided, provideKey)) {  
                result[key] = source._provided[provideKey]          
                break      
            }      
            source = source.$parent    
        }
    }

    provideKey常量保存的是每一个inject选项所定义的注入对象的from属性的值。

    「题外话:」
    在规范化阶段中,inject选项会被规范为一个对象,并且对象必然含有from属性。

    inject: ['data1', 'data2']

    那么被规范化后vm.$options.inject选项将变为:

    {  
        'data1': { from: 'data1' },  
        'data2': { from: 'data2' }
    }

    接着while循环,内部有一个if条件判断,检测source._provided属性是否存在,并且source._provided对象自身是否拥有provideKey键,如果有,则说明找到了注入的数据:source._provided[provideKey],并将其赋值给result对象的同名属性。

    如果if为假,则执行source = source.$parent; 重新赋值source变量,使其引用父组件,以及类推就完成了向父代组件查找数据的需求,直到找到数据为止。

    如果找不到数据:

    if (!source) {
        if ('default' in inject[key]) {    
            const provideDefault = inject[key].default    
            result[key] = typeof provideDefault === 'function' ? provideDefault.call(vm) : provideDefault  
        } else if (process.env.NODE_ENV !== 'production') {    
            warn(`Injection "${key}" not found`, vm)  
        }
    }

    查看inject[key]对象中是否定义了default选项,如果定义了default选项则使用default选项提供的数据作为注入数据,否则在非生产环境下会提示开发者。

    最后如果查找到数据,则将result返回。但是result常量的值可能是不存在的,所以需要进行一个result的判断。为真时,说明成功取得注入的数据,此时执行if语句块的内容。

    toggleObserving(false)
    Object.keys(result).forEach(key => { 
         /* istanbul ignore else */  
        if (process.env.NODE_ENV !== 'production') {    
            defineReactive(vm, key, result[key], () => {      
                warn(`Avoid mutating an injected value directly since the changes will be ` +        `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key}"`,        vm      )   
            })  
        }else {    
            defineReactive(vm, key, result[key])  
        }
    })toggleObserving(true)

    通过遍历result常量并调用defineReactive函数在当前组件实例对象vm上定义与注入名称相同的变量,并赋予取得的值。

    在使用defineReactive之前,调用了toggleObsesrving(false)函数关闭响应式定义的开关,之后又将开关开启,这会导致使用defineReactive定义属性时不会将该属性的值转换为响应式。

    provide做的事很简单,先看看initProvide的内容:

    export function initProvide (vm: Component) {  
        const provide = vm.$options.provide
        if (provide) {    
            vm._provided = typeof provide === 'function' ? provide.call(vm) : provide  
        }
    }

    同样是接收组件实例对象作为参数。定义provide,是vm.$options.provide选项的引用,接着是if判断语句,只有在provide选项存在时才执行。

    procide可以是对象,也可以是函数,所以使用typeof检测类型,如果是函数则执行该函数获取数据,否则直接将provide本身作为数据。最后将数据赋值给组件实例对象的vm._provided属性。也就是前面说的inject内,注入的数据就是从父组件实例的vm._provided属性中获取的。

    「最后提示:」
    provideinject主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

    作者: zhangwinwin
    链接:这几个小技巧,让你书写不一样的Vue!
    来源:github

  • 相关阅读:
    合格linux运维人员必会的30道shell编程实践题及讲解
    合格linux运维人员必会的30道shell编程实践题及讲解-13
    合格linux运维人员必会的30道shell编程实践题及讲解-12
    合格linux运维人员必会的30道shell编程实践题及讲解-11
    合格linux运维人员必会的30道shell编程实践题及讲解-10
    合格linux运维人员必会的30道shell编程实践题及讲解-09
    学习Vim的四周计划
    Linux shell 逻辑运算符、逻辑表达式详解
    60个DevOps开源工具,你在用哪些?
    误删重要文件怎么办?学会Linux 救援模式再也不担心
  • 原文地址:https://www.cnblogs.com/xzsj/p/13869362.html
Copyright © 2020-2023  润新知