mini mvvm

自用,bug较多,if和for指令不能使用

目前為 2022-05-06 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/444466/1047660/mini%20mvvm.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        mini mvvm 
// @namespace   Violentmonkey Scripts
// @match       *://*/*
// @grant       none
// @version     0.0.2
// @author      -
// @description 自用,bug较多,if和for指令不能使用
// ==/UserScript==
function* getSequence() {
    let i = 0;
    while (true) {
        yield (i = i + 1);
    }
}
let sequence = getSequence();
function getId(name) {
    return `${name ? name : 'none'}.${new Date().getTime()}.${Math.floor(Math.random() * 10000)}.${sequence.next().value}`;
}
class Dep {
    constructor(name) {
        this.subs = [];
        this.id = getId(name);
    }
    delete() {
        if (this.subs.length < 1)
            return;
        this.notify();
        this.subs.forEach((sub) => {
            sub.removeDep(this);
        });
        this.subs.length = 0;
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    depend() {
        Dep.target.addDep(this);
    }
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
}
Dep.target = null;

const extend = (a, b) => {
    for (const key in b) {
        a[key] = b[key];
    }
    return a;
};
const isFunction = (val) => typeof val === 'function';
const NOOP = () => { };
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
const isPlainObject = (val) => toTypeString(val) === '[object Object]';
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
function toArray(nodes) {
    return [].slice.call(nodes);
}
const unique = (arr) => Array.from(new Set(arr));

function observe(value, vm) {
    if (!value || typeof value !== 'object')
        return value;
    if(value.__ob__)
        return value
    return new Observer(value, vm).proxy;
}
class Observer {
    constructor(data, vm) {
        Object.keys(data).forEach((key) => {
            data[key] = observe(data[key], vm);
        });
        this.dep = new Dep('data');
        Object.defineProperty(data, '__ob__', {
            configurable: false,
            enumerable: false,
            value:this
        })
        this.proxy = new Proxy(data, {
            get: (target, key, receiver) => {
                const result = Reflect.get(target, key, receiver)
                if (Dep.target){
                  if(isPlainObject(result) && result.__ob__)
                    result.__ob__.dep.depend()
                  else
                    this.dep.depend()
                }
                return result;
            },
            set: (target, key, newValue, receiver) => {
                const result = Reflect.set(target, key, observe(newValue), receiver);
                this.dep.notify();
                return result;
            },
            deleteProperty: (target, key) => {
                const childObj = target[key];
                let result = false;
                if (isPlainObject(childObj) && hasOwn(childObj, '__ob__')) {
                    let ob = childObj['__ob__'];
                    ob.dep.delete();
                    ob = null;
                    result = Reflect.deleteProperty(target, key);
                    this.dep.notify();
                }
                return result;
            },
        });
    }
}

class EventEmitter {
    constructor(scope) {
        this._events = new Map();
        if (scope)
            this._scope = scope;
    }
    has(eventName){
      return this._events.has(eventName)
    }
    on(eventName, callback) {
        if (!this._events.has(eventName))
            this._events.set(eventName, []);
        this._events.get(eventName).push(callback);
    }
    emit(eventName, value) {
        if (!this._events.has(eventName))
            return;
        this._events.get(eventName).forEach((callback) => {
            if (isFunction(callback))
                    callback(value);
        });
    }
    off(eventName, callback) {
        if (callback) {
            this._events.set(eventName, this._events.get(eventName).filter((cb) => {
                if (cb === callback || cb.originFunction === callback)
                    return false;
            }));
        }
        else {
            this._events.delete(eventName);
        }
    }
    once(eventName, callback) {
        const self = this;
        const onceCallback = function () {
            self.off(eventName, onceCallback);
            callback.apply(self, arguments);
        };
        onceCallback.originFunction = callback;
        this.on(eventName, onceCallback);
    }
}

var EventLoop;
(function (EventLoop) {
    const callbacks = [];
    const p = Promise.resolve();
    let pending = false;
    let useMacroTask = false;
    function flushCallbacks() {
        pending = false;
        const copies = callbacks.slice(0);
        callbacks.length = 0;
        copies.forEach((fn) => fn());
    }
    EventLoop.flushCallbacks = flushCallbacks;
    const macroTimerFunction = () => {
        setTimeout(flushCallbacks, 0);
    };
    const microTimerFunction = () => {
        p.then(flushCallbacks);
    };
    function withMacroTask(fn) {
        return (fn._withTask ||
            (fn._withTask = function () {
                useMacroTask = true;
                const res = fn.apply(null, arguments);
                useMacroTask = false;
                return res;
            }));
    }
    EventLoop.withMacroTask = withMacroTask;
    function nextTick(context, callback) {
        let _resolve;
        callbacks.push(() => {
            if (callback)
                callback.call(context);
            else if (_resolve)
                _resolve(context);
        });
        if (!pending) {
            pending = true;
            if (useMacroTask)
                macroTimerFunction();
            else
                microTimerFunction();
        }
        if (!callback)
            return new Promise((resolve) => {
                _resolve = resolve;
            });
    }
    EventLoop.nextTick = nextTick;
})(EventLoop || (EventLoop = {}));

function getApplyFunction(fn, scope) {
    const func = function () {
        fn.apply(scope, arguments);
    };
    return func;
}
const createVM = (options = {}) => new MVVM(extend(options, {
    element: options.element ? options.element : document.body
}));
class MVVM {
    constructor(options = {}) {
        this.$event = new EventEmitter(this);
        this.$children = {};
        this.$refs = {};
        this.$on = getApplyFunction(this.$event.on, this.$event);
        this.$emit = getApplyFunction(this.$event.emit, this.$event);
        this.$off = getApplyFunction(this.$event.off, this.$event);
        this.$once = getApplyFunction(this.$event.once, this.$event);
        this.$options = options;
        this.components = options.components;
        MVVM.cid += 1;
        this.cid = MVVM.cid;
        this._init();
        if (this.$options.element)
            this.compile(this.$options.element);
    }
    $watch(key, cb) {
        new Watcher(this, key, cb);
    }
    $nextTick(callback) {
        if (callback)
            return EventLoop.nextTick(this, callback);
        return EventLoop.nextTick(this);
    }
    use(fn) {
        fn.call(this, this);
        return this;
    }
    compile(element) {
        this.$compile = new Compile(element, this);
        this.$emit('mounted');
    }
    _init() {
        this._initMethods();
        this._initLifecycle();
        this.$emit('beforeCreate');
        this._initData();
        this._initComputed();
        this._initWatch();
        this.$emit('created');
    }
    _initMethods() {
        let methods = this.$options.methods;
        if (typeof methods !== 'object')
            return;
        Object.keys(methods).forEach((key) => {
            let object = methods[key];
            if (!isFunction(object))
                return;
            if (this[key])
                return;
            this[key] = object;
        });
    }
    _initLifecycle() {
        this.$options.beforeCreate &&
            this.$on('beforeCreate', this.$options.beforeCreate.bind(this));
        this.$options.created && this.$on('created', this.$options.created.bind(this));
        this.$options.beforeMount &&
            this.$on('beforeMount', this.$options.beforeMount);
        this.$options.mounted && this.$on('mounted', this.$options.mounted.bind(this));
        this.$options.beforeUpdate &&
            this.$on('beforeUpdate', this.$options.beforeUpdate);
        this.$options.updated && this.$on('updated', this.$options.updated.bind(this));
    }
    _initData() {
        const data = this.$options.data;
        this.$data = isFunction(data) ? data.call(this) : data;
        Object.keys(this.$data).forEach((key) => Object.defineProperty(this, key, {
            configurable: false,
            enumerable: true,
            get: () => {
                return this.$data[key];
            },
            set: (newVal) => {
                this.$data[key] = newVal;
            },
        }));
        this.$data = observe(this.$data, this);
    }
    _initComputed() {
        let computed = this.$options.computed;
        if (!isPlainObject(computed))
            return;
        Object.keys(computed).forEach((key) => {
            let object = computed[key];
            Object.defineProperty(this, key, {
                get: isFunction(object)
                    ? object
                    : 'get' in object
                        ? object.get
                        : NOOP,
                set: isFunction(object)
                    ? object
                    : 'set' in object
                        ? object.set
                        : NOOP,
            });
        });
    }
    _initWatch() {
        let watch = this.$options.watch;
        if (typeof watch !== 'object')
            return;
        Object.keys(watch).forEach((key) => {
            let object = watch[key];
            if (!isFunction(object))
                return;
            this.$watch(key, object);
        });
    }
}
MVVM.cid = 0;
function getVMVal(vm, exp) {
    let temp;
    exp.split('.').forEach((k, i) => {
        if (i === 0)
            temp = vm[k];
        else
            temp = temp[k];
    });
    return temp;
}
function setVMVal(vm, exp, value) {
    let temp;
    let exps = exp.split('.');
    if (exps.length === 1)
        vm[exps[0]] = value;
    else
        exps.forEach((k, i, exps) => {
            if (i === 0)
                temp = vm[k];
            else if (i < exps.length - 1)
                temp = temp[k];
            else if (i === exps.length - 1)
                temp[k] = value;
        });
}

function parseGetter(exp) {
    return (vm) => getVMVal(vm, exp);
}
class Watcher {
    constructor(vm, expOrFn, callback,deep = false) {
        this.callback = callback;
        this.vm = vm;
        this.deep = deep
        this._depIds = {};
        if (isFunction(expOrFn))
            this._getter = expOrFn;
        else
            this._getter = parseGetter(expOrFn.trim());
        this.value = this.get();
    }
    update() {
        let newVal = this.get();
        let oldVal = this.value;
        if (newVal === oldVal && !(this.deep && isPlainObject(newVal) && isPlainObject(oldVal))) return
        this.value = newVal;
        this.callback.call(this.vm, newVal, oldVal);
    }
    removeDep(dep) {
        delete this._depIds[dep.id];
    }
    addDep(dep) {
        if (!hasOwn(this._depIds, dep.id)) {
            dep.addSub(this);
            this._depIds[dep.id] = dep;
        }
    }
    get() {
        Dep.target = this;
        let value = this._getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }
}

class ElementUtility {
    static fragment(el) {
        let fragment = document.createDocumentFragment(), child;
        while ((child = el.firstChild))
            fragment.appendChild(child);
        return fragment;
    }
    static parseHTML(html) {
        const domParser = new DOMParser();
        let temp = domParser.parseFromString(html, 'text/html');
        return temp.body.children;
    }
    static isElementNode(node) {
        if (node instanceof Element)
            return node.nodeType == 1;
        return false;
    }
    static isTextNode(node) {
        if (node instanceof Text)
            return node.nodeType == 3;
        return false;
    }
    static text(node, value) {
        if (typeof value === 'number')
            value = String(value);
        node.textContent = value ? value : '';
    }
    static html(node, value) {
        if (typeof value === 'number')
            value = String(value);
        node.innerHTML = value ? value : '';
    }
    static class(node, value, oldValue) {
        let className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');
        let space = className && String(value) ? ' ' : '';
        node.className = className + space + value;
    }
    static model(node, newValue) {
        if (typeof newValue === 'number')
            newValue = String(newValue);
        node.value = newValue ? newValue : '';
    }
    static style(node, newValue, oldValue) {
        if (!oldValue)
            oldValue = {};
        if (!newValue)
            newValue = {};
        const keys = Object.keys(oldValue).concat(Object.keys(newValue));
        unique(keys).forEach((key) => {
            if (hasOwn(oldValue, key) && hasOwn(newValue, key)) {
                if (oldValue[key] != newValue[key])
                    node.style.setProperty(key, newValue[key]);
            }
            else if (hasOwn(newValue, key))
                node.style.setProperty(key, newValue[key]);
            else
                node.style.removeProperty(key);
        });
    }
    static display(node, newValue, oldValue) {
        let func = (val) => {
            return {
                display: val ? 'block' : 'none',
            };
        };
        ElementUtility.style(node, func(newValue), null);
    }
}

class MVVMComponent extends MVVM {
    constructor(options) {
        super(options);
        this.$template = options.template || '';
        if (options.parent)
            this.$parent = options.parent;
    }
    $mount(element) {
        this.compile(element);
    }
}
const mountComponent = (element,component) => {
    const vm = new MVVMComponent(component)
    vm.$mount(element)
    return vm
}

const parseAnyDirectiveFunction = (parseString) => {
    return (dir) => dir.indexOf(parseString) == 0;
};
const isDirective = parseAnyDirectiveFunction('v-');
const isEventDirective = parseAnyDirectiveFunction('on');
const isTextDirective = parseAnyDirectiveFunction('text');
const isHtmlDirective = parseAnyDirectiveFunction('html');
const isModelDirective = parseAnyDirectiveFunction('model');
const isClassDirective = parseAnyDirectiveFunction('class');
const isStyleDirective = parseAnyDirectiveFunction('style');
const isShowDirective = parseAnyDirectiveFunction('show');
const isRefDirective = parseAnyDirectiveFunction('ref');
const isForDirective = parseAnyDirectiveFunction('for');

function bindWatcher(node, vm, exp, updater) {
    let __for__ = node['__for__'];
    let val;
    if (__for__ && __for__[exp]) 
        val = __for__[exp]
    else
        val = getVMVal(vm, exp);
    updater && updater(node, val);
    new Watcher(vm, exp, (newValue, oldValue) => {
        if (newValue === oldValue)
            return;
        updater && updater(node, newValue, oldValue);
    });
}
function eventHandler(node, vm, exp, eventType) {
    let fn = vm.$options.methods && vm.$options.methods[exp];
    if (eventType && fn) {
        if(node instanceof MVVMComponent)
          node.$on(eventType,fn.bind(vm))
        else
          node.addEventListener(eventType, fn.bind(vm), false);
    }
}
function vFor(node, vm, exp, c) {
    let reg = /\((.*)\)/;
    let item, index, list;
    if (reg.test(exp)) {
        const arr = RegExp.$1.trim().split(',');
        item = arr[0];
        index = arr[1];
        let rightString = RegExp.rightContext.trim();
        let rarr = rightString.split(' ');
        list = rarr[1];
        if (rarr[0] !== 'in')
            return;
        let val = getVMVal(vm, list);
        let children = [];
        toArray(node.children).forEach((element) => {
            children.push(element.cloneNode(true));
            node.removeChild(element);
        });
        for (let i = 0; i < val.length; i++) {
            children.forEach((element) => {
                let newNode = element.cloneNode(true);
                newNode.__for__ = {
                    [item]: val[i],
                    [index]: i
                };
                node.appendChild(newNode);
                c.compileElement(node);
            });
        }
    }
}
function forHandler(node, vm, exp, c) {
    vFor(node, vm, exp, c);
    new Watcher(vm, exp, (newValue, oldValue) => {
        if (newValue === oldValue)
            return;
        vFor(node, vm, exp, c);
    });
}
class Compile {
    constructor(el, vm) {
        this.slotCallback = [];
        this.$vm = vm;
        this.$el = ElementUtility.isElementNode(el)
            ? el
            : document.querySelector(el);
        this._init();
    }
    _init() {
        if (this.$vm instanceof MVVMComponent) {
            this.$slot = ElementUtility.fragment(this.$el);
            this.$fragment = this.parseComponentTemplate(this.$vm.$template);
            this.$vm.$el = this.$el;
            this.$vm.$emit('beforeMount');
            this.compileElement(this.$fragment);
            this.$el.parentNode.replaceChild(this.$fragment, this.$el);
        }
        else {
            this.$fragment = ElementUtility.fragment(this.$el);
            this.$vm.$el = this.$el;
            this.$vm.$emit('beforeMount');
            this.compileElement(this.$fragment);
            this.$el.appendChild(this.$fragment);
        }
        Object.entries(this.$vm.$children).forEach(([key, child]) => {
            const slotCallback = child.$compile.slotCallback;
            if (slotCallback.length < 1)
                return;
            slotCallback.forEach((fn) => {
                fn(this);
            });
        });
    }
    isSlot(node) {
        if (node.tagName === 'SLOT')
            return true;
        return false;
    }
    compileSlotElement(slot) {
        if (!(this.$vm instanceof MVVMComponent))
            return;
        if (this.$slot.children.length === 0) {
            slot.parentNode.removeChild(slot);
            return;
        }
        this.slotCallback.push(c => {
            c.compileElement(this.$slot);
            slot.parentNode.replaceChild(this.$slot, slot);
        });
    }
    parseComponentTemplate(templateHTML) {
        let element = ElementUtility.parseHTML(templateHTML);
        const template = document.createElement('template');
        if (element.length) {
            if (element.length === 1) {
                if (element[0].tagName.toLowerCase() !== 'template')
                    template.appendChild(element[0]);
            }
            else
                toArray(element).forEach((child) => {
                    template.appendChild(child);
                });
        }
        return ElementUtility.fragment(template);
    }
    parseTemplate(leftString, rightString) {
        return (node, newValue, oldValue) => {
            const str = leftString + newValue + rightString;
            ElementUtility.text(node, str);
        };
    }
    compileElement(el) {
        let childNodes = [];
        // slice
        el.childNodes.forEach(node=>{
          childNodes.push(node)
        })
        childNodes.forEach((node) => {
            if (el['__for__'])
                node['__for__'] = el['__for__'];
            let reg = /\{\{(.*)\}\}/;
            if (ElementUtility.isElementNode(node)) {
                if (this.isComponent(node))
                  this.compileComponent(node.tagName.toLowerCase(), node);
                else if (this.isSlot(node)) 
                    this.compileSlotElement(node);
                else
                    this.compile(node);
            }
            else if (ElementUtility.isTextNode(node) &&
                reg.test(node.textContent))
                bindWatcher(node, this.$vm, RegExp.$1.trim(), this.parseTemplate(RegExp.leftContext, RegExp.rightContext));
            if (node.childNodes && node.childNodes.length)
                this.compileElement(node);
        });
    }
    compile(node) {
        let nodeAttrs = node.attributes;
        toArray(nodeAttrs).forEach((attr) => {
            let attrName = attr.name;
            if (!isDirective(attrName)){
              if (attrName.startsWith('[') && attrName.endsWith(']')) {
                node.removeAttribute(attrName)
                let realAttrName = attrName.replace('[','')
                realAttrName = realAttrName.replace(']','')
                bindWatcher(node,this.$vm,attr.value,(node,newVal,oldVal)=>{
                  node.setAttribute(realAttrName,newVal)
                })
              }
              return;
            }
            let dir = attrName.substring(2);
            let suffix = dir.split(':')[1];
            let exp = attr.value || suffix;
            if (isEventDirective(dir))
                eventHandler(node, this.$vm, exp, suffix);
            else if (isTextDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.text);
            else if (isHtmlDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.html);
            else if (isClassDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.class);
            else if (isModelDirective(dir)) {
                bindWatcher(node, this.$vm, exp, ElementUtility.model);
                let val = getVMVal(this.$vm, exp);
                node.addEventListener('input', (e) => {
                    let target = e.target;
                    let newValue = target.value;
                    if (val === newValue)
                        return;
                    setVMVal(this.$vm, exp, newValue);
                    val = newValue;
                });
            }
            else if (isStyleDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.style);
            else if (isShowDirective(dir))
                bindWatcher(node, this.$vm, exp, ElementUtility.display);
            else if (isRefDirective(dir))
                this.$vm.$refs[exp] = node;
            else if (isForDirective(dir))
                forHandler(node, this.$vm, exp, this);
            node.removeAttribute(attrName);
        });
    }
    isComponent(node) {
        const tagName = node.tagName.toLowerCase();
        if (!/^[(a-zA-Z)-]*$/.test(tagName))
            return false;
        if (this.$vm.components && hasOwn(this.$vm.components, tagName))
            return true;
        return false;
    }
    compileComponent(componentName, node) {
        const attributes = []
        toArray(node.attributes).forEach((attr) => {
          attributes.push(attr)
        })
        const componentOptions = this.$vm.components[componentName];
        const component = new MVVMComponent(extend(componentOptions, {
            parent: this.$vm
        }));
        component.$mount(node);
        this.$vm.$children[componentName] = component;
        attributes.forEach(attr=>{
          let attrName = attr.name;
          if (!isDirective(attrName))
            return;
          let dir = attrName.substring(2);
          let suffix = dir.split(':')[1];
          let exp = attr.value || suffix;
          if (isEventDirective(dir))
            eventHandler(component, this.$vm, exp, suffix);
          else if (isRefDirective(dir))
              this.$vm.$refs[exp] = component;
        })
    }
}