vue的双向绑定仿写–简单实现
代码会放到最后--------------------------------------------
了解过vue的都知道老版vue使用的是defineproperty进行双向绑定,但是应对面试最好自己实现一下,了解一下实现也是面试的加分项,下面我来讲解一下我写的主要的原理以及实现过程----
主要原理就是发布订阅机制-----------(不了解的可以去学习一下,下期可能会介绍一下,其实发布订阅就是dom2)
1.new vue({data对象},对应的dom元素,完成双向绑定data对象中的一个内容)
首先 了解过vue的都知道使用 vue 肯定有一个vue实例,new vue()。所以我进行仿写的时候也先创建vue实例。
observer()
new watcher() 先不用管
```javascript
class Vue{
constructor(data,vm,exp){//data就是数值的存放
this.data=data
//Object.keys把键值名转化为数组,然后通过foreach完成set ,get的绑定,也就是 es5访问器属性
Object.keys(this.data).forEach((value)=>{ //把对象的值转为数组然后执行Proxyproperty()
this.Proxyproperty(value)
})
observer(data)
vm.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
vm.innerHTML = value;
});
return this;
}
//defineproperty 使用set get 完成绑定
Proxyproperty(key){
Object.defineProperty(this,key,{
enumerable:false,
configurable:true,
get(){
return this.data[key]
}
,set(newVal){
this.data[key]=newVal
}
})
}
}
2.observer()方法 这里的原理就是发布订阅机制
把需要存放的值放到一个Dep()对象中。var dep=new Dep(),该对象里面
有对应的subs数组this.subs=[] ,dep对象中有方法add为了添加数组,notify()检测里面所有元素,然后变成数组里面对应的数据,在这里可能就会有不懂的疑问了,为啥notify里面有一个update方法,可是在原型里面也没有啊,为啥在watcher对象里面有,执行的是watcher对象的数组,因为通过watcher完成的数据的绑定,如果watcher里面不监听任何的东西,observer里面就不会有任何的东西存在,所以sub可以对应执行update方法,如果还不懂,到watch里面在进行讲解(仔细)
function Observer(data){
this.data =data;
this.walk(data)
}
Observer.prototype={
defineReactive(data,key,val){ //data就是vue的对象 key就是键名 val就是键名对应的键值
var dep=new Dep()
var childObj = observer(val); //深度调用,检测是否是对象
Object.defineProperty(data,key,{
configurable:true,
enumerable:false,
get(){
if (Dep.target) {
dep.add(Dep.target); //如果增值不为空,就是放到sub数组中
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
})
}
,walk(data){
let _this=this
Object.keys(data).forEach(function(key) {
_this.defineReactive(data, key, data[key]);//监听this的每个变化
//这里的this指向的是window ,因为闭包。所以使用_this
});
}
}
function observer(value) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
//---发布订阅机制
class Dep{
constructor(){
this.subs=[]
}
add(value){ //增加值
this.subs.push(value)
}
notify(){ //更新
this.subs.forEach(function(sub){
sub.update()
})
}
}
Dep.target=null
如果认真看了上面代码的内容的话,就会发现observer尽管data中有内容,但是get方法是不会把对应的值 存入数组的,因Dep.target=null,所以没啥用,所以就一定需要一部可以把Dep.target=null不让其为空的一部,所以就会出现watcher()
function Watcher(vm, exp, cb) {
this.cb = cb;//回调函数
this.vm = vm;//对应this,也就是vue的实例
this.exp = exp;//data中对应绑定的值
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
这里非常的关键,尽管看上去代码很少, 这里一定要自己看,或者听我说,原型上get很关键,因为get是这里的核心,这是实现的点睛之笔,首先如果调用new watcher对象,就会执行里面对应的方法,就会执行get()方法,这时就会把dep.target变成this,就不再是null了,所以在这个时候调用可以引用调用data的值,就触发observer中的get方法,就会把对应的内容存放到subs的数组中,执行完之后,再让 Dep.target = null,不让其执行。如果不调用watch,就不让外界改变或增加subs数组
中的内容。这也是vue2中-watch可以监听变化的原因。
update方法还没有讲清楚,这里再讲一次,还是那个问题,为啥Dep里面的notify方法可以调用watcher里面的update,如果你看懂了上面的代码,现在你应该了解到了watcher和observer的联系, 因为每次subs数组的改变都是因为watch监听的变化,因为只有watcher中Dep.target不为null,所以,触发的环境是在watcher中的。这也就是为啥它可以调用原型的方法。
~~---------------------------------------------------------------------------~
如果哪里写的不对,欢迎指出,毕竟我也是新手,可以写在评论区,下面我会把代码全部放到底下,方便大家了解。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
{{name}}
</div>
<script>
function Observer(data){
this.data =data;
this.walk(data)
}
Observer.prototype={
defineReactive(data,key,val){ //data就是vue的对象 key就是键名 val就是键名对应的键值
var dep=new Dep()
var childObj = observer(val); //深度调用,检测是否是对象
Object.defineProperty(data,key,{
configurable:true,
get(){
if (Dep.target) {
dep.add(Dep.target); //如果增值不为空,就是放到sub数组中
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
})
}
,walk(data){
let _this=this
Object.keys(data).forEach(function(key) {
_this.defineReactive(data, key, data[key]);//监听this的每个变化
//这里的this指向的是window ,因为闭包。所以使用_this
});
}
}
function observer(value) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
//---发布订阅机制
class Dep{
constructor(){
this.subs=[]
}
add(value){ //增加值
this.subs.push(value)
}
notify(){ //更新
this.subs.forEach(function(sub){
sub.update()
})
}
}
Dep.target=null
function Watcher(vm, exp, cb) {
this.cb = cb;//回调函数
this.vm = vm;//对应this
this.exp = exp;//data
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
class Vue{
constructor(data,vm,exp){
this.data=data
Object.keys(this.data).forEach((value)=>{ //把对象的值转为数组然后执行Proxyproperty()
this.Proxyproperty(value)
})
observer(data)
vm.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
vm.innerHTML = value;
});
return this;
}
//defineproperty 使用set get 完成绑定
Proxyproperty(key){
Object.defineProperty(this,key,{
enumerable:false,
configurable:true,
get(){
return this.data[key]
}
,set(newVal){
this.data[key]=newVal
}
})
}
}
</script>
<script>
const appa =new Vue({name:'lyl',age:'18'}, app,'name')
console.log(appa.name)
appa.name=123
console.log(appa.name)
appa.age=20
console.log(appa.age)
</script>
</body>
</html>