VUE原理(一):observe和Object.defineProperty()数据劫持_Zoie_ting的博客-CSDN博客
VUE原理(二):依赖收集:Dep属性订阅器、Watcher订阅者
之前介绍了vue的数据劫持和依赖收集,深入了解了vue数据双向绑定的原理的前半部分:
- 对数据对象进行递归遍历,都加上getter和setter,这样一旦发生改变,就可以触发setter,从而监听到数据变化;
- watcher是订阅者,在自身实例化的时候往属性订阅器dep中添加自己,属性变化时触发setter,调用dep.notice通知,调用自身update方法更新。
本篇主要记录一下具体是如何更新的。
一、理论知识
- 什么是compile:compile是模板解析指令,将变量替换为数据,初始化时为节点绑定更新函数,添加监听数据的订阅者,每当数据变化时,触发函数更新视图。
- compile原理:将template转化成一个js函数,让浏览器执行这个函数,从而渲染出html元素,让视图跑起来。
- 三个阶段:解析、优化、生成
- 解析:创建虚拟节点,通过大量正则表达式对template字符串进行解析,包括标签、指令、属性等,转化为抽象语法树AST。
- 优化:遍历节点,标记静态节点,目的是在重新渲染页面时diff可以直接跳过。
- 生成:将最终的AST转化为render函数字符串
二、浅浅实践
首先使用document.createDocumentFragment();创建虚拟节点,然后将document.querySelector(el)的节点添加到虚拟节点中,对虚拟节点进行加工后再挂到el上。
class Compile{
constructor(el,vm){
this.$el = document.querySelector(el);
this.$vm = vm;
if(this.$el){
this.$fragment = this.getFragment(this.$el);
this.compile(this.$fragment);
this.$el.append(this.$fragment)
}
}
getFragment(el){
const fragment = document.createDocumentFragment();
let node;
while(node = el.firstChild){
fragment.appendChild(node)
}
return fragment;
}
}
以{{}}为例进行compile,首先这是个插值文本,nodeTypes为3;其次,通过正则表达式匹配花括号中间的属性名:
node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
初始化update,且添加监听数据的订阅者, 然后Watcher类中应该接收到实例、属性名和一个回调函数,用来更新视图:
update(node,vm,exp){
const updaterFn = this.handleText;
updaterFn && updaterFn(node,vm[exp]);
new Watcher(vm,exp,function (value) {
updaterFn && updaterFn(node,value)
})
}
handleText(node,value){
node.textContent = value;
}
Wacther类:
class Watcher {
constructor(vm,key,cb){
this.vm = vm;
this.key = key;
this.cb = cb;
Dep.target = this;
}
update(){
this.cb.call(this.vm,this.vm[this.key]);
}
}
另外,还要触发一下取值,在自身实例化时往属性订阅器dep中添加自己:this.vm[this.key];这样就实现了插值文本的双向绑定。
其他的也是如此,例如v-model,它是v-bind和@input的语法糖,以'v-'开头,我们且把它当做一个普通的指令处理:还是 初始化update且添加watcher,但是在这之外再添加一个对input事件的监听即可:
model(node,vm,exp){ //节点、实例、属性
this.update(node,vm,exp);
node.addEventListener('input',(e)=>{
vm[exp] = e.target.value; //输入内容改变,设置属性值,从而触发setter
})
}
update(node,vm,exp){
const updaterFn = this.handelModel;
updaterFn && updaterFn(node,vm[exp]);
new Watcher(vm,exp,function (value) {
updaterFn && updaterFn(node,value)
})
}
handelModel(node,value){
node.value = value;
}
读一下代码:
假设v-model绑定了name,初始化update,将name的初始值填在input输入框的value,并添加了一个Watcher,每次在输入框输入内容,属性值随着输入内容变化,属性值变化触发setter,setter触发dep.notice(),notice触发watcher的update,执行在compile中的回调函数handelModel,修改输入框的value,达到双向绑定的效果。
总结
本篇记录了什么是compile、compile的过程、每个过程的功能以及实现了一个简单的compile,讲解了v-model的双向绑定原理。由此可见,Watcher是observe和compile之间的桥梁:observe递归属性,数据更新,触发Watcher,compile解析模板,绑定Watcher,数据变化,watcher触发update,执行回调函数,通知更新视图,达到 数据变化——>视图更新,视图交互变化——>数据更新 的效果。
共勉!