手动实现vue数据双向绑定
1.看了vue 数据双向绑定的源码决定 根据自己的理解 实现一个简单版的 vue双向绑定
- 首先根据用法 new Vue({…}) 可以知道 vue 构造函数接收一个 对象参数
class Vue {
constructor(options){
}
}
- 其次 数据双向绑定的原理 是使用了观察者模式 和 Object.defineProperty 方法对vue实例data项的属性进行劫持,所以我们先实现这两步。代码如下
class Vue {
constructor(options){
this.$data = options.data || {}; //对data 属性进行初始化
this.dep = new Dep(); // 初始化订阅列表
this.Observer(this.$data); //将data 对象传入 观察者方法 内进行 属性劫持
}
//观察者
Observer(data){
for(let key in data){ // 对data 对象 进行 遍历
let val = data[key]; // 将data 属性值保存出来
this.dep[key] = []; // 将每个属性都注册一个 订阅列表 key 值作为 唯一标识
//此处 劫持 this 而不劫持this.$data 是为了 在vue 中可以直接使用 this.key 的方式 访问 this.$data 内的属性。相当与将this.$data 的属性代理到 this 上
Object.defineProperty(this,key,{
get:function(){
return val;//直接 返回 this.$data 内的对应属性值
},
set:function(newVal){
if(newVal === val) return;
val = newVal;
//数据 更新 触发 相应的 订阅列表 进行 视图的更新
this.dep.noify(key); // 将key 值传入 触发 对应的 订阅列表内的 方法
}
})
}
}
}
// 订阅列表
class Dep{
constructor(){
}
//触发 发布信息
noify(key){
this[key].forEach(item=>{
item.update(); //执行 对应的 更新方法
})
}
}
- 上述已经实现了基本的结构功能,还需要实现 一个监听器 来提供相应的 更新方法
class Vue {
constructor(options){
this.$data = options.data || {}; //对data 属性进行初始化
this.dep = new Dep(); // 初始化订阅列表
this.Observer(this.$data); //将data 对象传入 观察者方法 内进行 属性劫持
}
//观察者
Observer(data){
for(let key in data){ // 对data 对象 进行 遍历
let val = data[key]; // 将data 属性值保存出来
this.dep[key] = []; // 将每个属性都注册一个 订阅列表 key 值作为 唯一标识
//此处 劫持 this 而不劫持this.$data 是为了 在vue 中可以直接使用 this.key 的方式 访问 this.$data 内的属性。相当与将this.$data 的属性代理到 this 上
Object.defineProperty(this,key,{
get:function(){
return val;//直接 返回 this.$data 内的对应属性值
},
set:function(newVal){
if(newVal === val) return;
val = newVal;
//数据 更新 触发 相应的 订阅列表 进行 视图的更新
this.dep.noify(key); // 将key 值传入 触发 对应的 订阅列表内的 方法
}
})
}
}
}
// 订阅列表
class Dep{
constructor(){
}
//触发 发布信息
noify(key){
this[key].forEach(item=>{
item.update(); //执行 对应的 更新方法
})
}
}
//为了看起来方便 将上述代码 一起 copy 下来并做稍微的更改
//监听器
class Watcher {
constructor(vm,elem,cb){ // vm 就是需要更新的 vue 实例 elem 就是需要更新 dom节点 cb 就是提供的更新回调函数
this.vm = vm;
this.elem = elem;
this.cb = cb;
}
update(){
this.cb.call(this); //此处使用call 方法来将this 指向 当前的 watcher 监听实例
}
}
- 监听器已经有了,还需要一个收集依赖的方法,vue中是在解析渲染DOM的时候进行这些依赖的收集,下面实现这个complie 方法 并 将上述的代码 进行 改进 实现最终版本。
class Vue {
constructor(options){
this.$data = options.data || {}; //对data 属性进行初始化
this.$el = document.querySelector(options.el); // 此处只考虑传入 el 属性的vue实例 template 不考虑(过于麻烦) 根据传入的el 属性值选择出挂载节点
this.dep = new Dep(); // 初始化订阅列表
this.Observer(this.$data); //将data 对象传入 观察者方法 内进行 属性劫持
this.Complie(this.$el); // 解析
}
//观察者
Observer(data){
for(let key in data){ // 对data 对象 进行 遍历
let val = data[key]; // 将data 属性值保存出来
this.dep[key] = []; // 将每个属性都注册一个 订阅列表 key 值作为 唯一标识
//此处 劫持 this 而不劫持this.$data 是为了 在vue 中可以直接使用 this.key 的方式 访问 this.$data 内的属性。相当与将this.$data 的属性代理到 this 上
Object.defineProperty(this,key,{
get:function(){
return val;//直接 返回 this.$data 内的对应属性值
},
set:function(newVal){
if(newVal === val) return;
val = newVal;
//数据 更新 触发 相应的 订阅列表 进行 视图的更新
this.dep.noify(key); // 将key 值传入 触发 对应的 订阅列表内的 方法
}
})
}
}
//解析器 (收集依赖)
Complie(el){
const children = el.children || []; // 防止undefined 报错
const vm = this; // 将vue实例保存,用于后续的使用 主要是防止this指向性的问题出现
for(let element of children){
//此处实现双向数据绑定 就暂时实现 v-text v-model 指令
if(element.hasAttribute('v-model')){
const exp = element.getAttribute('v-model'); // 获取到 依赖属性
//进行input事件的处理
element.oninput = function(e){
vm[exp] = e.target.value; // 视图更新 触发 数据更新
}
//找到对应的订阅者列表 添加监听者实例(订阅者)
vm.dep[exp].push(new Watcher(vm,element,function(){ //更新回调函数 函数内部对视图进行更新 //此处this 最终调用时 会通过call 指向 watcher 实例
this.elem.value = this.vm[exp];
}))
}
//同理 对 v-text 指令进行处理
if(element.hasAttribute('v-text')){
const exp = element.getAttribute('v-text');// 获取到 依赖属性
vm.dep[exp].push(new Watcher(vm,element,function(){
this.elem.innerText = this.vm[exp];//此处为什么 能访问 exp 变量 =》 闭包知识点
}))
}
// 如果当前元素还有子元素 那么进行递归 解析
if(element.children && element.children.length){
this.Complie(element);
}
}
}
}
// 订阅列表
class Dep{
constructor(){
}
//触发 发布信息
noify(key){
this[key].forEach(item=>{
item.update(); //执行 对应的 更新方法
})
}
}
//为了看起来方便 将上述代码 一起 copy 下来并做稍微的更改
//监听器
class Watcher {
constructor(vm,elem,cb){ // vm 就是需要更新的 vue 实例 elem 就是需要更新 dom节点 cb 就是提供的更新回调函数
this.vm = vm;
this.elem = elem;
this.cb = cb;
}
update(){
this.cb.call(this); //此处使用call 方法来将this 指向 当前的 watcher 监听实例
}
}
- 简易的vue实现完成,看一下效果
//html
<div id="app">
<h3>this is vue 2.x</h3>
<input type="text" v-model="inputValue" />
<p v-text="inputValue"></p>
</div>
//js
new Vue({
el:'#app',
data:{
inputValue:''
}
})
//效果实现