Vue原理:初始化props

350 阅读1分钟

这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战

前文通过normalizeProps函数,已经完成了数组和对象两种形式定义的props统一格式化为对象格式,接下来看一下如何初始化props

在初始化过程中执行initProps函数对props进行初始化,传递当前实例vm和全部props,初始化initProps函数:代码如下,核心部分参考注释

const initProps = (vm, propsOptions) => {
    const propsData = vm.$options.propsData || {};
    const props = (vm._props = {});
    // 将props的key进行缓存
    const keys = (vm.$options._propKeys = []);
    // 是否是根元素
    const isRoot = !vm.$parent;

    // 根实例对象的props应该被转变
    if (!isRoot) {
        toggleObserving(false);
    }

    // 遍历经过 normalizeProps 格式过的对象
    for (const key in propsOptions) {
        // 将key存储
        keys.push(key);
        // 获取props的值
        const value = validateProp(key, propsOptions, propsData, vm);

        // 需要将props进行数据劫持
        defineReactive(props, key, value);

        // 代理props
        if (!(key in vm)) {
            proxy(vm, `_props`, key);
        }
    }

    toggleObserving(true);
};

toggleObserving作用

toggleObserving函数做了什么事情

let shouldObserve = true;
const toggleObserving = value => {
    shouldObserve = value;
}

initProps中调用toggleObserving两次,当是根元素时设置shouldObservefalse,否则为true

想要搞明白toggleObserving的作用,需要看一下在数据劫持入口observe函数的核心流程

const observe = (value) => {
    let ob;
    if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else if (shouldObserve) {
        ob = new Observer(value);
    }
};

可以看到shouldObservetrue时才会进行新的数据劫持流程

proxy作用

initProps过程中,使用proxy对所有的属性进行代理,为什么需要代理每一个prop呢?

若是不使用proxy,那么当我们想要使用props时需要这么使用

const vm = new Vue({
    props: ["fooProps"],
});

// 通过 $options 获取
vm.$options.fooProps
// 通过 _props 获取
vm._props.fooProps

对于使用者而言是十分不便利的,因此需要使用proxy对所有的props进行代理,可以直接通过组件实例获取

vm.fooProps

proxy函数的实现也是非常简单,本质是对所有的属性进行数据劫持,和对于datamethods等处理一致,因为他们也和props存在一样获取方式的问题

const proxy = (target, sourceKey, key) => {
    sharedPropertyDefinition.get = function proxyGetter() {
        return this[sourceKey][key];
    };
    sharedPropertyDefinition.set = function proxySetter(val) {
        this[sourceKey][key] = val;
    };
    
    Object.defineProperty(target, key, sharedPropertyDefinition);
};

validateProp作用

通过validateProp函数可以将传递的属性值取出,然后在defineReactive进行数据劫持时使用,在开始之前,先一起看看几个使用到的工具函数

  • 对象是否拥有该key
const hasOwn = (obj, key) => {
    return Object.prototype.hasOwnProperty.call(obj, key);
};
  • 判断是否为同一数据类型
// 获取数据的类型
const getType = (fn) => {
    const match = fn && fn.toString().match(functionTypeCheckRE);
    return match ? match[1] : "";
};

// 是否为同一数据类型
const isSameType = (a, b) => {
    return getType(a) === getType(b);
};

// 获取目标数据类型的索引:存在返回其索引,不存在则返回-1
const getTypeIndex = (type, expectedTypes) => {
    if (!Array.isArray(expectedTypes)) {
        return isSameType(expectedTypes, type) ? 0 : -1;
    }
    
    for (let i = 0, len = expectedTypes.length; i < len; i++) {
        if (isSameType(expectedTypes[i], type)) {
            return i;
        }
    }
    
    return -1;
};
  • kebab-case 格式转变为camelCasec格式
const hyphenateRE = /\B([A-Z])/g;
export const hyphenate = cached((str) => {
    return str.replace(hyphenateRE, '-$1').toLowerCase()
});

validateProp函数实现,核心逻辑结合注释


const validateProp = (key, propOptions, propsData, vm) => {
    // 属性的配置项
    const prop = propOptions[key]; 
    // 对象是否拥有key
    const absent = !hasOwn(propsData, key);
    let value = propsData[key];
    // 属性的类型是否为 boolean 类型
    const booleanIndex = getTypeIndex(Boolean, prop.type);
  
    // boolean 类型处理
    if (booleanIndex > -1) {
        if (absent && !hasOwn(prop, "default")) {
            // 默认设置为false
            value = false;
        } else if (value === "" || value === hyphenate(key)) {
            // 不是string类型 设置为true
            const stringIndex = getTypeIndex(String, prop.type);
            
            if (stringIndex < 0) {
                value = true;
            }
        }
    }
  
    // 默认值处理
    if (value === undefined) {
        value = getPropDefaultValue(vm, prop, key);
    }

    return value;
};

至此props的整个初始化流程便已完成