这是一个最近一年很火的面试题,很多人看到这个题目从下手,其实查阅一些资料后,简单的模拟还是不太难的:

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: ,
}
});
</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 === && reg.test(text)) {
console.log(RegExp.$); // a.a b
let arr = RegExp.$.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 === && reg.test(text)) {
// RegExp $1-$9 表示 最后使用的9个正则
console.log(RegExp.$); // a.a b
let arr = RegExp.$.split('.'); // [a,a] [b]
let val = vm;
arr.forEach( function(k) {
// 循环层级
val = val[k];
})
// 赋值
new Watcher( vm, RegExp.$, 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~~

实现一个简易的vue的mvvm(defineProperty)的更多相关文章

  1. mvvm实现一个简单的vue

    vue,基于mvvm模式下的一个前端框架 mvvm模式下简单的实现数据代理,数据劫持 1.是用Object.defineProperty 实现数据代理 2.使用发布订阅者模式,配合 Object.de ...

  2. 基于 getter 和 setter 撸一个简易的MVVM

    Angular 和 Vue 在对Angular的学习中,了解到AngularJS 的两个主要缺点: 对于每一次界面时间,Ajax 或者 timeout,都会进行一个脏检查,而每一次脏检查又会在内部循环 ...

  3. 实现一个类 Vue 的 MVVM 框架

    Vue 一个 MVVM 框架.一个响应式的组件系统,通过把页面抽象成一个个组件来增加复用性.降低复杂性 主要特色就是数据操纵视图变化,一旦数据变化自动更新所有关联组件~ 所以它的一大特性就是一个数据响 ...

  4. Vue源码分析之实现一个简易版的Vue

    目标 参考 https://cn.vuejs.org/v2/guide/reactivity.html 使用 Typescript 编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定 ...

  5. 实现一个简易vue

    vue主要的功能实现主要分为3部分: 数据劫持/数据代理:数据改变时通知相关函数进行更新操作 数据依赖收集:建立保存dom节点与数据的关联关系 模板与数据之间的绑定:接受到新数据时对dom节点进行更新 ...

  6. 使用MVVM框架avalon.js实现一个简易日历

    最近在做公司内部的运营管理系统,因为与日历密切相关,同时无需触发条件直接显示在页面上,所以针对这样的功能场景,我就用avalon快速实现了一个简易日历,毕竟也是第一次造日历这种轮子,所以这里记录下我当 ...

  7. vue + socket.io实现一个简易聊天室

    vue + vuex + elementUi + socket.io实现一个简易的在线聊天室,提高自己在对vue系列在项目中应用的深度.因为学会一个库或者框架容易,但要结合项目使用一个库或框架就不是那 ...

  8. vue实现一个简易Popover组件

    概述 之前写vue的时候,对于下拉框,我是通过在组件内设置标记来控制是否弹出的,但是这样有一个问题,就是点击组件外部的时候,怎么也控制不了下拉框的关闭,用户体验非常差. 当时想到的解决方法是:给根实例 ...

  9. 使用 js 实现一个简易版的 vue 框架

    使用 js 实现一个简易版的 vue 框架 具有挑战性的前端面试题 refs https://www.infoq.cn/article/0NUjpxGrqRX6Ss01BLLE xgqfrms 201 ...

随机推荐

  1. 关于Java-枚举的总结

    枚举 枚举的定义 枚举也是JDK5.0的新特性. JDK5.0加入了一个全新类型的“类”——枚举类型. 为此引入了一个新的关键字enum. 可以这样来定义一个枚举类型: public enum Col ...

  2. Dnsmasq安装与配置-搭建本地DNS服务器

    默认的情况下,我们平时上网用的本地DNS服务器都是使用电信或者联通的,但是这样也导致了不少的问题,首当其冲的就是上网时经常莫名地弹出广告,或者莫名的流量被消耗掉导致网速变慢.其次是部分网站域名不能正常 ...

  3. [Proposal][app]觅食去

    又要加班,午饭晚饭怎么解决?每天吃食堂换个口味可是不想出门怎么办?顿顿麦当劳,看见汉堡就想吐,下一顿吃什么? 来点个外卖吧! 可是去哪儿点呢—— 百度外卖?美团外卖?饿了么?KFC宅急送?………… 统 ...

  4. Linux 下建立 SSH 隧道做 Socket 代理

    背景 需要解决本地访问内部集群中各台机器上的内部web服务,但是内部集群不能直接访问,只能通过edge node节点跳转. 前提:edge node可以通过ssh方式访问,在edge node上可以访 ...

  5. jQuery---ajax---error函数及其参数详解

    使用jquery的ajax方法向服务器发送请求的时候,常常需要使用到error函数进行错误信息的处理,本文详细说明了ajax中error函数和函数中各个参数的用法. 一般error函数返回的参数有三个 ...

  6. ZKWeb网页框架1.5正式发布

    本次更新的内容有 明显的改进了IoC容器在部分情况下的性能, 当前的性能和Grace, DryIoC同等 添加IHttpResquestHandlerWrapper接口让重载Http上下文更容易 添加 ...

  7. SAS Shortcut Keys

    最常用的键有F3(运行代码).F6(查看log信息).Ctrl + / (注释代码).Ctrl + Shift + / (取消代码注释) SAS系统一共有4类快捷键,其中部分有重复, 第一类可自定义. ...

  8. 201621123018《Java程序设计》第5周学习报告

    1. 本周学习总结 1.1 写出你认为本周学习中比较重要的知识点关键词 接口.interface.implements.Comparable.Comparator. 1.2 尝试使用思维导图将这些关键 ...

  9. MATLAB 到 Python之路1_数据结构和简单操作

    在numpy中,用array来代替matrix,不同于MATLAB中的万物皆matrix,这里的数据首先以array存在,然后通过操作才能和矩阵形式的array运算 1,array的形式 1.1 一维 ...

  10. 利用净现值(NPV)分析对比方案的可行性

    最近在学经济管理方面课程,发现一个挺有意思的例题,mark一下. 题目描述 某投资项目有A.B两个方案,有关数据如下表,基准折现率为10%,请问那个方案较优? 项目 A方案 B方案 投资 15 3 年 ...