一、前言
解析vue源码之前,先写一个简单版的双向绑定原理代码,有利于之后后续源码的理解,涉及到的知识点有Object.defineproperty。
1 Object.defineproperty 用法
- Object.defineProperty(obj, prop, descriptor)
obj是要定义属性的对象,prop指要定义和修改的属性名称或Symbol ,descriptor指定义和修改的属性描述符。
get是属性的getter函数,返回值会用作属性的值,如果没有getter,属性值为undefined。访问该属性的时候会调用这个函数。
set是属性的setter函数,参数为新的属性值,如果没有setter,属性值为undefined。修改该属性的时候会调用这个函数。
二、思路
首先要使数据变得可以监测,利用Object.defineproperty的get和set可以实现这一点。那么怎样监测DOM中的数据呢?需要compile解析出双花括号中和v-bind等指令的属性名,从而获取改属性名对应的属性,添加到dom中的node或者attr的value中。
数据变化后,怎样更新到视图中呢?通过new watch(vm,key,cb)收集依赖,而Dep是订阅器,在get函数中添加订阅者。数据更新后,在set函数中,Dep遍历订阅者并更新函数。
三、具体实现
class Vue{
constructor(options){
this.$options = options;
this.$data = options.data;
this.observer(this.$data);
//代理不需要递归
Object.keys(this.$data).forEach(key=>{
this.proxyData(key);
})
new Compile(this.$options.el,this)
}
observer(obj){
if(!obj || typeof(obj) !=='object'){
return;
}
Object.keys(obj).forEach(key=>{
this.defineReactive(obj,key,obj[key]);
})
}
defineReactive(obj,key,value){
//递归嵌套object
let dep = new Dep();
// 一个key对应一个dep,但有可能对应多个watcher
Object.defineProperty(obj,key, {
get(){
Dep.target && dep.addSub();
return value;
},
set(newval){
if(value==newval){
return;
}
value = newval;
dep.notify();
}
})
this.observer(value);
}
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key];
},
set(newval){
// this.key设置的newvalue赋值在data上,get返回代理到vm上
this.$data[key] = newval;
}
})
}
}
// 订阅器,收集和更新订阅者
class Dep{
constructor(){
this.subs = [];
}
addSub(){
if(Dep.target){
this.subs.push(Dep.target);
}
}
notify(){
this.subs.forEach(sub=>{
sub.update();
})
}
}
class Watch{
constructor(vm,key,cb){
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.get();
}
get(){
Dep.target = this;
const value = this.vm.$data[this.key];
Dep.target = null;
return value;
}
update(){
let newval = this.vm.$data[this.key];
if(this.value !== newval){
this.value = newval;
}
this.cb(this.value);
}
}
class Compile{
constructor(el,vm){
this.el = document.querySelector(el);
this.vm = vm;
this.fragNodes = this.getFragmentNodes();
this.compileFragment(this.fragNodes);
this.el.appendChild(this.fragNodes);
}
// 获取文档碎片
getFragmentNodes(){
let fragNodes = document.createDocumentFragment();
while(this.el.firstChild){
fragNodes.appendChild(this.el.firstChild);
}
return fragNodes;
}
compileFragment(nodes){
let childNodes = nodes.childNodes;
const self = this;
// es5转换类数组方法:[].slice.call(类数组) Array.prototype.slice.call(类数组)
Array.from(childNodes).forEach(node=>{
if(this.isTextNode(node)){
// .+?一旦匹配到就不继续搜寻,惰性模式 exec可返回()里面匹配的值
let reg = /\{{2}(.+?)\}{2}/g,
t = node.textContent,
r = reg.exec(t),
key;
if(r){
key = r[1].trim();
this.CompileText(node,key);
}
}else if(this.isElement(node)){
let nodeAttrs = node.attributes;
if(nodeAttrs.length>0){
Array.from(nodeAttrs).forEach(nodeAttr=>{
const attrName = nodeAttr.name;
if(this.isBindDirective(attrName)){
this.CompileBind(nodeAttr,nodeAttr.value);
}
})
}
}
if(node.childNodes && node.childNodes.length>0){
self.compileFragment(node);
}
})
}
CompileText(node,key){
let val = this.vm.$data[key];
this.updateText(node,key,val)
}
updateText(node,key,val){
// get 新data
node.textContent = val;
// set 新data
new Watch(this.vm,key,function(newval){
node.textContent = newval;
})
}
isBindDirective(attrName){
const reg =/v\-bind|^\:/g;
return reg.exec(attrName);
}
CompileBind(nodeAttr,key){
let val = this.vm.$data[key];
nodeAttr.value = val;
new Watch(this.vm,key,function(newval){
nodeAttr.value = newval;
})
}
isTextNode(node){
return node.nodeType === 3;
}
isElement(node){
return node.nodeType === 1;
}
}
let vm = new Vue({
el:'#app',
data:{
name:'banana',price:'$10',place:'越南'
}
})