实现一个简易的vue的mvvm(defineProperty)

vue

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

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~~

以上是 实现一个简易的vue的mvvm(defineProperty) 的全部内容, 来源链接: utcz.com/z/379078.html

回到顶部