Vue源码学习之实现model数据双向绑定和代理数据

Vue源码学习之实现model数据双向绑定和代理数据

  1. 接之前,首先实现model数据的绑定,修改MyVue.js文件,在compileUtile中修改model方法.
/**
 * 
 * @param {*} exp 数据key
 * @param {*} vm vue实例
 * @param {*} inputVal 修改的值
 * @returns 
 */
setValue(exp, vm, inputVal) {
    return exp.split('.').reduce((data, currentVal) => {
        data[currentVal] = inputVal;
    }, vm.$data);
},
model(node, exp, vm) {
    let value = this.getValue(exp, vm);
    new Watcher(vm, exp, (newValue) => {
        this.updater.modelUpdater(node, newValue);
    })
    //-----------修改代码--------------
    // V ==> M 视图修改数据
    node.addEventListener('input', e => {

        // 设置值
        this.setValue(exp, vm, e.target.value);
    })
    //-----------修改代码--------------
    this.updater.modelUpdater(node, value);
},

即初始化时,添加对input事件的监听,在修改输入值时同时修改vue中data对应的值。

  1. 添加数据代理

    修改MyVue.js

    class MyVue {
        // 构造函数
        constructor(options) {
            this.$el = options.el;
            this.$data = options.data;
            this.$options = options;
            if (this.$el) {
                // 实现数据监听类
                new Observer(this.$data);
                // 实现指令解析类
                new Compile(this.$el, this);
                // 将this.$data 代理到 this
                this.proxyData(this.$data);
            }
        }
        proxyData(data) {
            for (let key in data) {
                Object.defineProperty(this, key, {
                    get() {
                        return data[key];
                    },
                    set(val) {
                        data[key] = val;
                    }
                })
    
            }
        }
    }
    

    即通过Object.defineProperty方法将this.$data代理到Vue实例上。

    至此,简单实现了Vue中MVVM模式。

    demo:Demo

    整个过程在我的博客有全部文章。

完整源码:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue学习demo实现</title>
</head>

<body>
    <div id="app">
        <h2>大家好!我是{{person.name}},今年{{person.age}}岁。</h2>
        <h3>我最喜欢的游戏是{{person.fav}}</h3>
        <div v-text="msg">这不是真的msg</div>
        <div v-html="htmlStr"></div>
        <input type="text" v-model="msg">
        <br />
        <button v-on:click="handlerClick">按钮</button>
        <button @click="handlerClick2">按钮2</button>
        <br />
        <a v-bind:href="url" target="_blank">{{url}}</a>
        <a :href="url" target="_blank">百度一下</a>
    </div>

    <!-- <script src="Vue.js"></script> -->
    <script src="MyVue.js"></script>
    <script src="Observer.js"></script>
    <script src="Watcher.js"></script>
    <script>
        vm = new MyVue({
            el: "#app",
            data: {
                person: {
                    name: "小明",
                    age: 19,
                    fav: "LOL"
                },
                msg: "我很喜欢聊天",
                htmlStr: "<h4>还喜欢和大家一起学习。</h4>",
                url: "https://www.yanfee.com"
            },
            methods: {
                handlerClick() {
                    console.log("this is a button!");
                },
                handlerClick2() {
                    this.$data.person.name = "大明"
                    console.log("button2");
                }
            }
        })
    </script>
    <style>
        #app {
            background-color: rgb(209, 255, 136);
        }
    </style>
    </div>
</body>

</html>

MyVue.js

/* 对指令进行编译实现 */
const compileUtil = {
    /**
     *  获取data中的值 如person.name="小明"
     */
    getValue(exp, vm) {
        // exp = person.fav
        return exp.split(".").reduce((data, curValue) => {
            return data[curValue]
        }, vm.$data)
    },
    /**
     * 
     * @param {*} exp 数据key
     * @param {*} vm vue实例
     * @param {*} inputVal 修改的值
     * @returns 
     */
    setValue(exp, vm, inputVal) {
        return exp.split('.').reduce((data, currentVal) => {
            data[currentVal] = inputVal;
        }, vm.$data);
    },
    getContentValue(exp, vm) {
        return exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm)
        })
    },
    /**
     * 
     * @param {*} node 元素节点
     * @param {string} exp 指令表达式 在data中找值需要
     * @param {*} vm Vue实例
     */
    text(node, exp, vm) {
        let value;
        if (exp.indexOf("{{") !== -1) {
            // 对{{person.name}}进行替换
            value = exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
                // 添加watcher
                new Watcher(vm, args[1], (newValue) => {
                    console.log(this.getContentValue(exp, vm));
                    this.updater.textUpdater(node, this.getContentValue(exp, vm));
                })
                // 获取到要替换的表达式 args中第二个元素即为需要的元素
                // console.log(args);
                return this.getValue(args[1], vm)
            })
        } else {
            // 获取指令表达式的值
            value = this.getValue(exp, vm);
            new Watcher(vm, exp, (newValue) => {
                this.updater.textUpdater(node, newValue);
            })
        }
        // 更新
        this.updater.textUpdater(node, value);
    },
    html(node, exp, vm) {
        let value = this.getValue(exp, vm);
        new Watcher(vm, exp, (newValue) => {
            this.updater.htmlUpdater(node, newValue);
        })
        this.updater.htmlUpdater(node, value);
    },
    model(node, exp, vm) {
        let value = this.getValue(exp, vm);
        new Watcher(vm, exp, (newValue) => {
            this.updater.modelUpdater(node, newValue);
        })
        // V ==> M 视图修改数据
        node.addEventListener('input', e => {

            // 设置值
            this.setValue(exp, vm, e.target.value);
        })
        this.updater.modelUpdater(node, value);
    },
    /**
     * 
     * @param {*} node 元素节点
     * @param {*} exp 指令表达式
     * @param {*} vm Vue实例
     * @param {*} eventName 事件名称
     */
    on(node, exp, vm, eventName) {
        let fn = vm.$options.methods && vm.$options.methods[exp];
        node.addEventListener(eventName, fn.bind(vm), false)
    },
    /**
     * 
     * @param {*} node 元素节点
     * @param {*} exp 指令表达式
     * @param {*} vm Vue实例
     * @param {*} attrName 属性名称
     */
    bind(node, exp, vm, attrName) {
        let value = this.getValue(exp, vm);
        new Watcher(vm, exp, (newValue) => {
            this.updater.bindUpdater(node, attrName, newValue);
        })
        this.updater.bindUpdater(node, attrName, value);
    },
    updater: {
        // 更新文本内容
        textUpdater(node, value) {
            node.textContent = value;
        },
        // 插入html碎片
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        // 绑定input的value
        modelUpdater(node, value) {
            node.value = value;
        },
        // 绑定属性
        bindUpdater(node, attrName, attrValue) {
            node.setAttribute(attrName, attrValue)
        }
    }
}
class Compile {
    constructor(el, vm) {
        // 检查el是否为dom节点 如果不是则获取节点
        this.el = this.isElmentNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 获取文档碎片
        const fragment = this.node2Fragment(this.el);
        // 编译文档碎片
        this.compile(fragment);
        // 追加编译好的元素到根节点
        this.el.appendChild(fragment);
    }
    // 实现指令编译方法
    compile(fragment) {
        // 获取子节点
        const childNodes = fragment.childNodes;
        // 识别节点类型并处理
        [...childNodes].forEach(child => {
            if (this.isElmentNode(child)) {
                // 元素节点处理
                this.compileElment(child);
            } else {
                // 文本节点处理
                this.compileText(child)
            }
            if (child.childNodes && child.childNodes.length) {
                this.compile(child)
            }
        })
    }
    // 处理元素节点 
    compileElment(node) {
        const attributes = node.attributes; //获取元素属性
        [...attributes].forEach(attr => {
            const { name, value } = attr; // v-text="msg"
            // console.log(name, value); // v-text msg
            // 是否为指令
            if (this.isDirective(name)) { // 得到指令 v-text v-model... v-on:click v-bind
                const [, directive] = name.split("-"); // 得到指令 text model bind on:click
                // 对on:click 进一步处理
                const [directName, attrName] = directive.split(":")
                // 编译指令工具类
                compileUtil[directName](node, value, this.vm, attrName);
                // 删除带有指令的属性标签
                node.removeAttribute('v-' + directive);
            } else if (this.isEventName(name)) { //绑定事件指令 @click
                // 获取事件类型
                let [, eventName] = name.split("@");
                compileUtil["on"](node, value, this.vm, eventName);
                // 删除带有指令的属性标签
                node.removeAttribute('@' + eventName);
            } else if (this.isBindName(name)) { //绑定属性简写 :src :style...
                // 获取绑定属性的key
                let [, attrName] = name.split(":");
                compileUtil["bind"](node, value, this.vm, attrName);
                // 删除带有指令的属性标签
                node.removeAttribute(':' + attrName);
            }
        })
    }
    /**
     * 编译文本指令 如 {{person.name}}
     */
    compileText(node) {
        const content = node.textContent;
        // 正则匹配 {{***}} 内容
        if (/\{\{(.+?)\}\}/.test(content)) {
            compileUtil["text"](node, content, this.vm)
        }
    }
    node2Fragment(node) {
        const fragment = document.createDocumentFragment();
        while (node.firstChild) {
            fragment.appendChild(node.firstChild);
        }
        return fragment;
    }
    /** 识别是否为属性简写绑定 */
    isBindName(attrName) {
        return attrName.startsWith(":")
    }
    /** 识别是否为事件简写 */
    isEventName(attrName) {
        return attrName.startsWith("@")
    }
    /** 识别是否为指令 */
    isDirective(attrName) {
        return attrName.startsWith("v-")
    }
    /** 识别是否为元素节点 */
    isElmentNode(node) {
        return node.nodeType === 1;
    }
}
class MyVue {
    // 构造函数
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
            // 实现数据监听类
            new Observer(this.$data);
            // 实现指令解析类
            new Compile(this.$el, this);
            // 将this.$data 代理到 this
            this.proxyData(this.$data);
        }
    }
    proxyData(data) {
        for (let key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(val) {
                    data[key] = val;
                }
            })

        }
    }
}

Observer.js

class Observer {
    constructor(data) {
        this.observer(data);
    }
    // 实现数据变化监听
    observer(data) {
        // console.log(data);
        // 判断数据是否为对象并处理
        if (data && typeof data === 'object') {
            // console.log(Object.keys(data));
            // 利用Object.keys方法取出data的key值
            Object.keys(data).forEach(key => {
                // 定义响应方法 即劫持数据的getter和setter方法
                this.defineReactive(data, key, data[key]);
            })
        }
    }
    /**
     * 
     * @param {object} data vue实例中的data数据
     * @param {string} key 遍历到的key值
     * @param {string} value key值对应的value
     */
defineReactive(data, key, value) {
    const self = this;
    // 递归遍历 直到value不再为object
    self.observer(value);
    const dep = new Dep();
    // 劫持并监听所有属性 利用Object.defineProperty重新定义get和set方法
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get() {
            // 如果是初始化Watcher 则添加到dep数组监听
            Dep.target && dep.addSub(Dep.target);
            return value;
        },
        set(newVal) {
            // 注意在修改对象值时,要对新值添加监听
            self.observer(newVal)
            if (value !== newVal) {
                console.log("监听到数据变化:" + key + " 由 " + value + " 变成 " + newVal);
                value = newVal;
            }
            dep.notify();
        }
    })
}
}

Watcher.js

class Watcher {
    /**
     * 
     * @param {*} vm Vue实例
     * @param {*} exp data数据key值
     * @param {*} cb 回调函数 用以修改视图
     */
    constructor(vm, exp, cb) {
        this.vm = vm;
        this.exp = exp;
        this.cb = cb;
        this.preValue = this.getInitValue();
    }
getInitValue() {
    Dep.target = this;
    // 添加Watcher时 获取初始值
    const preValue = compileUtil.getValue(this.exp, this.vm);
    Dep.target = null;
    return preValue;
}
    update() {
        // 更新时检查当前值是否有变化 有变化则更新数值
        const newValue = compileUtil.getValue(this.exp, this.vm);
        if (newValue !== this.oldValue) {
            this.cb(newValue)
        }
    }
}
class Dep {
    constructor() {
        // 初始化数组存放watcher
        this.subs = []
    }
    // 收集watcher
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 通知watcher 更新
    notify() {
        // console.log("watcher:", this.subs);
        this.subs.forEach(e => {
            e.update()
        })
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值