MVVM 发表于 2019-10-19 | 更新于: 2019-12-10 字数统计: 1.8k 字 | 阅读时长 ≈ 9 分钟 简单的双向绑定,不是MVVM的 12345678910111213141516171819202122232425262728293031<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>如何实现双向绑定 mvvm</title> </head> <body> <p>Hello, <span id="name"></span></p> <input type="text" id="input" /> <script> const obj = { input: "" }; const inputDOM = document.getElementById("input"); const nameDOM = document.getElementById("name"); inputDOM.addEventListener("input", function(e) { obj.input = e.target.value; }); Object.defineProperty(obj, "input", { set: function(newValue) { nameDOM.innerHTML = newValue.trim().toUpperCase(); } }); </script> </body></html> 正宗的MVVM,通过双向绑定来解说 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>如何实现双向绑定 mvvm</title> </head> <body> <div id="app"> <input type="text" v-model="school.name" /> <div>姓名:{{ school.name }}</div> <div>年龄:{{ school.age }}</div> 介绍:{{ getNewName }} <ul> <li>1</li> <li>1</li> </ul> <button v-on:click="change">更新操作</button> 消息: <div v-html="message"></div> </div> <!-- <script src="./node_modules/vue/dist/vue.min.js"></script> --> <script src="./MVVM.js"></script> <script> let vm = new Vue({ el: "#app", data: { school: { name: "芳草", age: 30 }, message: "<h1>欢迎大家</h1>" }, computed: { getNewName() { return this.school.name + "架构"; } }, methods: { change() { this.school.name = "天涯"; } } }); </script> </body></html> 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307//MVVM.js//https://github.com/DMQ/mvvm/** * 观察者(发布订阅) 观察者 被观察者 * * @class Dep */class Dep { constructor() { this.subs = []; //存放所有的watcher } //订阅 addSub(watcher) { //添加watcher this.subs.push(watcher); } //发布 notify() { this.subs.forEach(watcher => { watcher.update(); }); }}/** * 观察者 * * @class Watcher */class Watcher { /** *Creates an instance of Watcher. * @param {*} vm 数据 * @param {*} expr 表达式 * @param {*} cb callback * @memberof Watcher */ constructor(vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; //默认先存放一个老值 this.oldValue = this.get(); } get() { // vm.$data.school vm.$data.school.name Dep.target = this; //先把自己放在this上 //取值 把这个观察者 和数据关联起来 let value = CompileUtil.getVal(this.vm, this.expr); Dep.target = null; //不取消 任何值取值,都会添加watcher return value; } update() { //更新操作 数据变化后 会调用观察者的update方法 let newVal = CompileUtil.getVal(this.vm, this.expr); if (newVal !== this.oldValue) { this.cb(newVal); } }}// vm.$watch(vm, 'school.name', newVal => { })//实现数据挟持过程class Observer { constructor(data) { this.observer(data); } observer(data) { if (data && typeof data === "object") { //如果是对象 for (let key in data) { this.defineReactive(data, key, data[key]); } } } defineReactive(obj, key, value) { this.observer(value); // school : [watcher,warcher] b:[wartcher] let dep = new Dep(); //给每一个属性 都加上一个具有发布订阅的功能 Object.defineProperty(obj, key, { get() { //创建watcher时 回取到对应的内容,并且把wartcher放到了全局上 Dep.target && dep.addSub(Dep.target); return value; }, set: newVal => { if (newVal !== value) { // 新的值是object的话,进行监听 this.observer(newVal); value = newVal; // 通知订阅者 dep.notify(); } } }); }}//基类 调度 解析模板指令class Compiler { constructor(el, vm) { // 判断el属性 是不是一个元素 如果不是元素 那就获取 this.el = this.isElementNode(el) ? el : document.querySelector(el); //把当前节点中的元素 获取到 放到内存中 this.vm = vm; let fragment = this.node2fragment(this.el); //把节点中的内容进行替换 //编译模版 用数据编译 this.compile(fragment); //把内容再塞到页面中 this.el.appendChild(fragment); } isDirective(attrName) { return attrName.startsWith("v-"); } //编译元素 compileElement(node) { let attributes = node.attributes; //类数组 [...attributes].forEach(attr => { let { name, value: expr } = attr; //判断是不是指令 if (this.isDirective(name)) { let [, directive] = name.split("-"); //v-on:click let [directiveName, eventName] = directive.split(":"); CompileUtil[directiveName](node, expr, this.vm, eventName); } }); } //编译文本 compileText(node) { //判断当前文本节点中内容是否包含{{xxx}} let content = node.textContent; if (/\{\{(.+?)\}\}/.test(content)) { // console.log(content); //找到所有文本 //文本节点 CompileUtil["text"](node, content, this.vm); //{{a}} {{b}} } } //核心的编译方法 compile(node) { //用来编译内存中的dom节点 let childNodes = node.childNodes; [...childNodes].forEach(child => { if (this.isElementNode(child)) { this.compileElement(child); //如果是元素的话 需要把自己传进去 再去遍历子节点 this.compile(child); } else { this.compileText(child); } }); } //把节点移动到内存中 node2fragment(node) { //创建一个文档节点 let fragment = document.createDocumentFragment(); let firstChild; // 将原生节点拷贝到fragment while ((firstChild = node.firstChild)) { //appendChild具有移动性 fragment.appendChild(firstChild); } return fragment; } isElementNode(node) { //是不是元素节点 return node.nodeType === 1; }}// 指令处理集合CompileUtil = { //根据表达式来取到对应的数据 getVal(vm, expr) { // let arr = expr.split("."); // if (arr.length === 0) { // return vm.$data[expr]; // } return expr .replace(/^\s+(.*?)\s+$/g, "$1") .split(".") .reduce((data, current) => { return data[current]; }, vm.$data); }, setValue(vm, expr, value) { expr.replace(/^\s+(.*?)\s+$/g, "$1") .split(".") .reduce((data, current, index, arr) => { if (index == arr.length - 1) { data[current] = value; } return data[current]; }, vm.$data); }, //解析v-model这个指令 model(node, expr, vm) { //给输入框赋予value属性 node.value=xxx let fn = this.updater["modelUpdater"]; new Watcher(vm, expr, newVal => { //给输入框加一个观察者 数据更新后会出发此方法,会拿新值 给输入框赋值 fn(node, newVal); }); node.addEventListener("input", e => { let value = e.target.value; this.setValue(vm, expr, value); }); let value = this.getVal(vm, expr); fn(node, value); }, html(node, expr, vm) { //v-html="message" let fn = this.updater["htmlUpdater"]; new Watcher(vm, expr, newVal => { //给输入框加一个观察者 数据更新后会出发此方法,会拿新值 给输入框赋值 fn(node, newVal); }); let value = this.getVal(vm, expr); fn(node, value); }, getContentValue(vm, expr) { //遍历表达式 将内容 重新替换成一个完整的内容 返还回去 return expr.replace(/\{\{(.+?)\}\}/g, (...args) => { return this.getVal(vm, args[1]); }); }, on(node, expr, vm, eventName) { //expr: v-on:click="change" node.addEventListener(eventName, e => { vm[expr].call(vm, e); }); }, text(node, expr, vm) { let fn = this.updater["textUpdater"]; let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { //给表达式每个人 {{}} 都加上观察者 new Watcher(vm, args[1], newVal => { fn(node, this.getContentValue(vm, expr)); //返回了一个全的字符串 }); return this.getVal(vm, args[1]); }); fn(node, content); }, updater: { //把数据插入到节点中 modelUpdater(node, value) { // node.value = value; node.value = typeof value == "undefined" ? "" : value; }, htmlUpdater(node, value) { //xss攻击 // node.innerHTML = value; node.innerHTML = typeof value == "undefined" ? "" : value; }, textUpdater(node, value) { // node.textContent = value; node.textContent = typeof value == "undefined" ? "" : value; } }};class Vue { constructor(options) { this.$el = options.el; this.$data = options.data; let computed = options.computed; let methods = options.methods; //这个根元素 存在 编译模版 if (this.$el) { //把数据 全部转化成用Object.defineProperty来定义 new Observer(this.$data); for (let key in computed) { Object.defineProperty(this.$data, key, { get: () => { return computed[key].call(this); } }); } for (let key in methods) { Object.defineProperty(this, key, { get() { return methods[key]; } }); } //把数据获取操作 vm上的取值操作 都代理套vm.$data this.proxyVm(this.$data); new Compiler(this.$el, this); } } proxyVm(data) { for (let key in data) { Object.defineProperty(this, key, { get() { return data[key]; } }); } }}