目录
双向数据绑定
数据劫持
vue.js采用的是数据劫持结合发布者-订阅模式,通过Object.defineProperty()来劫持各个属性的Setter和Getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
图解:
Compile:
解析指令,并初始化视图,同时订阅数据变化,通过Watcher绑定更新函数
Observer :
劫持监听所有属性,向Dep通知变化
Dep:
向Watcher通知变化
Watcher:
向Dep添加订阅者,并触发Updater更新视图
简单实现:
- 创建一个模板(index.html),引入MVue.js和Observer.js。仿照出类似于vue.js的指令,包含(v-text、v-html、v-model、v-bind、v-on:click等,并使用{{变量名}}的方式)
- 创建Class MVue初始化构造函数el,data。分别实现Compile 和 Observer
- 创建Class Compile,首先判断el是否为根节点,随后将根节点通过循环appendChild全部追加到文档碎片对象当中(document.createDocumentFragment()).,进行模板编译。将文档碎片的内容追加回el当中。
- 在编译(compile(fragment))的过程中,通过解构的方式遍历区分出出元素节点(node.nodeType === 1),和文本节点,采用不同的方式编译
- 元素节点拆分出所有属性(node.attributes),然后通过解构遍历出属性名和属性值。随后通难过属性名startsWith()找出所有'v-'开头的指令,@开头的事件,以及:开头的bind绑定。解构过滤出所有属性名
- 将过滤出来的{{变量名}}表达式传入compileUtils['属性名'](例如:compileUtils['text'])方法用来解析指令表达式和值。通过expr.split('.').reduce((data,curr)=>{return data[curr]},vm.$data)来获取data中的变量值(如果是v-text,则需要expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{},去除花括号,取出args[1]在进行reduce累加)
- 将累加查找出的变量值更新到视图中,其中每种类型的节点属性都有不同的方式(textNode.textContent = value,htmlNode.innerHTML = value,modelNode.value =value,bindNode[arrtName]=value),解析指令完成
- 文本节点({{...}})则可以直接使用创建的CompileUtils来进行text解析
- 创建Observer.js ,创建Observer类。初始化调用方法,判断数据类型是否为对象,遍历出data中所有的key值,(object.keys(data).forEach(key=>{})),用获取到的Key值,获取value(data[key]),将value再次判断数据类型,递归深层对象,直到拿到对象所有的属性值
- 通过获取到的data key,value 使用Object.defineProperty(obj,key,{get(){},set:()=>{}})劫持监听data中所有的属性值得变化。
- 创建Class Dep订阅器,也称观察者收集器。初始化一个数组arr进行收集watcher.添加方法addWatcjer(),添加方法notify用来通知观察者更新(this.arr.forEach(w=>w.update())).
- 当订阅数据发生变化时(defineProperty.get()获取数据更新时)往Dep中添加watcher。当数据发生改变时(defineProperty.set()),通知dep将变化通知给watcher
- 创建Class Watcher,用来观察调用更新视图初始化存储旧的数据,等到dep.notify()通知随后进行更新回调,通过对比新老数据,决定是否更新。
- 观察者应当绑定在所有数据要发生变化的地方,来绑定劫持每个属性的变化。通过回调对比新老数据。将新数据更新在视图当中。
- 表单双向数据绑定:添加input事件监听,将input值set到data[currVal]当中
简单代码实现
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{msg}}</h1>
<h1>{{person.name}}--{{person.age}}</h1>
<h1>{{person.fav}}</h1>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div v-text="msg"></div>
<div v-html="htmlStr">111</div>
<input type="text" v-model="msg">
<a :href="link">跳转</a>
<button v-on:click="handleClick">on按钮</button>
<button @click="handleClick">@按钮</button>
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script >
let vm = new MVue({
el:'#app',
data:{
person:{
name:"kiki",
age:23,
fav:'姑娘'
},
msg:'学习MVVM实现原理',
link:'http://www.baidu.com',
htmlStr:'<h1>htmlStr</h1>'
},
methods:{
handleClick(){
console.log(this);
// this.$data.person.name = '更改了person.name';
this.person.name = 'proxy取值';//proxy取值
}
}
})
</script>
</body>
</html>
MVue.js
const compileUtils = {
getVal(expr,vm){
return expr.split('.').reduce((data,currentVal)=>{
//console.log("currentVal:"+currentVal);//msg
return data[currentVal];
},vm.$data)
},
setVal(expr,vm,inputVal){
return expr.split('.').reduce((data,currentVal)=>{
data[currentVal] = inputVal;
},vm.$data)
},
getContentVal(expr,vm){
return value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(args[1],vm);
})
},
text(node,expr,vm){//expr:msg 学习MVVM原理
let value;
if(expr.indexOf('{{') !== -1){
value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
//console.log(args);// ['{{msg}}', 'msg', 0, '{{msg}}'] ['{{person.name}}', 'person.name', 0, '{{person.name}}--{{person.age}}']
//绑定观察者,将来数据反生变化,触发这里的回调进行更新
new Watcher(vm,args[1],(newVal)=>{
this.updater.textUpdater(node,this.getContentVal(expr,vm));
})
return this.getVal(args[1],vm);
});
}else{
value =this.getVal(expr,vm);
}
this.updater.textUpdater(node,value);
},
html(node,expr,vm){
let value =this.getVal(expr,vm);
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdater(node,newVal);
});
this.updater.htmlUpdater(node,value);
},
model(node,expr,vm){
//绑定更新函数,数据=>视图
const value =this.getVal(expr,vm);
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdater(node,newVal);
});
//视图=>数据=>视图
node.addEventListener('input',(e)=>{
//设置值
this.setVal(expr,vm,e.target.value);
})
this.updater.modelUpdater(node,value);
},
on(node,expr,vm,eventName){
let fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName,fn.bind(vm),false);//fn.bind(vm)绑定实例事件
},
bind(node,expr,vm,attrName){
//自己实现
const value =this.getVal(expr,vm);
this.updater.bindUpdater(node,value,attrName);
},
//更新函数
updater:{
textUpdater(node,value){
// console.log(node,value);
node.textContent = value;
},
htmlUpdater(node,value){
node.innerHTML = value;
},
modelUpdater(node,value){
node.value = value;
},
bindUpdater(node,value,attrName){
node[attrName]= value;
}
}
}
//解析指令,初始化视图,订阅数据变化,绑定更新函数
class Compile{
constructor(el,vm){
this.el = this.isElementNode(el)? el : document.querySelector(el);
this.vm = vm;
//获取文档碎片对象,放入内存中减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// console.log(fragment);
//编译模板
this.compile(fragment);
//追加子元素到根元素
this.el.appendChild(fragment);
}
//编译方法
compile(fragment){
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if(this.isElementNode(child)){
// console.log('元素节点',child);
this.compileElement(child);
}else{
// console.log('文本节点',child);
this.compileText(child);
}
//递归遍历出所有子节点元素
if(child.childNodes && child.childNodes.length){
this.compile(child);
}
})
}
//编译节点
compileElement(node){
// console.log("eleNode:"+node);
const attributes = node.attributes;
[...attributes].forEach(attr =>{
// console.log(attr);
const {name,value} = attr;//解构属性和值
if(this.isDirective(name)){//属于指令 v-text v-html v-model v-on:click...
const [,dirctive] = name.split('-');//text html model on:click
const [dirName,eventName] = dirctive.split(':');//事件名分割 //on click
//更新数据,数据驱动视图
compileUtils[dirName](node,value,this.vm,eventName);
//删除有指令的标签上的属性
node.removeAttribute('v-' + dirctive);
}else if(this.isEventName(name)){//@事件,v-on简写
let [,eventName] = name.split('@');
compileUtils['on'](node,value,this.vm,eventName);
}else if(this.isAttrsName(name)){//:事件,v-bind简写
let [,eventName] = name.split(':');
compileUtils['bind'](node,value,this.vm,eventName);
}
})
}
//判断是否是'v-'开头的指令
isDirective(attrName){
return attrName.startsWith('v-');
}
isEventName(attrName){
return attrName.startsWith('@');
}
isAttrsName(attrName){
return attrName.startsWith(':');
}
//编译文本
compileText(node){
// console.log("textNode"+node);
const content = node.textContent;
if(/\{\{(.+?)\}\}/.test(content)){
// console.log(content);
compileUtils['text'](node,content,this.vm)
}
}
//将所有获取到的节点元素追加到文档碎片对象中
node2Fragment(el){
//创建文档碎片对象
const f = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
f.appendChild(firstChild);
}
return f;
}
//判断是否是节点元素
isElementNode(node){
return node.nodeType === 1;
}
}
//入口
class MVue{
constructor(options){
this.$el= options.el;
this.$data = options.data;
this.$options = options;
if(this.$el){
//实现一个数据的观察者
new Observer(this.$data);
//实现指令解析器
new Compile(this.$el,this);
//proxy代理,将vm.$data.attr 简化为vm[attr]直接取值
this.proxyData(this.$data);
}
}
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{
get(){
return data[key];
},
set(newVal){
data[key] = newVal;
}
})
}
}
}
Observer.js
//观察者,添加到订阅器,通知updater更新视图
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
//先将旧值保存起来
this.oldVal = this.getOldVal();
}
getOldVal(){
Dep.target= this;
const oldVal = compileUtils.getVal(this.expr,this.vm);
Dep.target = null;
return oldVal;
}
update(){
const newVal = compileUtils.getVal(this.expr,this.vm);
if(newVal !== this.oldVal){
this.cb(newVal)
}
}
}
//订阅器,收集观察者Watcher,通知观察者变化
class Dep {
constructor(){
this.subs = [];
}
//收集观察者
addSub(watcher){
this.subs.push(watcher);
}
//通知观察者去更新
notify(){
console.log("通知了观察者,this.subs:",this.subs);
this.subs.forEach(w=>w.update());//遍历所有观察者进行更新
}
}
//劫持监听所有属性,通知Dep
class Observer{
constructor(data) {
this.observer(data)
}
observer(data){
if(data && typeof data === 'object'){
// console.log( Object.keys(data));
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key]);
})
}
}
defineReactive(obj,key,value){
//递归劫持深层次对象
this.observer(value);
const dep = new Dep();
Object.defineProperty(obj,key,{
enumerable:true,
configurable:false,
get(){
//初始化
//订阅数据发生变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set:(newVal)=>{
this.observer(newVal);
if(newVal !== value){
value = newVal;
}
//告诉Dep通知变化
dep.notify();
}
})
}
}
总结:面试阐述
vue 是采用数据劫持配合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter 和 getter,在数据变动时,发布消息给依赖收集器,通知观察者做出对应的回调函数,然后更新视图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,同构Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通讯桥梁,达到数据变化=>视图更新,视图交互变化=>数据model变更的双向数据绑定。