在这里跟大家分享下Vue的MVVM实现原理,主要是简单的实现了下数据的双向绑定,v-model和{{}}语法和计算属性。前面每句都有注释和步骤,但是后面的部分由于不好进行步骤划分,就只解释了原因。这部分呢都是看的前端大佬姜文老师的讲解实现的。如有不对的地方,请提出来。
目录:
先看html文件:
在这里插入代码片<!DOCTYPE html>
<html lang="en">
<head>
<title>Vue的MVVM原理的简单实现</title>
</head>
<body>
<div id="app">
<input type="text" v-model="school.name">
<div>{{school.name}}</div>
<div>{{school.age}}</div>
<div>{{getNewName}}</div>
<ul>
<li>1</li>
<li>2</li>
</ul>
</div>
</body>
<script src="./yuanMa.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
school: {
name: 'dapeng',
age: 18,
}
},
computed: {
getNewName() {
return this.school.name + '666'
}
}
})
</script>
</html>
接下来就是我们的重中之重,源码文件,实现上面的功能可以和Vue.js进行无缝对接。
yuanMa.js
class Dep { // 39.订阅,里面存放着观察者,如果数据一变,就通知对应的观察着进行更新
constructor() {
this.subs = []; // 存放所有的watcher
}
// 订阅
addSub(watcher) { // 添加watcher
this.subs.push(watcher)
}
// 发布
notify() { // 这个方法会在Object.defineProperty中的set方法中调用,因为那个时候数据变化了,就改通知观察者更新视图了
this.subs.forEach(watcher => watcher.update())
}
}
class Watcher { // 37.观察者,拿到每个值的老值,然后当值变化了,我们就进行视图的更新,也就是重新改变input和textContent的值
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 37.默认存放个老值
this.oldValue = this.get()
}
get() {
Dep.target = this // 先把自己放在this上
// 取值,把这个观察者和数据关联起来
let value = CompileUtil.getVal(this.vm, this.expr)
Dep.target = null //
return value
}
update() { // 38.数据更新后 ,会调用观察者的update方法
let newVal = CompileUtil.getVal(this.vm, this.expr) // 更新的时候才会调用update,后面看
if (newVal !== this.oldValue) { // 新旧老值比较,不一样就进行更新
this.cb(newVal)
}
}
}
//30. 创建Observer类,数据劫持,劫持数据用Object.defineproperty来定义
//36. 但是这样的话,我们只是劫持了数据,但是数据变化的时候,视图还是不会更新的,因为我们还没处理,所以我们就需要观察者watcher和dep订阅
class Observer {
constructor(data) {
// console.log(data)
this.observer(data) // 31.调用observer方法
}
observer(data) {
if (data && typeof data == 'object') { // 判读数据合法性和是否是对象,是对象就用Object.defineproperty来定义
for (let key in data) {
this.defineReactive(data, key, data[key]) // 32. 将对象的属性进行转换,调用defineReactive
}
}
}
defineReactive(obj, key, value) { // 32.调用defineReactive
this.observer(value) // 33.如果value也是对象的就会再次使用Object.defineproperty来定义
let dep = new Dep() // 给每一个属性,都加上一个具有发布订阅的功能
Object.defineProperty(obj, key, { // obj是 { school: { name: 'dapeng', age: 18 } }, key就是name或者age
get() {
Dep.target && dep.addSub(Dep.target) // 创建watcher时,会取到对应的内容,并且把watcher放到了全局上
return value // 33. Object.defineproperty的get返回的值就是对应key的值
},
set: (newVal) => { // 这里使用箭头函数是因为下面的this指向问题
if (newVal != value) {
// 35.如果我们修改的新增是个对象,那么我们也要调用this.observer()
this.observer(newVal)
// 34.一旦修改对应key的值,Object.defineproperty的set的newVal就是修改后的那个值,我们就让原来值等于新值,get返回的就是修改后的值了
value = newVal
dep.notify() // 数据变化了就要通知视图进行更改
}
}
})
}
}
// 23. 在这里统一的地方来分别用不同的方法来处理v-开头的指令和{{}}
CompileUtil = {
// 28.!!!! 我们这里做完了那就相当于编译完成了,接下来如果我们要做的就是数据的双向绑定,也就是使用Object.defineproperty()
getVal(vm, expr) { // 25.这个方法是用来辅助取值的,expr是属性值school.name,从vue的实例上找出这个变量所代表的值,这里没有考虑数组
return expr.split('.').reduce((data, current) => { // 最后执行完毕这个reduce方法,这里返回的就是dapeng
return data[current];
}, vm.$data)
},
setValue(vm, expr, value) { // 用来监听视图变化去改变数据的方法
expr.split('.').reduce((data, current, index, arr) => { // school.name
if (index == arr.length -1) { // 取到最后一层的时候,就代表可以赋值了
return data[current] = value
}
return data[current];
}, vm.$data)
},
model(node, expr, vm) { // 24.处理v-model的方法, node是节点,expr是属性值school.name,vm是Vue的实例
// 给输入框赋予value属性 node.value = xxx
let fn = this.updater['modelUpdater']
new Watcher(vm, expr, (newVal) => { // 给输入框添加个观察者,如果稍后数据更新了会触发此方法会拿新值赋予输入框
fn(node, newVal)
})
let value = this.getVal(vm, expr) // 25.expr是属性值school.name,从vue的实例上找出这个变量所代表的值
fn(node, value) // 这里一执行,那么input输入框的值就变为school.name所代表的值了
node.addEventListener('input', (e) => { // 解析v-model的指令,监听input输入框,当值改变后,我们拿到新增去改变vm.$data对应变量的值
let value = e.target.value
this.setValue(vm, expr, value)
})
},
html() { // 24.处理v-html的方法 node.innerHTML = xxx
},
getContentValue(vm, expr) {
// 遍历表达式,将内容重新替换成一个完整的内容,返还回去
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1])
})
},
text(node, expr, vm) { // 24.处理{{}}语法的方法
let fn = this.updater['textUpdater']
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// console.log(args[1])
new Watcher(vm, args[1], () => {
fn(node, this.getContentValue(vm, expr)) // 返回了一个新的字符串
})
return this.getVal(vm, args[1])
})
fn(node, content)
},
updater: {
modelUpdater(node, value) { // 26.处理v-model的方法,给输入框赋予value属性 node.value = xxx
node.value = value
},
htmlUpdater() {
},
textUpdater(node, value) {// 27.处理{{}}语法
node.textContent = value // 这样能让对应节点的内容为编译后的内容
}
}
}
// 4.创建一个专门进行编译替换的类,将数据和页面的指令等等编译替换
class Compiler {
constructor(el, vm) { //5.这里的el可能就是个字符串如#app,也有可能是元素document.getElementById('app'),所以我们要进行判断
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// console.log(this.el)
// 7. 然后我们就可以进行替换,但是这样有个问题,我们如果直接替换的话,那就是挨个去找,找到个语法替换个语法,这样页面就一直刷新,这样不行,
// 所以我们可以把内容全部放在内存中去,当全部替换之后在放到页面上
// 我们可以把当前的元素节点,如这里的#app下所有的内容放到内存中去,我们就定义个存放到内存中的方法
let fragment = this.node2fragment(this.el)
// 13. 然后我们就可以把节点中的内容进行替换,编辑模板,用数据编译,然后把内容塞在页面中,这就是我们接来下需要做的,慢慢来
// 14. 进行编译模板,用我们创建vue实例时传入的数据去编译,this.compile(fragment)
this.compile(fragment)
// 把内容在塞到页面中,注意: 这一步是在上面所有模板编译完成之后进行的
this.el.appendChild(fragment)
}
// 22.判断这个属性是不是v-开头的方法
isDirective(attrName) {
return attrName.startsWith('v-') // 该方法判断一个字符串是否在另一个字符串的头部,返回一个布尔值,true表示存在,es6的新东西
}
// 18. 创建编译节点和文本的方法compileElement(child)和compileText(child)
// 19. 编译元素的方法
compileElement(node) {
let attributes = node.attributes; // 20.拿到元素的属性,看上面有没有v-开头的指令
// console.log(node, attributes);
[...attributes].forEach(attr => {
let { name, value } = attr
if (this.isDirective(name)) { // 21.判断这个属性是不是v-开头的指令this.isDirective()
// console.log(name)
let [, directive] = name.split('-') // directive这里就是model
// 22. 我们在这里找到v-开头的属性和19步的compileText方法找到了{{}}语法的内容,那么我们就需要
// 在一个统一的地方来分别用不同的方法来处理,CompileUtil
CompileUtil[directive](node, value, this.vm) // 调用这个方法,来处理不同的指令, value这里就是school.name,这this.vm上的data有我们school.name代表的值
}
})
}
// 19. 编译文本的方法
compileText(node) {
let content = node.textContent // node.textContent可以拿到当前节点的文本内容
// console.log(content)
if (/\{\{(.+?)\}\}/.test(content)) { // 判断文本节点中内容是否包含{{ xxx }} {{ aaa }}
// console.log(content)
CompileUtil['text'](node, content, this.vm)
}
}
//15. 创建用来编译内容中的dom节点的方法
compile(node) {
let childNodes = node.childNodes; // 16.这里拿到的是第一层的节点,就是第一层元素和空白节点,不包括子节点下的孙子节点
// console.log(childNodes) // [text, input, text, div, text, div, text, ul, text]
// 17. 判断节点是不是元素节点
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
// console.log('element', child)
this.compileElement(child)
// 如果是元素的话,就需要把自己传进去,再去遍历自己的子节点
this.compile(child)
} else {
// console.log('text', child) // 这里要处理空白节点是因为我们可能直接在#app下写个{{ school.name}}
this.compileText(child)
}
})
}
// 8.创建个方法把app下的所有内容都放在内存中去
node2fragment(node) {
let fragment = document.createDocumentFragment(); // 9.这是创建一个文档碎片
let firstChild
while(firstChild = node.firstChild) {
// 10.fragment有个方法appendChild,具有移动性,能把东西放在内存中去,放一个到内存中,那么页面就少一个
fragment.appendChild(firstChild) // 11. 每放一个firstChild,那么页面上就少个节点,那么下个节点就又变成firstChild,这样循环下去,就
//把app下的所有节点全部放在页面中去了,那么此刻页面上就没有内容了。
}
// 12.当所有节点都被放在内存中之后,循环终止,那么就把内容中的fragment文档碎片返回出去
// console.log(fragment)
return fragment
}
// 6. 判断是不是元素节点
isElementNode(node) {
return node.nodeType === 1; // 如果一个节点的nodeType为1,那么就是DOM元素
}
}
//1.创建一个基类,Vue
class Vue {
constructor(options) {
this.$el = options.el // 2.将创建的vue实例的时候传入的数据传入到options中
this.$data = options.data
let computed = options.computed // 先不管计算属性,建议看完了编译渲染和数据劫持在回看computed
//3.如果传入了el,则我们需要进行将内容进行编译,将数据和页面的东西编译在一起,所以创建一个专门进行编译的类
if (this.$el) {
new Observer(this.$data) // 29.回到这里,我们要把this.$data的数据全部转换成Object.defineProperty来定义,我们定义一个Observer类
// console.log(this.$data)
for (let key in computed) { // 先不管计算属性,建议看完了编译渲染和数据劫持在回看computed
Object.defineProperty(this.$data, key, { // 给this.$data上添加了个属性,这里为getNewName
get: () => {
return computed[key].call(this)
}
})
}
this.proxyVm(this.$data) // 这个地方可以后面在看,只是做代理,让vm.school就相当于vm.$data.school
new Compiler(this.$el, this) // 3.创建一个专门进行编译的类
}
}
proxyVm(data) {
for (let key in data) { // { school: {name, age } }
Object.defineProperty(this, key, { // this就是实例vm,这里如key就是school,让vm多了个属性是school,值就是$data.school的值
get() {
return data[key]
}
})
}
}
}