实现一个简易的vue的mvvm(defineProperty)
这是一个最近一年很火的面试题,很多人看到这个题目从下手,其实查阅一些资料后,简单的模拟还是不太难的:
vue不兼容IE8以下是因为他的实现原理使用了 Object.defineProperty 的get和set方法,首先简单介绍以下这个方法
我们看到控制台打印出了这个对象的 key 和 value:
这时候,我们删除这个 name :
let obj = {}; Object.defineProperty( obj, 'name', { value: 'langkui' }) delete obj.name; console.log(obj)
查看控制台,其实并没有删除:
添加 configurable属性:
let obj = {}; Object.defineProperty( obj, 'name', { configurable: true, value: 'langkui' }) delete obj.name; console.log(obj)
我们发现 name 被删除了:
此时,注释掉删除 name 的代码,继续添加修改 name 属性的值
let obj = {}; Object.defineProperty( obj, 'name', { configurable: true, value: 'langkui' }) // delete obj.name; obj.name = 'xiaoming'; console.log(obj)
打开控制台,我们发现 name 的值并没有被修改
我们添加writable: true 的属性:
let obj = {}; Object.defineProperty( obj, 'name', { configurable: true, writable: true, value: 'langkui' }) // delete obj.name; obj.name = 'xiaoming'; console.log(obj)
此时obj.name的值被修改了,
我们试着循环obj:
let obj = {}; Object.defineProperty( obj, 'name', { configurable: true, writable: true, value: 'langkui' }) // delete obj.name; // obj.name = 'xiaoming'; for(let key in obj) { console.log(obj[key]) } console.log(obj)
但是控制台什么也没有输出;
添加 enumerable: true 属性后, 控制台显示执行了循环
let obj = {}; Object.defineProperty( obj, 'name', { configurable: true, writable: true, enumerable: true, value: 'langkui' }) // delete obj.name; // obj.name = 'xiaoming'; for(let key in obj) { console.log(obj[key]) } console.log(obj)
我们还可以给Object.defineProperty 添加 get 和 set 的方法:
let obj = {}; Object.defineProperty( obj, 'name', { configurable: true, // writable: true, enumerable: true, get() { console.log('正在获取name的值') return 'langming' }, set(newVal) { console.log(`正在设置name的值为${newVal}`) } }) // delete obj.name; // obj.name = 'xiaoming'; for(let key in obj) { console.log(obj[key]) } console.log(obj)
然后我们试着在控制台改变 name 的值为100
这些就是Object.defineProperty一些常用设置。
接下来我们用它来实现一个简单的mvvm:
有如下一个简单的看似很像vue的东西:
<!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>Document</title> </head> <body> <div id="app"> {{a}} </div> <script src="1.js"></script> <script> // 数据劫持 Observe let vue = new Vue({ el: 'app', data: { a: 1, } }); </script> </body> </html>
首先我们创建一个Vue的构造函数,并把_data和$options作为他的属性,同时我们希望有个一observe的函数来监听_data的变化,在_data发生变化的时候我们修改Vue构造函数上添加一个对应相同key的属性的值并且同时监听这个新的key的值的变化:
function Vue( options = {} ) { this.$options = options; // this._data; var data = this._data = this.$options.data; // 监听 data 的变化 observe(data); // 实现代理 this.a 代理到 this._data.a for(let name in data) { Object.defineProperty( this, name, { enumerable: true, get() { // this.a 获取的时候返回 this._data.a return this._data[name]; }, set(newVal) { // 设置 this.a 的时候相当于设置 this._data.a this._data[name] = newVal; } }) } } function Observe(data) { for(let key in data) { let val = data[key]; observe(val) Object.defineProperty(data, key, { enumerable: true, get() { return val; }, set(newVal) { if(newVal === val) { return; } // 设置值的时候触发 val = newVal; // 实现赋值后的对象监测功能 observe(newVal); } }) } } // 观察数据,给data中的数据object.defineProperty function observe(data) { if(typeof data !== 'object') { return; } return new Observe(data); }
我们在控制台查看vue 并且 修改 vue.a 的值为100 并再次查看 vue:
接下来我们通过正则匹配页面上的{{}} 并且获取 {{}} 里面的变量 并把 vue上对应的key 替换进去 :
function Vue( options = {} ) { this.$options = options; // this._data; var data = this._data = this.$options.data; // 监听 data 的变化 observe(data); // 实现代理 this.a 代理到 this._data.a for(let name in data) { Object.defineProperty( this, name, { enumerable: true, get() { // this.a 获取的时候返回 this._data.a return this._data[name]; }, set(newVal) { // 设置 this.a 的时候相当于设置 this._data.a this._data[name] = newVal; } }) } // 实现魔板编译 new Compile(this.$options.el, this) } // el:当前Vue实例挂载的元素, vm:当前Vue实例上data,已代理到 this._data function Compile(el, vm) { // $el 表示替换的范围 vm.$el = document.querySelector(el); let fragment = document.createDocumentFragment(); // 将 $el 中的内容移到内存中去 while( child = vm.$el.firstChild ) { fragment.appendChild(child); } replace(fragment); // 替换{{}}中的内容 function replace(fragment) { Array.from(fragment.childNodes).forEach( function (node) { let text = node.textContent; let reg = /\{\{(.*)\}\}/; // 当前节点是文本节点并且通过{{}}的正则匹配 if(node.nodeType === 3 && reg.test(text)) { console.log(RegExp.$1); // a.a b let arr = RegExp.$1.split('.'); // [a,a] [b] let val = vm; arr.forEach( function(k) { // 循环层级 val = val[k]; }) // 赋值 node.textContent = text.replace(reg, val); } vm.$el.appendChild(fragment) // 如果当前节点还有子节点,进行递归操作 if(node.childNodes) { replace(node); } }) } } function Observe(data) { for(let key in data) { let val = data[key]; observe(val) Object.defineProperty(data, key, { enumerable: true, get() { return val; }, set(newVal) { if(newVal === val) { return; } // 设置值的时候触发 val = newVal; // 实现赋值后的对象监测功能 observe(newVal); } }) } } // 观察数据,给data中的数据object.defineProperty function observe(data) { if(typeof data !== 'object') { return; } return new Observe(data); }
这时我们剩下要做的就是在data改变的时候进行一次页面更新, 此时需要提一下订阅发布模式:
订阅模式其实就是就是一个队列,我们把需要执行的函数推进一个数组,在需要用的时候依次去执行这个数组中方法:
// 发布订阅模式 先订阅 再有发布 一个数组的队列 [fn1, fn2, fn3] // 约定绑定的每一个方法,都有一个update属性 function Dep() { this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.notify = function () { this.subs.forEach( sub => sub.update()); } // Watch是一个类,通过这个类创建的实例都有update的方法ßß function Watcher (fn) { this.fn = fn } Watcher.prototype.update = function() { this.fn(); } let watcher = new Watcher( function () { console.log('开始了发布'); }) let dep = new Dep(); dep.addSub(watcher); dep.addSub(watcher); console.log(dep.subs); dep.notify(); // 订阅发布模式其实就是一个数组关系,订阅就是讲函数push到数组队列,发布就是以此的执行这些函数
执行这个文件:
这个就是简单的订阅发布模式,我们把这个应用到们的mvvm中,在数据改变的时候进行实时的更新页面操作:
function Vue( options = {} ) { this.$options = options; // this._data; var data = this._data = this.$options.data; // 监听 data 的变化 observe(data); // 实现代理 this.a 代理到 this._data.a for(let name in data) { Object.defineProperty( this, name, { enumerable: true, get() { // this.a 获取的时候返回 this._data.a return this._data[name]; }, set(newVal) { // 设置 this.a 的时候相当于设置 this._data.a this._data[name] = newVal; } }) } // 实现魔板编译 new Compile(this.$options.el, this) } // el:当前Vue实例挂载的元素, vm:当前Vue实例上data,已代理到 this._data function Compile(el, vm) { // $el 表示替换的范围 vm.$el = document.querySelector(el); let fragment = document.createDocumentFragment(); // 将 $el 中的内容移到内存中去 while( child = vm.$el.firstChild ) { fragment.appendChild(child); } replace(fragment); // 替换{{}}中的内容 function replace(fragment) { Array.from(fragment.childNodes).forEach( function (node) { let text = node.textContent; let reg = /\{\{(.*)\}\}/; // 当前节点是文本节点并且通过{{}}的正则匹配 if(node.nodeType === 3 && reg.test(text)) { // RegExp $1-$9 表示 最后使用的9个正则 console.log(RegExp.$1); // a.a b let arr = RegExp.$1.split('.'); // [a,a] [b] let val = vm; arr.forEach( function(k) { // 循环层级 val = val[k]; }) // 赋值 new Watcher( vm, RegExp.$1, function(newVal) { node.textContent = text.replace(reg, newVal); }) node.textContent = text.replace(reg, val); } vm.$el.appendChild(fragment) // 如果当前节点还有子节点,进行递归操作 if(node.childNodes) { replace(node); } }) } } function Observe(data) { // 开启订阅发布模式 let dep = new Dep(); for(let key in data) { let val = data[key]; observe(val) Object.defineProperty(data, key, { enumerable: true, get() { Dep.target && dep.addSub(Dep.target); return val; }, set(newVal) { if(newVal === val) { return; } // 设置值的时候触发 val = newVal; // 实现赋值后的对象监测功能 observe(newVal); // 让所有的watch的update方法都执行 dep.notify(); } }) } } // 观察数据,给data中的数据object.defineProperty function observe(data) { if(typeof data !== 'object') { return; } return new Observe(data); } // 发布订阅模式 function Dep() { this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.notify = function () { this.subs.forEach( sub => sub.update()); } // watcher function Watcher (vm, exp, fn) { this.vm = vm; this.exp = exp; this.fn = fn // 将watch添加到订阅中 Dep.target = this; let val = vm; let arr = exp.split('.'); arr.forEach(function (k) { // 取值,也就是取 this.a.a/this.b 此时会调用 Object.defineProperty的get的方法 val = val[k]; }); Dep.target = null; } Watcher.prototype.update = function() { let val = this.vm; let arr = this.exp.split('.'); arr.forEach( function (k) { val = val[k]; }) // 需要传入newVal this.fn(val); }
在控制台修改数据页面出现了更新:
一个简单的mvvm就实现了。
源码已经放到了我的github: https://github.com/Jasonwang911/vueMVVM 如果对你有帮助,可以star~~