// 创建一个Mvvm构造函数 // 这里用es6方法将options赋一个初始值,防止没传,等同于options || {} function Mvvm(options = {}) { // vm.$options Vue上是将所有属性挂载到上面 // 所以我们也同样实现,将所有属性挂载到了$options console.log(options) this.$options = options; // this._data 这里也和Vue一样 let data = this._data = this.$options.data; // 数据劫持 observe(data); // this 代理了this._data for(let key in data) { Object.defineProperty(this, key, { configurable: true, get() { return this._data[key]; // 如this.a = {b: 1} }, set(newVal) { this._data[key] = newVal; } }); } // 初始化computed,将this指向实例 initComputed.call(this); // 编译 new Compile(options.el, this); // 所有事情处理好后执行mounted钩子函数 options.mounted.call(this); // 这就实现了mounted钩子函数 } // 创建一个Observe构造函数 // 写数据劫持的主要逻辑 function Observe(data) { let dep = new Dep(); // 所谓数据劫持就是给对象增加get,set // 先遍历一遍对象再说 for(let key in data) { // 把data属性通过defineProperty的方式定义属性 let val = data[key]; observe(val); // 递归继续向下找,实现深度的数据劫持 Object.defineProperty(data, key, { configurable: true, get() { Dep.target && dep.addSub(Dep.target); return val; }, set(newVal) { // 更改值的时候 if(val === newVal) { // 设置的值和以前值一样就不理它 return; } val = newVal; // 如果以后再获取值(get)的时候,将刚才设置的值再返回去 observe(newVal); // 当设置为新值后,也需要把新值再去定义成属性 dep.notify(); // 让所有watcher的update方法执行即可 } }); } } // 外面再写一个函数 // 不用每次调用都写个new // 也方便递归调用 function observe(data) { // 如果不是对象的话就直接return掉 // 防止递归溢出 if(!data || typeof data !== 'object') return; return new Observe(data); } // 创建Compile构造函数 function Compile(el, vm) { // 将el挂载到实例上方便调用 vm.$el = document.querySelector(el); console.log(vm.$el) // 在el范围里将内容都拿到,当然不能一个一个的拿 // 可以选择移到内存中去然后放入文档碎片中,节省开销 let fragment = document.createDocumentFragment(); console.log(fragment) console.log(vm.$el.firstChild) while(child = vm.$el.firstChild) { fragment.appendChild(child); // 此时将el中的内容放入内存中 } console.log(fragment.childNodes) // 对el里面的内容进行替换 function replace(frag) { Array.from(frag.childNodes).forEach(node => { console.log(node, node.textContent) let txt = node.textContent; let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}} if(node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}} // console.log(RegExp.$1); // 匹配到的第一个分组 如: a.b, c // let arr = RegExp.$1.split('.'); // console.log(arr) // let val = vm; // arr.forEach(key => { // val = val[key]; // 如this.a.b // }); // // 用trim方法去除一下首尾空格 // node.textContent = txt.replace(reg, val).trim(); function replaceTxt() { node.textContent = txt.replace(reg, (matched, placeholder) => { console.log(placeholder); // 匹配到的分组 如:song, album.name, singer... new Watcher(vm, placeholder, replaceTxt); // 监听变化,进行匹配替换内容 return placeholder.split('.').reduce((val, key) => { return val[key]; }, vm); }); }; // 替换 replaceTxt(); } if(node.nodeType === 1) { // 元素节点 let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组 console.log(node.attributes) Array.from(nodeAttr).forEach(attr => { let name = attr.name; // v-model type let exp = attr.value; // c text if(name.includes('v-')) { node.value = vm[exp]; // this.c 为 2 } console.log(attr.value) // debugger // 监听变化 new Watcher(vm, exp, function(newVal) { node.value = newVal; // 当watcher触发时会自动将内容放进输入框中 }); node.addEventListener('input', e => { debugger let newVal = e.target.value; // 相当于给this.c赋了一个新值 // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新 vm[exp] = newVal; console.log(vm) }); }); } // 如果还有子节点,继续递归replace if(node.childNodes && node.childNodes.length) { replace(node); } }); // 监听变化 // 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参 new Watcher(vm, RegExp.$1, newVal => { node.textContent = txt.replace(reg, newVal).trim(); }); } replace(fragment); // 替换内容 vm.$el.appendChild(fragment); // 再将文档碎片放入el中 } // 发布订阅模式 订阅和发布 如[fn1, fn2, fn3] function Dep() { // 一个数组(存放函数的事件池) this.subs = []; } Dep.prototype = { addSub(sub) { this.subs.push(sub); }, notify() { // 绑定的方法,都有一个update方法 this.subs.forEach(sub => sub.update()); } }; // 监听函数 // 通过Watcher这个类创建的实例,都拥有update方法 function Watcher(vm, exp, fn) { this.fn = fn; // 将fn放到实例上 this.vm = vm; this.exp = exp; // 添加一个事件 // 这里我们先定义一个属性 Dep.target = this; console.log(exp) let arr = toString(exp).split('.'); let val = vm; arr.forEach(key => { // 取值 val = val[key]; // 获取到this.a.b,默认就会调用get方法 }); Dep.target = null; } Watcher.prototype.update = function() { // notify的时候值已经更改了 // 再通过vm, exp来获取新的值 console.log(this.exp) let arr = toString(this.exp).split('.'); let val = this.vm; arr.forEach(key => { val = val[key]; // 通过get获取到新的值 }); console.log(this) this.fn(val); }; let watcher = new Watcher(() => console.log(111)); // let dep = new Dep(); dep.addSub(watcher); // 将watcher放到数组中,watcher自带update方法, => [watcher] dep.addSub(watcher); dep.notify(); // 111, 111 function initComputed() { let vm = this; let computed = this.$options.computed; // 从options上拿到computed属性 {sum: ƒ, noop: ƒ} // 得到的都是对象的key可以通过Object.keys转化为数组 Object.keys(computed).forEach(key => { // key就是sum,noop Object.defineProperty(vm, key, { // 这里判断是computed里的key是对象还是函数 // 如果是函数直接就会调get方法 // 如果是对象的话,手动调一下get方法即可 // 如: sum() {return this.a + this.b;},他们获取a和b的值就会调用get方法 // 所以不需要new Watcher去监听变化了 get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, set() {} }); }); }
二:
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>双向绑定实现</title> <style> #app { text-align: center; } </style> </head> <body> <div id="app"> <h2>{{title}}</h2> <input v-model="name"> <h1>{{name}}</h1> <button v-on:click="clickMe">点击!</button> </div> </body> <script src="js/observer.js"></script> <script src="js/watcher.js"></script> <script src="js/compile.js"></script> <script src="js/index.js"></script> <script> new Vue({ el: '#app', data: { title: 'vue code', name: '练习' }, methods: { clickMe() { this.title = 'vue code click'; } }, mounted() { window.setTimeout(() => { this.title = '1秒'; }, 1000); } }); </script> </html>
index.js
function Vue(options) { var self = this; this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(function(key) { console.log(key); // title name self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this); options.mounted.call(this); // 所有事情处理好后执行mounted函数 } Vue.prototype = { proxyKeys: function(key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function() { return self.data[key]; }, set: function(newVal) { self.data[key] = newVal; } }); } };
observer.js
function Observer(data) { this.data = data; this.walk(data); } Observer.prototype = { walk: function(data) { var self = this; Object.keys(data).forEach(function(key) { self.defineReactive(data, key, data[key]); }); }, defineReactive: function(data, key, val) { var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function getter() { if (Dep.target) { console.log(Dep.target) dep.addSub(Dep.target); } return val; }, set: function setter(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); } }); } }; function observe(val, vm) { if (!val || typeof val !== 'object') { return; } return new Observer(val); } function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { console.log(sub.update) sub.update(); }); } }; Dep.target = null;
watcher.js
function Watcher(vm, exp, cb) { this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 将自己添加到订阅器的操作 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 缓存自己 var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; } }
compile.js
function Compile(el, vm) { this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } Compile.prototype = { init: function() { if (this.el) { this.fragment = this.nodeToFragment(this.el); this.compileElement(this.fragment); this.el.appendChild(this.fragment); } else { console.log('Dom元素不存在'); } }, nodeToFragment: function(el) { var fragment = document.createDocumentFragment(); var child = el.firstElementChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstElementChild; } return fragment; }, compileElement: function(el) { var self = this; var childNodes = el.childNodes; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; var text = node.textContent; if (self.isElementNode(node)) { self.compile(node); } else if (self.isTextNode(node) && reg.test(text)) { self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }, compile: function(node) { var self = this; var nodeAttrs = node.attributes; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; if (self.isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); // model on:click if (self.isEventDirective(dir)) { // 事件命令 self.compileEvent(node, self.vm, exp, dir); } else { // v-model指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); } }); }, compileText: function(node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); new Watcher(this.vm, exp, function(value) { self.updateText(node, value); }); }, compileEvent: function(node, vm, exp, dir) { var eventType = dir.split(':')[1]; var cb = vm.methods && vm.methods[exp]; if (eventType && cb) { node.addEventListener(eventType, cb.bind(vm), false); } }, compileModel: function(node, vm, exp, dir) { var self = this; var val = this.vm[exp]; // name this.modelUpdater(node, val); new Watcher(this.vm, exp, function(value) { self.modelUpdater(node, value); }); node.addEventListener('input', function(e) { var newVal = e.target.value; if (val === newVal) { return; } self.vm[exp] = newVal; val = newVal; }); }, updateText: function(node, value) { node.textContent = typeof value === 'undefined' ? '' : value; }, modelUpdater: function(node, value, oldVal) { node.value = typeof value === 'undefined' ? '' : value; }, isDirective: function(attr) { return attr.indexOf('v-') === 0; }, isEventDirective: function(dir) { return dir.indexOf('on:') === 0; }, isElementNode: function(node) { return node.nodeType === 1; }, isTextNode: function(node) { return node.nodeType === 3; } };