Based on simple homemade vue framework mvvm

Homemade simple framework mvvm

The principle

  1. With Object.defineProperty () for data agency, adding getter and setter methods. Thus when the data is changed, setter method will be executed
  2. With a fragment () to copy the template, save it to memory.
  3. Node.nodeType fragment obtained by the node type, node on the template and then the instruction parsing, and a method of adding data corresponding to the node, then placed on the page fragment.

Implementation steps

1. To achieve mvvm

  1. Vm create objects of data objects into the Data Agent 1.1 pairs of data parameters passed to save 1.2 pairs of data in the key proxy, so to achieve vm.key to access the data in the data 1.3 calls for data stored in the observe method in the vm All data proxy data, add a description and getter and setter methods 2.getter method is called dep object is dependent on vm manage, setter method calls notify dep method in order to call the update method is called the watcher .



  2. Acting compile data call after the completion of the template compilation (must be carried out after the completion of the proxy data)
class mvvm{
    constructor(options) {
        // 将选项对象保存到vm
        this.$options = options;
        // 将data对象保存到vm和data变量中
        this._data = this.$options.data
        // 实现data对象的数据代理
        this.observe(this._data);
        // 执行编译
        this.$compile = new Compile(options.el, this);
    }
    observe(value) {
        if (!this.isObject(value)) {
            return;
        }
        // 遍历value中所有的属性
        Object.keys(value).forEach(key => {
            // 为vue的data做属性代理
            this.proxyData(key);
            // 对指定属性实现响应式数据绑定
            this.defineReactive(value, key, value[key])
        });
    }
    proxyData(key) {
        Object.defineProperty(this, key, {
            configurable: false, // 不能再重新定义
            enumerable: true, // 可以枚举
            get() {
                // 当通过vm.属性名 读取属性值时自动调用
                return this._data[key];
            },
            set(newVal) {
                // 将最新的值保存到data中对应的属性上(实现代理写操作)
                this._data[key] = newVal;
            },
        });
    }
    defineReactive(obj, key, val) {
        // 创建与当前属性相对应的Dep对象
        let dep = new Dep();
        // 递归查找嵌套属性
        this.observe(val);
        let this_ = this;
        // 为data对象定义属性
        Object.defineProperty(obj, key, {
            configurable: false,
            enumerable: true,
            get() {
                // 建立dep与watcher的关系
                if (Dep.target) {
                    dep.depend();
                }
                console.log(dep.deps)
                return val;
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行监听
                this_.observe(newVal);
                // 通知watcher属性发生改变
                dep.notify()
            }
        })
    }
    isObject(obj) {
        return obj !== null && typeof obj === 'object'
    }
}

2. To achieve comlile

  1. Gets a page node
  2. The child node is copied to the fragment
  3. Nodetype fragment using different types of nodes of the parsing operation categories, call update method parses data replacement method of binding or
    3.1 to parse text node: If there is {{ }}then a direct textContentreplacement for the corresponding data
    3.2 instruction element node is parsed :
    + v-html: using the innerHTMLalternative method of data
    + v-model: first value is assigned, then the addEventListenerbinding method, if artificially modified call the corresponding methods,
    + @click: content obtained from the method vm by addEventListenerand re-directed to a method of binding to this binding method .
    3.3 pairs of text, html, model, etc. after replacing the data or assignment, to add a method to data Watcher subscriber data currently used to build bridges between the watcher and compile, save the key and current data callback method (callback method call the method node updates).
// 扫描模板中所有依赖创建更新函数和watcher
class Compile {
    constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);
        if (this.$el) {
            // 将dom节点转换为Frament提高执行效率
            this.$fragment = this.node2Fragment(this.$el);
            // 编译fragment中所有层次子节点
            this.init()
            // 将生成的结果追加到el中
            this.$el.appendChild(this.$fragment)
        }
    }
    node2Fragment(el) {
        // 创建一个新的Fragment
        let fragment = document.createDocumentFragment();
        let child;
        // 将原生节点拷贝至fragment
        while ((child = el.firstChild)) {
            fragment.appendChild(child);
        }
        return fragment;
    }
    init() {
        // 编译fragment
        this.compileElement(this.$fragment)
    }
    compileElement(el) {
        let childNode = el.childNodes;

        Array.from(childNode).forEach(node => {
            if (this.isElementNode(node)) {
                // 元素节点要识别指令属性
                this.compile(node)
            } else if (this.isTextNode(node) && /\{\{(.*)\}\}/.test(node.textContent)) {
                // 文本节点,只关心{{xx}}格式
                this.compileText(node, RegExp.$1) // RegExp.$1 匹配上面正则分组的内容
            }
            // 遍历可能存在的子节点
            if (node.childNodes && node.childNodes.length) {
                // 递归调用实现所有层次节点的编译
                this.compileElement(node);
            }
        })
    }
    compile(node) {
        // 获取所有标签属性节点
        const nodeAttrs = node.attributes;
        // 遍历所有属性
        Array.from(nodeAttrs).forEach(attr => {
            // attr: v-on:click="onclick" 或 @click="onClick", v-text="test"
            // 属性名: v-on:click, v-text,@click
            const attrName = attr.name
            // 判断是否是指令属性
            if (this.isDirective(attrName)) {
                // 得到表达式(属性值): test,onClick,
                let exp = attr.value;
                // 判断是否是事件指令
                if (this.isEventDirective(attrName)) {
                    // 解析事件指令
                    let dir = attrName.indexOf("@") >= 0 ? attrName.substr(1) : attrName.substr(5);
                    this.eventHandler(node, this.$vm, exp, dir)
                } else {
                    let dir = attrName.substr(2);
                    // 解析普通指令
                    this[dir] && this[dir](node, this.$vm, exp)
                }
                // 移除指令属性
                node.removeAttribute(attrName);
            }
        })
    }
    compileText(node, exp) {
        this.text(node, this.$vm, exp.replace(/^\s+|\s+$/g, ''));
    }
    isElementNode(node) {
        return node.nodeType == 1; // 元素节点
    }
    isTextNode(node) {
        return node.nodeType == 3; // 文本节点
    }
    // 判断是否是指令
    isDirective(attr) {
        return /^v-|^@|^:/.test(attr);
    }
    // 判断是否是事件指令
    isEventDirective(dir) {
        return /^@|^v-on:/.test(dir);
    }
    // 解析v-text/{{}}
    text(node, vm, exp) {
        this.update(node, vm, exp, 'text')
    }
    // 解析v-html
    html(node, vm, exp) {
        this.update(node, vm, exp, 'html')
    }
    // 解析v-class
    class(node, vm, exp) {
        this.update(node, vm, exp, 'class')
    }
    // 解析v-mode
    model(node, vm, exp) {
        this.update(node, vm, exp, 'model')
        let val = this.getVMVal(vm, exp)
        node.addEventListener('input', e => {
            let newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            this.setVmVal(vm, exp, newValue);
            val = newValue;
        })
    }
    // 得到表达式对应的value
    getVMVal(vm, exp) {
        let val = vm._data;
        exp = exp.split('.');
        exp.forEach(k => {
            val = val[k];
        });
        return val;
    }
    setVmVal(vm, exp, value) {
        let val = vm._data;
        exp = exp.split('.');
        exp.forEach((k, i) => {
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value
            }
        })
    }
    // 事件处理
    eventHandler(node, vm, exp, dir) {
        let fn = vm.$options.methods && vm.$options.methods[exp];
        if (dir && fn) {
            // 1.绑定指定事件名和回调函数的DOM事件监听
            // 2.将回调函数中的this强制绑定为vm
            node.addEventListener(dir, fn.bind(vm), false)
        }
    }
    // 更新节点方法
    update(node, vm, exp, dir) {
        let updaterFn = this[dir + "Updater"];
        // 执行节点更新
        updaterFn && updaterFn(node, this.getVMVal(vm, exp));
        // 创建表达式对应的watcher对象
        new Watcher(vm, exp, (val, oldVal) => {
            // 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
            updaterFn && updaterFn(node, val, oldVal);
        })
    }
    textUpdater(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    htmlUpdater(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    }
    modelUpdater(node, value) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
    classUpdater(node, value, oldValue) {
        let className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');
        let space = className && String(value) ? ' ' : '';
        node.className = className + space + value;
    }
}

3. To achieve watcher

Watcher subscribers as a bridge of communication between observe and compile.

  1. Subscribers comlile added time will trigger getter method of proxy data to add a watcher subscribers in the dep, build bridges and observe the watcher.
  2. setter methods in data agency calls will trigger updata method all stored in dep rely Manager watcher, and then call delay the get method trigger for the latest data, trigger callback, call the update node unobtrusively updated page is data.
// Watcher: 订阅者:具体的更新执行者
class Watcher {
    constructor(vm, exp, callack) {
        this.vm = vm;
        this.exp = exp;
        this.cb = callack;
        this.depIds = {};
        this.value = this.get();
    }
    update() {
        // 得到最新的值
        let value = this.get();
        // 得到旧值
        let oldVal = this.value;
        // 如果不相同
        if (value !== oldVal) {
            this.value = value;
            // 调用回调函数更新对应的界面
            this.cb.call(this.vm, value, oldVal);
        }
    }
    addDep(dep) {
        if (!this.depIds.hasOwnProperty(dep.id)) {
            // 建立dep到wacther的关系
            dep.addDep(this)
            // 建立wacther到dep的关系
            this.depIds[dep.id] = dep;
        }
    }
    get() {
        Dep.target = this;
        // 获取当前表达式的值, 内部会导致属性的get()调用
        let value = this.getVMVal();
        Dep.target = null;
        return value;
    }
    getVMVal() {
        let exp = this.exp.split('.');
        let val = this.vm._data;
        exp.forEach(key => {
            val = val[key]
        })
        return val;
    }
}

4. Implement dep

Dep rely Manager: Responsible for the management of all dependent collect vm, including adding and rely notice

var uid = 0;
// 依赖管理器:负责将vm中的所有依赖收集管理,包括依赖添加和通知
class Dep {
    constructor() {
        this.id = uid++;
        this.deps = []; // 存放相关的所有watcher数组
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    depend() {
        Dep.target.addDep(this);
    }
    notify() {
        // 通知所有相关的watcher执行更新
        this.deps.forEach(dep => {
            dep.update();
        })
    }
}

test:

<div id="app"> 
    <p>{{ msg }}</p>
    <p v-html="msg" v-class="red"></p>
    <input type="text" v-model="msg">
    <button @click="click">点击</button>
</div>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
<script type="text/javascript" src="js/compile.js"></script>
<script type="text/javascript" src="js/mvvm.js"></script>
<script type="text/javascript" src="js/Dep.js"></script>
<script type="text/javascript" src="js/watcher.js"></script>
<script>
        var vm = new mvvm({
            el: '#app',
            data: {
                msg: '<span>test</span>',
                red:'red'
            },
            methods: {
                click() {
                    alert('点击')
                }
            }
        })
</script>

Guess you like

Origin www.cnblogs.com/g-h-l/p/11626741.html