在正式实现前,我们先介绍一些铺垫知识。
Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
- obj: 要处理的对象
- prop: 要定义或者修改的属性名
- descriptor: 将被定义或者修改的属性描述符
说说几个重要属性。
(1)configurable
configurable如果在descriptor中不写,就默认为false,定义后不能再次修改该属性,也无法删除该属性。
let obj = {a: 1};
// 我们对obj对象增加一个b属性
obj.b = 2;
console.log(obj); // {a: 1, b: 2}
// 正常情况下我们可以删除obj的属性的
delete obj.b;
console.log(obj); // {a: 1}
// 但是如果我们用Object.defineProperty的configurable属性就不能删除/修改了
Object.defineProperty(obj, 'b', {
value: 2,
configurable: false
})
console.log(obj); //此时obj对象里已经有了b属性, {a: 1, b: 2}
// 但是我们不能修改或者删除属性b
obj.b = 3;
console.log(obj); // {a: 1, b: 2}
// 我们也不能删除属性b
delete obj.b; // false
console.log(obj); // {a: 1, b: 2}
(2)** enumerable**
enumerable设置为false后,便不能遍历该属性。
let obj = {a: 1};
Object.defineProperty(obj, 'b', {
value: 2,
enumerable: false
})
console.log(obj); // {a: 1, b:2};
// 但是遍历的时候只能遍历出属性a
for(let key in obj) {
console.log(key, obj[key])
}
(3)** writable**
定义了writable为false后,该属性不能被改写(注:与上述的configurable有些类似,但是上述的configurable是既不能被修改,也不能被删除)
let obj = {a: 1};
Object.defineProperty(obj, 'b', {
value: 2,
writable: false
})
obj.b = 3;
console.log(obj); // {a: 1, b:2};
注:value和writable称为「数据描述符」
(4)get和set
get和set称为「存取描述符」。
let obj = {a: 1};
let b;
Object.defineProperty(obj, 'b', {
get: function() {
console.log('get b..');
return b;
},
set(val) {
console.log('set b..');
b = val;
}
})
obj.b = 2; // set b..
console.log(obj.b);
// get b..
// 2
get: 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。
注:「存取描述符」和「数据描述符」不能同时使用
let obj = {a: 1};
let b;
Object.defineProperty(obj, 'b', {
value: 3,
get: function() {
console.log('get b..');
return b;
},
set(val) {
console.log('set b..');
b = val;
}
})
上述代码会报错。
上述篇幅简述了「Object.defineProperty」的用法,现在我们用这个方法来做「数据劫持和监听」。
let obj = {
name: 'Jason',
friends: [1, 2, 3, 4]
};
observe(obj);
console.log(obj.name);
obj.name = 'Jack';
obj.friends[0] = 5;
function observe(obj) {
if(JSON.stringify(obj) === '{}' || typeof obj !== 'object') return; // 如果是空对象或者不是对象的数据,则返回空。
// 然后我们遍历对象obj,并把其中的属性都用Object.defineProperty重新定义一遍
for(let key in obj) {
let val = obj[key]; // 注意1:这里不能使用var
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get ${val}`);
return val;
},
set: function(newVal) {
console.log(`changes happen: ${val} => ${newVal}`);
val = newVal
}
});
if(typeof val === 'object') {
observe(val);
}
}
}
![](https://img.haomeiwen.com/i13186984/42ba123e34aec788.png)
上述的observe函数实现了一个数据监听,当监听了某个对象后,我们就可以在用户读取(get)或者设置(set)属性的时候做个拦截。
那为啥「注意1」那里不能用var呢?我们看看用var后输出啥?
![](https://img.haomeiwen.com/i13186984/c35369aa507bdb4f.png)
我们可以看到get获得的值全部为4了。
如果使用var那就也要配合「立即执行函数」。
function observe(obj) {
if(JSON.stringify(obj) === '{}' || typeof obj !== 'object') return; // 如果是空对象或者不是对象的数据,则返回空。
// 然后我们遍历对象obj,并把其中的属性都用Object.defineProperty重新定义一遍
for(let key in obj) {
(function() {
var val = obj[key]; // 注意:这里不能使用var
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get ${val}`);
return val;
},
set: function(newVal) {
console.log(`changes happen: ${val} => ${newVal}`);
val = newVal
}
});
if(typeof val === 'object') {
observe(val);
}
})()
}
}
观察者模式
通俗来说,一个典型的观察者模式应用场景,用户在一个网站订阅主题
- 多个用户(观察者, Observe)都可以订阅该网站的某个主题(Subject)。
- 当该主题内容更新的时候,订阅该主题的用户就都能收到消息。
function Subject() {
this.observes = [];
}
Subject.prototype.addObserve = function(observe) {
let index = this.observes.indexOf(observe);
if(index === -1) {
this.observes.push(observe);
}
}
Subject.prototype.removeObserve = function(observe) {
let index = this.observes.indexOf(observe);
if(index > -1) {
this.observes.slice(index, 1);
}
}
Subject.prototype.notify = function() {
this.observes.forEach((observe) => {
observe.update();
})
};
function Observe(name) {
this.name = name;
this.update = function() {
console.log(`${this.name} update...`);
}
}
// 创建主题
var subject = new Subject();
// 创建观察者1
var observe1 = new Observe('Jason');
// 创建观察者2
var observe2 = new Observe('Jack');
// 主题中添加观察者1
subject.addObserve(observe1);
// 主题中添加观察者2
subject.addObserve(observe2);
// 主题通知所有观察者更新
subject.notify();
![](https://img.haomeiwen.com/i13186984/0ae3f6dcf86ad97d.png)
Subject是构造函数,new Subject() 创建一个主题实例,该对象的内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者(addObserve),删除观察者(removeObserve),通知观察者更新)(notify),当notify后,订阅了该主题的所有观察者都会调用自身的update方法。
Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法。
我们换用ES6重写一遍。
class Subject {
constructor() {
this.observes = [];
}
addObserve(observe) {
let index = this.observes.indexOf(observe);
if(index === -1) {
this.observes.push(observe);
}
}
removeObserve(observe) {
let index = this.observes.indexOf(observe);
if(index > -1) {
this.observes.slice(index, 1);
}
}
notify() {
this.observes.forEach((observe) => {
observe.update();
})
}
}
class Observe {
constructor(name) {
this.name = name;
}
update() {
console.log(`${this.name} update...`);
}
}
// 创建主题
var subject = new Subject();
// 创建观察者1
var observe1 = new Observe('Jason');
// 创建观察者2
var observe2 = new Observe('Jack');
// 主题中添加观察者1
subject.addObserve(observe1);
// 主题中添加观察者2
subject.addObserve(observe2);
// 主题通知所有观察者更新
subject.notify();
上面代码中,主题被观察者订阅的写法是「subject.addObserve(observe)」,不是很直观,我们给观察者添加订阅方法。
class Observe {
constructor(name) {
this.name = name;
}
update() {
console.log(`${this.name} update...`);
}
subscribeTo(subject) {
subject.addObserve(this);
}
}
// 创建主题
var subject = new Subject();
// 创建观察者1
var observe1 = new Observe('Jason');
// 创建观察者2
var observe2 = new Observe('Jack');
// 观察者1订阅主题subject
observe1.subscribeTo(subject);
// 观察者2订阅主题subject
observe2.subscribeTo(subject);
MVVM实现单向绑定
首先说书MVVM(Model-View-ViewModel),是一种用于把数据和UI分离的模式。
MVVM中的Model表示应用程序的数据,比如说一个账户的信息(姓名,年龄,电子邮件等等)。Model保存信息,但通常不处理信息,不会对信息进行再次加工,数据的格式化是由View处理的。行为一般是业务逻辑,封装到ViewModel中。
View是与用户交流的桥梁。
ViewModel充当数据转换器,将Model信息转换为View的信息,将命令从View传到Model层。
假如我们又如下代码,data里面的「name」会和视图中的「{{name}}」一一映射,修改data里面的「name」可以引起视图里面的值的变化。
<body>
<div id="app" >{{name}}</div>
<script>
function mvvm(){
//todo...
}
var vm = new mvvm({
el: '#app',
data: {
name: 'jirengu'
}
})
</script>
<body>
如何实现上述MVVM呢?我们回顾下上文中讲述的观察者模式。
(1)主题(subject)是什么?
(2)观察者(observe)是什么?
(3)观察者何时订阅?
(4)主题何时通知更新?
上面的例子中,主题(subject)是data中的「name」属性,观察者是视图中的「{{name}}」,当一开始执行MVVM初始化(根据el发现「{{name}}」的时候订阅主题),当data中的「name」改变的时候通知观察者更新。
function observe(data) {
if(JSON.stringify(data) === '{}' || typeof data !== 'object') return;
for(let key in data) {
let val = data[key];
let subject = new Subject(); // 每一个key都是一个主题
Object.defineProperty(data, key, {
get: function() {
console.log(`get ${key}`)
if(currentObserver) {
console.log('hasCurrentObserver');
currentObserver.subscribeTo(subject);
}
return val;
},
set: function(newVal) {
val = newVal;
console.log('start notify....');
subject.notify();
}
})
if(typeof val === 'object') {
this.observe(val);
}
}
}
let id = 0;
let currentObserver = null;
class Subject {
constructor() {
this.id = id++;
this.observers = [];
}
addObserver(observer) {
let index = this.observers.indexOf(observer);
if(index === -1) {
this.observers.push(observer);
}
}
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if(index > -1) {
this.observers.slice(index, 1);
}
}
notify() {
this.observers.forEach((observer) => {
observer.update();
})
}
}
class Observer {
constructor(vm, key, cb) {
this.subjects = {}; // 存放该observer订阅的主题
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.getValue();
}
update() {
let oldVal = this.value;
let newVal = this.getValue();
if(oldVal !== newVal) {
// 值有更改,要更新
this.value = newVal;
this.cb.bind(this.vm)(newVal, oldVal);
}
}
subscribeTo(subject) {
if(! this.subjects[subject.id]) {
// 如果我们还未订阅该主题
console.log('SubscribeTo ...', subject);
subject.addObserver(this);
this.subjects[subject.id] = subject;
}
}
getValue() {
currentObserver = this;
let value = this.vm.$data[this.key];
currentObserver = null;
return value;
}
}
class MVVM {
constructor(opts) {
this.init(opts);
observe(this.$data);
// 编译
this.compile();
}
init(opts) {
this.$el = document.querySelector(opts.el);
this.$data = opts.data;
this.observes = [];
}
compile() {
// 转换视图层的节点
this.traverse(this.$el);
}
traverse(node) {
if(node.nodeType === 1) { // 如果为节点,就递归转换
node.childNodes.forEach((childNode) => {
this.traverse(childNode);
})
} else if(node.nodeType === 3){ //如果是文本
this.renderText(node); // 就渲染文字
}
}
renderText(node) {
let reg = /{{(.+?)}}/g;
let match;
while((match = reg.exec(node.nodeValue))) {
let raw = match[0];
let key = match[1].trim();
node.nodeValue = node.nodeValue.replace(raw, this.$data[key]);
// 针对每一个key,创建一个观察者
new Observer(this, key, function(val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val);
})
}
}
}
let vm = new MVVM({
el: '#app',
data: {
name: 'Jason',
age: 23
}
})
![](https://img.haomeiwen.com/i13186984/fc5b74c624aac4e2.png)
当我们在控制台修改「age」为20。视图页面也跟着改变。
![](https://img.haomeiwen.com/i13186984/c195a70b23a8b995.png)
MVVM实现双向绑定
function observe(data) {
if(JSON.stringify(data) === '{}' || typeof data !== 'object') return;
for(let key in data) {
let val = data[key];
let subject = new Subject(); // 每一个key都是一个主题
Object.defineProperty(data, key, {
get: function() {
console.log(`get ${key}`)
if(currentObserver) {
console.log('hasCurrentObserver');
currentObserver.subscribeTo(subject);
}
return val;
},
set: function(newVal) {
val = newVal;
console.log('start notify....');
subject.notify();
}
})
if(typeof val === 'object') {
this.observe(val);
}
}
}
let id = 0;
let currentObserver = null;
class Subject {
constructor() {
this.id = id++;
this.observers = [];
}
addObserver(observer) {
let index = this.observers.indexOf(observer);
if(index === -1) {
this.observers.push(observer);
}
}
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if(index > -1) {
this.observers.slice(index, 1);
}
}
notify() {
this.observers.forEach((observer) => {
observer.update();
})
}
}
class Observer {
constructor(vm, key, cb) {
this.subjects = {}; // 存放该observer订阅的主题
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.getValue();
}
update() {
let oldVal = this.value;
let newVal = this.getValue();
if(oldVal !== newVal) {
// 值有更改,要更新
this.value = newVal;
this.cb.bind(this.vm)(newVal, oldVal);
}
}
subscribeTo(subject) {
if(! this.subjects[subject.id]) {
// 如果我们还未订阅该主题
console.log('SubscribeTo ...', subject);
subject.addObserver(this);
this.subjects[subject.id] = subject;
}
}
getValue() {
currentObserver = this;
let value = this.vm.$data[this.key];
currentObserver = null;
return value;
}
}
class MVVM {
constructor(opts) {
this.init(opts);
observe(this.$data);
// 编译
new Compile(this)
}
init(opts) {
this.$el = document.querySelector(opts.el);
this.$data = opts.data;
this.$methods = opts.methods;
// 把$data中的数据直接代理到当前vm对象
for(let key in this.$data) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: () => {
return this.$data[key]
},
set: (newVal) => {
this.$data[key] = newVal;
}
})
}
// 把「this.$methods」里面的函数中的this,都指向this,也就是vm
for(let key in this.$methods) {
this.$methods[key] = this.$methods[key].bind(this);
}
}
}
class Compile {
constructor(vm) {
this.vm = vm;
this.node = vm.$el;
this.compile();
}
compile(){
this.traverse(this.node)
}
traverse(node){
if(node.nodeType === 1){
this.compileNode(node) //解析节点上的v-bind 属性
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
}else if(node.nodeType === 3){ //处理文本
this.compileText(node)
}
}
compileText(node){
let reg = /{{(.+?)}}/g
let match
console.log(node)
while(match = reg.exec(node.nodeValue)){
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key])
new Observer(this.vm, key, function(val, oldVal){
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
//处理指令
compileNode(node){
let attrs = [...node.attributes] //类数组对象转换成数组,也可用其他方法
attrs.forEach(attr=>{
//attr 是个对象,attr.name 是属性的名字如 v-model, attr.value 是对应的值,如 name
if(this.isModelDirective(attr.name)){
this.bindModelHander(node, attr);
} else if(this.isEventDirective(attr.name)) {
this.bindEventHander(node, attr);
}
})
}
bindModelHander(node, attr) {
let key = attr.value //attr.value === 'name'
node.value = this.vm.$data[key]
new Observer(this.vm, key, function(newVal){
node.value = newVal
})
node.oninput = (e)=>{
this.vm.$data[key] = e.target.value //因为是箭头函数,所以这里的 this 是 compile 对象
}
}
bindEventHander(node, attr) {
console.log(attr)
let eventType = attr.name.substr(5);
let methodName = attr.value;
node.addEventListener(eventType, this.vm.$methods[methodName]);
}
//判断属性名是否是指令
isModelDirective(attrName){
return attrName === 'v-model'
}
isEventDirective(attrName){
return attrName.indexOf('v-on') === 0
}
}
let vm = new MVVM({
el: '#app',
data: {
name: 'Jason',
age: 23
},
methods: {
sayHi(){
alert(`hi ${this.name}` )
}
}
})
注意「编译」步骤,同时在MVVM中我们MVVM类中转移了「this.methods」的this都指向vm。
网友评论