Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue(v3.0.11)源码简析之一些内置指令的相关实现 #28

Open
unproductive-wanyicheng opened this issue May 16, 2021 · 0 comments

Comments

@unproductive-wanyicheng
Copy link
Owner

vue中的运行时指令其实都是有一些钩子函数组成的对象,在挂载组件生成组件实例的时候会被加入到dirs数组中,随着vnode对应的元素的生命周期而执行各个阶段的钩子,所以我们看下这些指令对象的内置实现:

// 一些指令的相关实现 我们知道 一些特殊指令会影响vnode的生成格式 这些指令往往在render创建的时候就发挥了作用了 而剩下的呢 则是以钩子函数组成的对象存在 这些指令会成为vnode的dirs数组的一部分 随着vnode和el的生命周期阶段而执行不同的钩子 从而实现指令本身的功能
// v-model有关的指令实现
// 获取一个或者多个v-model对应产生的事件
const getModelAssigner = (vnode) => {
    const fn = vnode.props['onUpdate:modelValue'];
    return isArray(fn) ? value => invokeArrayFns(fn, value) : fn;
};
// 输入汉字这种的组合事件start
function onCompositionStart(e) {
    e.target.composing = true;
}
// 输入汉字这种的组合事件end
function onCompositionEnd(e) {
    const target = e.target;
    if (target.composing) {
        target.composing = false;
        // 最终触发一次就可以了 输入过程中不触发
        trigger$1(target, 'input');
    }
}
// 触发某类事件
function trigger$1(el, type) {
    const e = document.createEvent('HTMLEvents');
    e.initEvent(type, true, true);
    el.dispatchEvent(e);
}
// We are exporting the v-model runtime directly as vnode hooks so that it can
// be tree-shaken in case v-model is never used.
// 使用v-model的时候 看下它是如何影响实际的元素的
const vModelText = {
    // el创建后
    created(el, { modifiers: { lazy, trim, number } }, vnode) {
        // 获取v-model的自定义事件函数引用
        el._assign = getModelAssigner(vnode);
        const castToNumber = number || el.type === 'number';
        // 添加事件
        addEventListener(el, lazy ? 'change' : 'input', e => {
            // 输入汉字过程的组合事件
            if (e.target.composing)
                return;
            let domValue = el.value;
            if (trim) {
                domValue = domValue.trim();
            }
            else if (castToNumber) {
                domValue = toNumber(domValue);
            }
            // 触发用户的回调
            el._assign(domValue);
        });
        // 最终的change一次
        if (trim) {
            addEventListener(el, 'change', () => {
                el.value = el.value.trim();
            });
        }
        // 输入汉语之类的事件 最终输入完成后触发一次input上的回调
        if (!lazy) {
            addEventListener(el, 'compositionstart', onCompositionStart);
            addEventListener(el, 'compositionend', onCompositionEnd);
            // Safari < 10.2 & UIWebView doesn't fire compositionend when
            // switching focus before confirming composition choice
            // this also fixes the issue where some browsers e.g. iOS Chrome
            // fires "change" instead of "input" on autocomplete.
            addEventListener(el, 'change', onCompositionEnd);
        }
    },
    // set value on mounted so it's after min/max for type="range"
    mounted(el, { value }) {
        el.value = value == null ? '' : value;
    },
    // vnode更新 prop属性value发生改变了
    beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode);
        // avoid clearing unresolved text. #2302
        if (el.composing)
            return;
        if (document.activeElement === el) {
            if (trim && el.value.trim() === value) {
                return;
            }
            if ((number || el.type === 'number') && toNumber(el.value) === value) {
                return;
            }
        }
        const newValue = value == null ? '' : value;
        // 更新值
        if (el.value !== newValue) {
            el.value = newValue;
        }
    }
};
// checkbox绑定的其实可以是一个数组或者单个任意对象 数组中的内容是否有选中 主要依靠 looseIndexOf 来判断的
const vModelCheckbox = {
    created(el, _, vnode) {
        // 取出用户事件函数包裹体
        el._assign = getModelAssigner(vnode);
        // 添加事件
        addEventListener(el, 'change', () => {
            // 之前的值
            const modelValue = el._modelValue;
            // 现在el的值
            const elementValue = getValue(el);
            const checked = el.checked;
            const assign = el._assign;
            if (isArray(modelValue)) {
                // 判断当前value在不在之前的值数组中
                const index = looseIndexOf(modelValue, elementValue);
                const found = index !== -1;
                if (checked && !found) {
                    // 更新新的值进入
                    assign(modelValue.concat(elementValue));
                }
                else if (!checked && found) {
                    const filtered = [...modelValue];
                    filtered.splice(index, 1);
                    // 移除后 更新新值 主要是新数组对象
                    assign(filtered);
                }
            }
            // set结构也支持
            else if (isSet(modelValue)) {
                const cloned = new Set(modelValue);
                if (checked) {
                    cloned.add(elementValue);
                }
                else {
                    cloned.delete(elementValue);
                }
                assign(cloned);
            }
            // 单个checkbox
            else {
                assign(getCheckboxValue(el, checked));
            }
        });
    },
    // set initial checked on mount to wait for true-value/false-value
    mounted: setChecked,
    beforeUpdate(el, binding, vnode) {
        el._assign = getModelAssigner(vnode);
        setChecked(el, binding, vnode);
    }
};
// 设置当前值 且设置元素是否选中
function setChecked(el, { value, oldValue }, vnode) {
    el._modelValue = value;
    if (isArray(value)) {
        el.checked = looseIndexOf(value, vnode.props.value) > -1;
    }
    else if (isSet(value)) {
        el.checked = value.has(vnode.props.value);
    }
    else if (value !== oldValue) {
        el.checked = looseEqual(value, getCheckboxValue(el, true));
    }
}
// 单选框比较简单 绑定一个值
const vModelRadio = {
    created(el, { value }, vnode) {
        el.checked = looseEqual(value, vnode.props.value);
        el._assign = getModelAssigner(vnode);
        addEventListener(el, 'change', () => {
            el._assign(getValue(el));
        });
    },
    beforeUpdate(el, { value, oldValue }, vnode) {
        el._assign = getModelAssigner(vnode);
        if (value !== oldValue) {
            el.checked = looseEqual(value, vnode.props.value);
        }
    }
};
// select标签 绑定的就是一个数组
const vModelSelect = {
    created(el, { value, modifiers: { number } }, vnode) {
        const isSetModel = isSet(value);
        // 添加事件
        addEventListener(el, 'change', () => {
            const selectedVal = Array.prototype.filter
                .call(el.options, (o) => o.selected)
                .map((o) => number ? toNumber(getValue(o)) : getValue(o));
            el._assign(el.multiple
                ? isSetModel
                    ? new Set(selectedVal)
                    : selectedVal
                : selectedVal[0]);
        });
        el._assign = getModelAssigner(vnode);
    },
    // set value in mounted & updated because <select> relies on its children
    // <option>s.
    mounted(el, { value }) {
        setSelected(el, value);
    },
    beforeUpdate(el, _binding, vnode) {
        el._assign = getModelAssigner(vnode);
    },
    updated(el, { value }) {
        setSelected(el, value);
    }
};
// 判断value是否选中
function setSelected(el, value) {
    const isMultiple = el.multiple;
    if (isMultiple && !isArray(value) && !isSet(value)) {
        warn(`<select multiple v-model> expects an Array or Set value for its binding, ` +
                `but got ${Object.prototype.toString.call(value).slice(8, -1)}.`);
        return;
    }
    for (let i = 0, l = el.options.length; i < l; i++) {
        const option = el.options[i];
        const optionValue = getValue(option);
        if (isMultiple) {
            if (isArray(value)) {
                option.selected = looseIndexOf(value, optionValue) > -1;
            }
            else {
                option.selected = value.has(optionValue);
            }
        }
        else {
            if (looseEqual(getValue(option), value)) {
                el.selectedIndex = i;
                return;
            }
        }
    }
    if (!isMultiple) {
        el.selectedIndex = -1;
    }
}
// retrieve raw value set via :value bindings
// 取设置prop时候设置的原始值或者普通值 有些属性值会被string话 _value保留着原始对象值
function getValue(el) {
    return '_value' in el ? el._value : el.value;
}
// retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
// checkbox获取用户设置的true false指定取什么值
function getCheckboxValue(el, checked) {
    const key = checked ? '_trueValue' : '_falseValue';
    return key in el ? el[key] : checked;
}
// 动态type v-model指令的实际实现
const vModelDynamic = {
    created(el, binding, vnode) {
        callModelHook(el, binding, vnode, null, 'created');
    },
    mounted(el, binding, vnode) {
        callModelHook(el, binding, vnode, null, 'mounted');
    },
    beforeUpdate(el, binding, vnode, prevVNode) {
        callModelHook(el, binding, vnode, prevVNode, 'beforeUpdate');
    },
    updated(el, binding, vnode, prevVNode) {
        callModelHook(el, binding, vnode, prevVNode, 'updated');
    }
};
// 动态type的v-model绑定 延迟执行 调用实际某种type
function callModelHook(el, binding, vnode, prevVNode, hook) {
    let modelToUse;
    switch (el.tagName) {
        case 'SELECT':
            modelToUse = vModelSelect;
            break;
        case 'TEXTAREA':
            modelToUse = vModelText;
            break;
        default:
            switch (vnode.props && vnode.props.type) {
                case 'checkbox':
                    modelToUse = vModelCheckbox;
                    break;
                case 'radio':
                    modelToUse = vModelRadio;
                    break;
                default:
                    modelToUse = vModelText;
            }
    }
    const fn = modelToUse[hook];
    fn && fn(el, binding, vnode, prevVNode);
}

// 一些事件modify的实际需要添加的执行代码
const systemModifiers = ['ctrl', 'shift', 'alt', 'meta'];
const modifierGuards = {
    stop: e => e.stopPropagation(),
    prevent: e => e.preventDefault(),
    self: e => e.target !== e.currentTarget,
    ctrl: e => !e.ctrlKey,
    shift: e => !e.shiftKey,
    alt: e => !e.altKey,
    meta: e => !e.metaKey,
    left: e => 'button' in e && e.button !== 0,
    middle: e => 'button' in e && e.button !== 1,
    right: e => 'button' in e && e.button !== 2,
    exact: (e, modifiers) => systemModifiers.some(m => e[`${m}Key`] && !modifiers.includes(m))
};
/**
 * @private
 */
// 带有modify的事件执行包裹函数
const withModifiers = (fn, modifiers) => {
    return (event, ...args) => {
        for (let i = 0; i < modifiers.length; i++) {
            const guard = modifierGuards[modifiers[i]];
            // 注意返回结果 对应上文的返回false这里才不会直接return 导致实际的 用户方法不执行了 所以上面的结果是 满足条件时 返回false 不满足 返回 true
            if (guard && guard(event, modifiers))
                return;
        }
        // 最后再执行用户设置的fn
        return fn(event, ...args);
    };
};
// Kept for 2.x compat.
// Note: IE11 compat for `spacebar` and `del` is removed for now.
const keyNames = {
    esc: 'escape',
    space: ' ',
    up: 'arrow-up',
    left: 'arrow-left',
    right: 'arrow-right',
    down: 'arrow-down',
    delete: 'backspace'
};
/**
 * @private
 */
// 带有键盘事件的包裹函数
const withKeys = (fn, modifiers) => {
    return (event) => {
        if (!('key' in event))
            return;
        const eventKey = hyphenate(event.key);
        // 只有带有指定内容的键盘某个键才符合条件
        if (
        // None of the provided key modifiers match the current event key
        !modifiers.some(k => k === eventKey || keyNames[k] === eventKey)) {
            return;
        }
        return fn(event);
    };
};

// v-show的实际内置指令配置
// 几个钩子函数 在vnode对应的阶段去触发执行
const vShow = {
    beforeMount(el, { value }, { transition }) {
        // 保留初始的原始值
        el._vod = el.style.display === 'none' ? '' : el.style.display;
        if (transition && value) {
            transition.beforeEnter(el);
        }
        else {
            setDisplay(el, value);
        }
    },
    mounted(el, { value }, { transition }) {
        if (transition && value) {
            transition.enter(el);
        }
    },
    updated(el, { value, oldValue }, { transition }) {
        if (!value === !oldValue)
            return;
        if (transition) {
            if (value) {
                transition.beforeEnter(el);
                setDisplay(el, true);
                transition.enter(el);
            }
            else {
                transition.leave(el, () => {
                    setDisplay(el, false);
                });
            }
        }
        else {
            setDisplay(el, value);
        }
    },
    beforeUnmount(el, { value }) {
        setDisplay(el, value);
    }
};
// 设置style的display的值
function setDisplay(el, value) {
    // 所谓的true就是恢复原始存在的值 否则就是display:none
    el.style.display = value ? el._vod : 'none';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant