300行代码实现Vue的MVVM响应式原理
前言
Vue的响应式原理是面试老生常谈的问题了,而大多数人会选择直接背答案这样的形式去应付面试,一旦面试官继续追问,便什么也答不上来了,所以,我希望能通过参考Vue源码的形式,动手编写代码(大概300行左右)实现一个简单的Vue的MVVM框架,从而让我们更好的理解Vue的响应式原理。
我们会通过
- 实现指令解析器Compile
- 实现数据监听器Observer
- 实现观察者Watcher
来实现整个Vue的MVVM的响应式原理
实现的功能不是很完善,如果大家有兴趣,可以自己补充,本次代码主要是完成整个响应式原理的流程,理解Observer,Compile,Watcher是什么,用来干什么,如何通过这三者,搭建起整个MVVM架构的桥梁,从而实现Vue的响应式原理。
创建一个简单的html作为测试
<div id="app">
<div>{{person.name}} -- {{person.age}}</div>
<div>{{person.fav}}</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div v-html="htmlStr" v-bind:style="{backgroundColor:red}"></div>
<div v-text="person.fav"></div>
<!-- <input type="text" v-bind:value='msg'> -->
<input type="text" v-model='msg'>
<div>{{msg}}</div>
<h2 v-text="tip" v-on:click="clickMe"></h2>
</div>
和Vue一样在下面的script标签中创建一个Vue对象
// 为了避免跟Vue冲突 这里使用MVue作为类名
const app = new MVue({
el: "#app",
data: {
person: {
name: 'GHkmmm',
age: '21',
fav: '编程'
},
msg: '理解Vue的双向绑定原理',
tip: '点我',
htmlStr: 'hello world!'
},
methods: {
clickMe(){
console.log('click');
}
}
})
创建MVue类
用于接收传入的参数
class MVue{
constructor(options){
this.$options = options;
this.$el = options.el;
this.$data = options.data;
if(this.$el){
// 实现一个指令解析器
new Compile(this.$el, this);
}
}
}
实现一个指令解析器Compile
class Compile{
constructor(el, vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
/*
对页面内容进行编译的时候,因为需要遍历每个对象,所以不可能一一去更改
而是通过文档碎片的形式,统一修改完,再使用fragment去更新整个页面
*/
// 1.获取文档碎片对象,放入内存中,减少页面的回流与重绘
const fragment = this.nodeFragment(this.el);
// 2.编译模版
this.compile(fragment);
// 3.追加子元素到根元素
this.el.append(fragment);
}
}
获取文档碎片
遍历子节点,添加到文档碎片对象中
nodeFragment(node){
// 创建文档碎片对象
const f = document.createDocumentFragment();
let firstChild;
//遍历<div id="app">下的子节点
while(node.firstChild){
firstChild = node.firstChild;
// 将遍历得到的节点添加到文档碎片对象中
f.append(firstChild);
}
// 返回文档碎片对象
return f;
}
编译模版
传入fragment之后,我们需要对fragment中的节点进行遍历,判断是元素节点还是文本节点,因为两者的处理方式不同,所以需要使用不同的方法进行编译
compile(fragment){
// 传入刚刚得到的文档碎片对象fragment
// 1.获取子节点
const childNodes = fragment.childNodes;
// 遍历childNodes数组
[...childNodes].forEach(child => {
/*
isElementNode(el){
return el.nodeType === 1;
}
*/
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){
// 获取节点上的所有属性
const attributes = node.attributes;
// 遍历属性数组
[...attributes].forEach(attr => {
// 使用解构赋值,将attr中的name属性和value属性,单独提取出来
// 这里的name和value是和attr中的属性对应的,不能更改
// name: v-text,v-model,v-bind....
// vaule: v-text,v-model..后面跟的值
const { name, value } = attr;
/*
判断name是否以‘v-’开头,如果是,才进行编译,如果不是,则不进行处理
isDirective(attrName){
return attrName.startsWith('v-');
}
*/
if(this.isDirective(name)){
// 根据‘-’分割name,splite方法返回的是一个数组,第一个参数我们不用,第二个参数赋值给directive
// 这里的directive和上面不一样,这里自己随意取个名字就好
const [,directive] = name.split('-');
// 因为指令不仅可能是v-text这种,还可能是v-on:click这种,所以需要进一步分割
const [dirName,eventName] = directive.split(':');
// 更新数据 数据驱动视图
// 根据dirName的不同,调用complileUtil中不同的方法
// dirName指的是text,html,bind,on...
complileUtil[dirName](node, value, this.vm, eventName)
// 删除有指令的标签上的属性
node.removeAttribute(name);
}
})
}
编译文本节点
compileText(node){
// 获取文本内容
const content = node.textContent;
if(/\{\{(.+?)\}\}/.test(content)){
complileUtil['text'](node,content,this.vm)
}
}
complileUtil对象
内部实现了对不同指令的处理方法
const complileUtil = {
getVal(expr, vm){
/*
expr可能是msg 也可能是person.name,所以需要对expr再进行一次分割
分割完得到一个数组,比如是['person', 'name'],使用reduce方法后
第一次返回vm.$data['person']
第二次返回vm.$data['person']['name']
再比如是msg,则得到数组['msg'],所以直接返回vm.$data['msg']即可
*/
return expr.split('.').reduce((data, currentVal) => {
// console.log(data);
// console.log(currentVal);
return data[currentVal];
}, vm.$data//初始值)
},
setVal(expr, vm, inputVal){
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
}, vm.$data)
},
// 编译v-text指令以及文本节点的mustache语法{{msg}}
text(node, expr, vm){
let value;
// 判断是否以‘{{’开头
if(expr.indexOf("{{")!==-1){
// 通过正则表达式 匹配双大括号,将大括号中的字符串提取出来
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 传入getVal方法,在vm.$data中查找数据
return this.getVal(args[1], vm);
})
}else{
value = this.getVal(expr, vm);
}
// 更新视图
this.updater.textUpdater(node, value);
},
// 编译v-html指令
html(node, expr, vm){
const value = this.getVal(expr, vm);
this.updater.htmlUpdater(node, value);
},
// 编译v-model指令
model(node, expr, vm){
const value = this.getVal(expr, vm);
// 视图=>数据=>视图
// 监听input事件
node.addEventListener('input', (e)=>{
this.setVal(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
// 编译v-on指令
on(node, expr, vm, eventName){
let fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false)
},
// 编译v-model指令
bind(node, expr, vm, attrName){
...//这里大家可以自己动手去实现一下
},
//更新的函数
updater: {
textUpdater(node, value){
node.textContent = value;
},
htmlUpdater(node, value){
node.innerHTML = value;
},
modelUpdater(node, value){
node.value = value;
},
bindUpdater(node, attrName, value){
node[attrName] = value
}
}
}
实现一个数据监听器Observer
创建Observer类
作用
使用Object.defineProperty,为data中的所有属性设置getter和setter
class Observer{
constructor(data){
this.observer(data);
}
observer(data){
if(data && typeof data === 'object'){
// 遍历data对象
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
defineReactive(data, key, value){
// 递归遍历
this.observer(value);
const dep = new Dep();
// 监听并劫持所有的属性
// defineProperty传入data,key就是为了给data[key]添加对应的gettter和setter
Object.defineProperty(data, key, {
enumerable: true,
configurable: false, //描述属性是否配置,以及可否删除
get(){
// 当访问属性时,会调用此函数
// 初始化
// 订阅数据发生变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
// 当属性值被修改时,会调用此函数
// 这里使用箭头函数,是为了让this指向上层,而不是Object
set: (newVal) => {
this.observer(newVal)
if(newVal !== value){
value = newVal;
}
//告诉Dep通知变化
dep.notify();
}
});
}
}
在MVue类中实现
class MVue{
constructor(options){
this.$options = options;
this.$el = options.el;
this.$data = options.data;
if(this.$el){
// 实现一个数据观察者
new Observer(this.$data);
// 实现一个指令解析器
new Compile(this.$el, this);
}
}
}
实现依赖收集器Dep
创建Dep类
作用
- 收集观察者
- 如果Observer中劫持的数据发生变化,会通知Dep去通知对应的观察者
class Dep{
constructor() {
this.subs = [];
}
// 收集观察者
addSub(watcher){
this.subs.push(watcher);
}
// 通知观察者更新
notify(){
this.subs.forEach(w => w.update())
}
}
实现一个Watcher去更新视图
创建Watcher类
class Watcher{
constructor(vm, expr, callback){
this.vm = vm;
this.expr = expr;
this.callback = callback;
this.oldVal = this.getOldVal()
}
getOldVal(){
Dep.target = this;
const oldVal = complileUtil.getVal(this.expr, this.vm)
Dep.target = null;
return oldVal;
}
update(){
const newVal = complileUtil.getVal(this.expr, this.vm);
if(newVal !== this.oldVal){
this.callback(newVal);
}
}
}
在complileUtil的每个处理指令的方法中 实现Watcher
new Watcher(vm, expr, callback)
const complileUtil = {
getContentVal(expr, vm){
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm)
})
},
text(node, expr, vm){
let value;
if(expr.indexOf("{{")!==-1){
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], ()=>{
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){
const value = this.getVal(expr, vm);
// 绑定观察者,将来数据发生变化,触发这里的回调,进行更新
new Watcher(vm, expr, (newVal)=>{
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node, value);
},
...
}
展示
到这,我们就实现了整个Vue的MVVM响应式,也实现了数据的双向绑定
总结
vue是采用数据劫持配合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器Dep,去通知观察者,做出对应的回调函数,去更新视图。
MVVM作为绑定的入口,整合了Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起
Observer,Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果
如果文章有问题欢迎大家指出
参考教程:参考教程
上一篇: 一个简单的时钟程序
下一篇: Vue 实现mvvm框架