手写vue源码(精简版) -- 更新显示(Watcher),双向数据绑定

vue原理图

vue原理图

剩余工作

上一节我们实现了编译工作,完成了视图的初始化显示,剩下的就是数据的响应化了,所谓响应化,即当数据发生变化时,视图能够响应变化并更新视图,这时我们需要引入一个Watcher对象来帮我们完成这部分的工作。在完成这部分工作前,我们需要弄清楚两个问题:
1、什么时候引入Watcher。要搞明白这个问题,我们时刻要记住,Watcher是为了实现数据响应化的,是什么数据的响应化?是那些依赖如大括号表达式,v-xxx指令等所对应数据的响应化,所以我们其实应该在编译阶段,依赖的统一入口处update中引入Watcher,这样就保证了每一个依赖都对应一个Watcher,确保了每个依赖在数据改变时都能得到更新。
2、传什么参数给Watcher。首先需要Watcher传递一个更新函数,在数据变化时执行更新,其次是启动依赖收集工作,我们之前给data中的每个数据定义了get方法,每个数据还对应了一个Dep实例,在get中当我们检测到Dep.target中有值时,就将Dep.target收集起来,这个Dep.target就是Watcher实例,所以我们要在Watcher构造函数中给Dep.target赋值,并且调用一下get,从而启动依赖的收集,所以还需要传入vue实例和依赖所对应的key。

实现更新显示

下面我们实现更新显示。

//ES6实现
//更新显示实现
class Watcher {
	constructor(vm, exp, cb) {
		this.cb = cb;
		Dep.target = this; //把Watcher赋值给Dep.target
		vm[exp]; //调用get
		Dep.target = null;
	}
	update() { //dep.notify()通知更新最终入口
		//内存 --> 视图,也是双向数据绑定中的另一方向
		this.cb(); //更新显示
	}
}
class Vue {
    constructor(options) {
   		//缓存配置项
        this.$el = options.el || document.body;
        this.$options = options;
        const data = this.$data = options.data;
		//劫持数据
        this.hijackData(data);
        //编译模版
		this.compile(this.$el);
    }
	
	compile(el) {
		//找到vue管理的区域
		this.$el = this.isElementNode(el) ? el : document.querySelector(el);
		if (this.$el) {
			//将this.$el中的子节点转移到内存中,document --> 内存
			this.$fragment = this.node2Fragment(this.$el);
			//编译
			this.compileElements(this.$fragment);
			//内存 --> document
			this.$el.appendChild(this.$fragment);
		}
	}
	//将文档节点转移到fragment(内存)中
	node2Fragment(node) {
		let child = null;
		let fragment = document.createDocumentFragment();
		while (child = node.firstChild) {
			//节点有且只有一个父节点,
			//所以是转移,不是复制,不会出现两份,
			//相当于是一个水缸的水舀到另一个水缸里,fragemnt满了,this.$el空了
			fragment.appendChild(child); 
		}
		return fragment;
	}
	compileElements(vNode) {
		let text = '';
		//正则表达式,用于匹配大括号表达式
		const reg = /\{\{(.*)\}\}/;
		//转为真数组并遍历所有节点
		Array.from(vNode.childNodes).forEach(node => {
			text = node.textContent;
			if (this.isElementNode(node) { //元素节点,解析所有的指令属性
				let exp = ''; //表达式
				let dir = ''; //指令
				let attrName = '';
				let attrs = node.attributes; 
				 //取出所有属性,转为数组并遍历
				Array.from(attrs).forEach(attr => {
					exp = attr.value;
					attrName =  attr.name;
					// 普通指令v-text,v-html,v-model等
					if (this.isDirective(attrName) {
						dir = attrName.substring(2);
						this.update(node, exp, dir);
						node.removeAttribute(attrName);
					//事件指令@click等
					} else if (this.isEvent(attrName) {
						dir = attrName.substring(1);
						this.eventHandler(node, exp, dir);
						node.removeAttribute(attrName);
					}
				});
			} else if (this.isTextNode(node) && reg.test(text)) { //文本节点,大括号表达式
				this.update(node, RegExp.$1.trim(), 'text');
			}
			//递归遍历所有层次的节点
			if (node.hasChildNodes()) {
				this.compileElements(node);
			}
		});
	}
	//事件处理器
	eventHandler(node, exp, eType) {
		const cb = this.$options.methods && this.$options.methods[exp];
		cb && node.addEventListener(eType, cb.bind(this));
	}
	//更新视图,依赖的统一入口,每个引用过data中数据的依赖都会进来这里
	update(node, exp, dir) {
		//拿到相对应的更新函数
		const fn = this[dir+'Updater'];
		//初始化更新显示,这里需要指定调用的实例this,即vue实例
		fn && fn.call(this, node, exp);
		//变化更新显示,下一节内容
		new Watcher(this, exp, () => {
			fn && fn.call(this, node, exp);
		});
	}
	textUpdater(vm, node, exp) {
		node.textContent = vm[exp];
	}
	htmlUpdater(vm, node, exp) {
		node.innerHTML = vm[exp];
	}
	modelUpdater(vm, node, exp) {
		node.value = vm[exp];
		 //添加input事件监听
		 //v-model双向数据绑定的其中一个方向,即:视图 --> 内存
		node.addEventListener('input', e => {
			vm[exp] = e.target.value;
		});
	}
	
	//判断是否为元素节点
	isElementNode(node) {
		return (node.nodeType === 1);
	}
	//判断是否为文本节点
	isTextNode(node) {
		return (node.nodeType === 3);
	}

    hijackData(data) {
        if (!data || typeof data !== 'object') { //不存在或者不为对象,返回(递归退出条件)
            return;
        }
        //拿到data中所有可枚举的属性
        Object.keys(data).forEach(key => {
        	//递归遍历所有层次
            this.hijackData(data[key]);
            //这里可以把第一节([数据代理](https://blog.csdn.net/huolinianyu/article/details/100111065))
            //中的数据代理放到下面,复用相同的处理逻辑
            //this.proxyData(key);
            //创建观察者
            new Observer(data, key);
        });
    }
    proxyData(key) {
    	Object.key(this, key, {
    		enumerable: true,
    		configurable: false,
    		get() {
    			return this.$data[key];
    		},
    		set(newVal) {
    			this.$data[key] = newVal;
    		}
    	});
    }
}

//观察者实现
class Observer {
    constructor(data, key) {
    	//对数据进行观察
        this.observe(data, key, data[key]);
    }
    observe(data, key, val) {
    	//data中每一个数据对应一个Dep容器,存放所有依赖于该数据的依赖项
        const dep = new Dep();
        Object.defineProperty(data, key, {
            enumerable: true, //可枚举
            configurable: false,//不能再配置
            get() {
                if (Dep.target) {//Dep.target存放具体的依赖,在编译阶段检测到依赖后被赋值
                    dep.addDep(Dep.target); //依赖收集
                }
                return val;
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify(); //当数据发生变化时,通知所有的依赖进行更新显示
            }
        });
    }
}

//Dep容器,data中的每个数据会对应一个,用来收集并存储依赖
class Dep {
    constructor() {
        this.deps = []; //所有的依赖将存放在该数组中
    }
    //收集依赖
    addDep(dep) {
        this.deps.push(dep);
    }
    //通知更新
    notify() {
        this.deps.forEach(dep => {
            dep.updata();
        });
    }
}

完整源码
上一节:模版编译, 双向数据绑定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值