目标
- 掌握
Object.defineProperty
的使用 - 掌握js的观察者模式
- 手动实现v-model
1- 前置知识
1. Object.defineProperty
-
作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
Object.defineProperty(对象, 属性, 描述符对象)
-
为对象的属性直接赋值的情况下,对象的属性也可以修改和删除,但是通过
Object.defineProperty()
定义属性,通过描述符的设置可以进行更精准的控制对象属性
<script>
var Person={}
var reactiveName = "zhangsan"
Object.defineProperty(Person, 'name', {
// 可配置
configurable: true,
// 可枚举
enumerable: true,
// 访问器属性`get`
// 读取`Person.name`的时候触发的方法
get () {
console.log('执行get')
return reactiveName
},
// 访问器属性`set`
// 修改`Person.name`的时候触发的方法
set (newValue) {
console.log('执行set')
reactiveName = newValue
}
})
console.log(Person.name); // 'zhangsan'
Person.name = 'lisi';
console.log(Person.name); // 'lisi'
</script>
2. 观察者模式
<script>
// 订阅者
class Dep {
constructor() {
this.watchers = []
}
// 添加观察者
addWatcher (watcher) {
this.watchers.push(watcher)
}
// 通知
notify () {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
// 观察者
class Watcher {
constructor(callback) {
this.callback = callback
}
update () {
console.log('update');
this.callback();
}
}
// 创建订阅者
const dep = new Dep();
// 创建观察者
const watcher1 = new Watcher(() => console.log('watcher1'));
const watcher2 = new Watcher(() => console.log('watcher2'));
// 添加观察者
dep.addWatcher(watcher1)
dep.addWatcher(watcher2)
// 触发通知
dep.notify();
</script>
3. 小结
Object.defineProperty
用来定义对象的属性,可以配置getter和setter访问器- 观察者模式就是有两个订阅者和观察者对象,订阅者可以收集观察者,并且在合适的时机触发观察者执行更新操作
- 实现v-model时,就是在get方法里面收集观察者,set方法里面触发更新操作
2- 实现v-model
1. 采用类似Vue的调用方式
<div id="app">
<input type="text" v-model="msg" />
</div>
<script>
const vm = new MyVue({
el: '#app',
data: {
msg: 'abc'
}
})
</script>
所以我们要创建MyVue对象
<script>
class MyVue {
constructor({el, data}) {
this.container = document.querySelector(el);
this.data = data;
}
}
</script>
2. 初始化data数据
- 使用Object.defineProperty定义data中的每个属性
<script>
class MyVue {
constructor({el, data}) {
this.container = document.querySelector(el);
this.data = data;
// 初始化data
this.initData(this, this.data)
}
initData(vm, data) {
for (let key in data) {
let initVal = data[key];
Object.defineProperty(vm, key, {
get() {
return initVal
},
set(val) {
initVal = val;
}
})
// 判断是否是对象,递归调用initData
if (Object.prototype.toString.call(data[key]) === "[object Object]") {
this.initData(vm[key], data[key])
}
}
}
}
</script>
<script>
const vm = new MyVue({
el: '#app',
data: {
msg: 'abc'
}
});
console.log(vm.msg) // abc
</script>
3. 初始化v-model表单
<script>
class MyVue {
constructor({ el, data }) {
this.container = document.querySelector(el);
this.data = data;
// 初始化data
this.initData(this, this.data)
// 初始化v-model表单
this.initVModel();
}
initVModel () {
// 获取所有v-model元素
const nodes = this.container.querySelectorAll('[v-model]');
nodes.forEach(node => {
const key = node.getAttribute('v-model');
// 初始化赋值
node.value = this[key];
// 监听输入事件
node.addEventListener('input', ev => {
this[key] = ev.target.value;
}, false)
});
}
}
</script>
上面的代码只能拿到data中第一层的数据,如果是这样绑定:
<input type="text" v-model="obj.a" />
那其实获取this['obj.a']
是获取不到数据的,所以还需要重写一个获取data数据的方法:
getData(str) {
const arr = str.split('.'); // ["obj", "a"]
const res = arr.reduce((target, item) => {
// 第一次遍历 target等于data
return target[item] // return之后,target就等于target[item] ===> data.obj
// 第二次遍历 target等于data.obj
// return之后,target就等于data.obj.a
}, this)
return res;
},
initVModel () {
// 获取所有v-model元素
const nodes = this.container.querySelectorAll('[v-model]');
nodes.forEach(node => {
const key = node.getAttribute('v-model');
// 初始化赋值
// node.value = this[key]; // 如果key是"obj.a"这种字符串,获取不到数据
node.value = this.getData(key);
// 监听输入事件
node.addEventListener('input', ev => {
// this[key] = ev.target.value; // 也要处理key是"obj.a"格式的字符串
const arr = key.split("."); // ["obj", "a"]
// 如果arr只有一个元素,直接赋值
if (arr.length === 1) {
this[key] = node.value;
return;
}
// 获取倒数第二级的对象(对于`obj.a`,那就获取this.obj)
const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
// 赋值最后一级(this.obj.a = node.value)
res[arr[arr.length - 1]] = node.value;
}, false)
});
},
此时已经实现了视图变化触发模型的变化,接下来实现模型的变化触发视图的变化
4. 模型的变化触发视图更新
1. 引入观察者模式
// 订阅者
class Dep {
constructor() {
this.watchers = []
}
// 添加观察者
addWatcher (watcher) {
this.watchers.push(watcher)
}
// 通知
notify () {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
// 观察者
class Watcher {
constructor(callback) {
this.callback = callback
}
update () {
console.log('update');
this.callback();
}
}
2. 通知
触发的时机
模型驱动视图更新,就是当修改vm.msg
时(比如vm.msg="123"
),需要触发dom更新。所以我们需要在set方法里面调用dep.notify()
// 初始化data
initData(vm, data) {
for (let key in data) {
let initVal = data[key];
//++++++++++++++++++++++++++++++
const dep = new Dep();
//++++++++++++++++++++++++++++++
Object.defineProperty(vm, key, {
get() {
return initVal
},
set(val) {
initVal = val;
//++++++++++++++++++++++++++++++
// 通知视图更新
dep.notify();
//++++++++++++++++++++++++++++++
}
})
// 判断是否是对象,递归调用initData
if (Object.prototype.toString.call(data[key]) === "[object Object]") {
this.initData(vm[key], data[key])
}
}
}
3. 创建观察者
观察者需要更新dom,所以需要在初始化v-model表单的时候创建观察者
initVModel () {
// 获取所有v-model元素
const nodes = this.container.querySelectorAll('[v-model]');
nodes.forEach(node => {
const key = node.getAttribute('v-model');
// 初始化赋值
// node.value = this[key]; // 如果key是"obj.a"这种字符串,获取不到数据
node.value = this.getData(key);
//++++++++++++++++++++++++++++++
// 创建观察者
const watcher = new Watcher(() => {
node.value = this.getData(key)
})
//++++++++++++++++++++++++++++++
// 监听输入事件
node.addEventListener('input', ev => {
// this[key] = ev.target.value; // 也要处理key是"obj.a"格式的字符串
const arr = key.split("."); // ["obj", "a"]
// 如果arr只有一个元素,直接赋值
if (arr.length === 1) {
this[key] = node.value;
return;
}
// 获取倒数第二级的对象(对于`obj.a`,那就获取this.obj)
const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
// 赋值最后一级(this.obj.a = node.value)
res[arr[arr.length - 1]] = node.value;
}, false)
});
},
4. 添加观察者
当创建了观察者后,应该马上添加到订阅者中,但是订阅者怎么知道已经创建了观察者呢?
- 这时我们在观察者的构造函数里面获取一下data数据,就会执行
Object.defineProperty
定义的的get方法。所以我们在get方法里面添加观察者。
// 创建观察者时传入this和key
const watcher = new Watcher(this, key, () => {
node.value = this.getData(key)
})
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
// 获取data数据,触发get方法
vm.getData(key);
}
update () {
console.log('update');
this.callback();
}
}
// get方法中添加观察者
get() {
dep.addWatcher(watcher)
return initVal
},
5. 将观察者实例赋值给Dep的某个属性
上面dep.addWatcher(watcher)
中的watcher
并不存在,我们需要在观察者的构造函数中先将观察者实例赋值给Dep的某个属性,再获取data数据
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
//将观察者实例赋值给Dep的target属性
Dep.target = this;
// 获取data数据,触发get方法
vm.getData(key);
}
update () {
console.log('update');
this.callback();
}
}
// get方法
get() {
dep.addWatcher(Dep.target)
return initVal
},
6. 避免反复添加观察者
上面已经实现了模型数据驱动视图更新,但是我们发现update方法会执行多次,那是因为dep.addWatcher(Dep.target)
执行了多次,所以dep的watchers数组中有多个观察者,所以update方法会执行多次。解决方法就是获取data数据后重置Dep.target。
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
//将观察者实例赋值给Dep的target属性
Dep.target = this;
// 获取data数据,触发get方法
vm.getData(key);
// 获取data数据后重置Dep.target,避免重复添加观察者
Dep.target = null;
}
update () {
console.log('update');
this.callback();
}
}
// get方法
get() {
Dep.target && dep.addWatcher(Dep.target)
return initVal
},
7. data多次设置为同样的值时不需要触发更新
set(val) {
// 如果前后两次设置的值相等,就不触发更新
if (initVal === val) {
return;
}
initVal = val;
dep.notify(); // 通知视图更新
}
完整代码
// 订阅者
class Dep {
constructor() {
this.watchers = []
}
// 添加观察者
addWatcher(watcher) {
this.watchers.push(watcher)
}
// 通知
notify() {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback
// 第二件事,把当前实例传给get方法
Dep.target = this;
// 第一件事,获取当前的data
const val = vm.getData(key)
// 为了防止重复添加观察者,添加完之后马上清空
Dep.target = null;
}
update() {
console.log('update');
this.callback();
}
}
class MyVue {
constructor({
el,
data
}) {
this.container = document.querySelector(el);
this.data = data;
// 初始化数据
this.initData(this, this.data)
// 初始化v-model表单
this.initVModel()
}
initData(vm, data) {
for (let key in data) {
let initVal = data[key];
const dep = new Dep();
Object.defineProperty(vm, key, {
get() {
// 只要获取data数据,就会进入get方法,然后添加观察者
Dep.target && dep.addWatcher(Dep.target)
return initVal
},
set(val) {
// 如果前后两次设置的值相等,就不触发更新
if (initVal === val) {
return;
}
// console.log(key + '被修改数据')
initVal = val;
dep.notify(); // 通知视图更新
}
})
// 判断是否是对象,递归调用initData
if (Object.prototype.toString.call(data[key]) === "[object Object]") {
this.initData(vm[key], data[key])
}
}
}
initVModel() {
// 获取所有的v-model表单
const nodes = this.container.querySelectorAll('[v-model]')
// console.log(nodes)
nodes.forEach(node => {
// 获取v-model属性的值
const key = node.getAttribute('v-model')
// 把表单创建成观察者
new Watcher(this, key, () => {
node.value = this.getData(key)
})
// console.log(123, key, this[key])
const val = this.getData(key)
// console.log(234234, val)
// 给表单赋值
node.value = val
// 视图驱动数据更新:监听表单事件,修改data。
node.addEventListener('input', () => {
// this[key] = node.value // key === "obj.b"
const arr = key.split("."); // ["obj", "b"]
// 如果arr只有一个元素,直接赋值
if (arr.length === 1) {
this[key] = node.value;
return;
}
// let res = this.getData(key) // this.obj.b ===> 2
const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
// res = node.value;
res[arr[arr.length - 1]] = node.value;
})
})
}
// 传入'a.b.c'这种格式的字符串,获取this.a.b.c的值
getData(str) {
const arr = str.split('.'); // ["obj", "a"]
const res = arr.reduce((target, item) => {
// 第一次遍历 target等于data
return target[item] // return之后,target就等于target[item] ===> data.obj
// 第二次遍历 target等于data.obj
// return之后,target就等于data.obj.a
}, this)
return res;
}
}
总结
Object.defineProperty
- 观察者模式
- v-model手动实现
- 通过
Object.defineProperty
定义data的所有属性,在get方法中收集观察者,在set方法中触发观察者更新DOM
- 通过