手写一个简易的vue框架
一、前言
由于本人使用vue技术比较多,有一天看到有些博客和视频写了一些对源码的理解和解析,正好自己有些空闲时间,便尝试一下自己在对vue有一些使用后的,对vue的理解,特此写此博客作为学习记录
二、数据劫持
看过vue2的官方文档就应该知道,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter,通过这种数据劫持的方式来对原数据进行修改和监控,因此我们先了解如果像vue一样对数据进行劫持
function Monitor(data) {
if (!data || typeof data !== 'object') return;//当数据不存在或不是对象时就返回
for (let key in data) {
let value = data[key];//这里用let是确保每个value是独立的
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`get ${value}`);
return value;
},
set: function (val) {
console.log(`change ${value}=>${val}`);
value = val;
}
})
if (typeof value === 'object') {
Monitor(value);
}
}
}
var Data = {
name: "jxm",
age: 18,
money: ['10W', '20W', '30W'],
};
Monitor(Data)
console.log(Data.name)
Data.name = 'jza'
Data.money[0] = 4
对以上代码做简单的解释说明:通过一个Monitor
函数,我们将一个数据传入并使用Object.defineProperty
来对数据进行修改,当检测到数据内部的数据类型还是对象类型时,则继续对数据进行劫持,直到数据不再是对象类型;以下是代码执行结果:
三、观察者模式
在使用vue时,我们会发现当我们修改了一个data
的内容时,页面会自动更新,这便是观察者模式的应用,一个典型的观察者模式应用场景:用户在一个网站订阅了主题,多个用户都可以去订阅主题,当主题发生变化时会通知用户更新主题。
class Subject {
constructor() {
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
var index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(observer=> {
observer.update()
})
}
}
class Observer{
constructor() {
this.update = function() {}
}
subscribeTo(subject) {
subject.addObserver(this)
}
}
let subject = new Subject()//创建主题
let observer = new Observer()//创建观察者
observer.update = function() {
console.log('observer update')
}
observer.subscribeTo(subject) //观察者订阅主题
subject.notify()//主题更新通知观察者
对以上代码进行简单的解释:如果对es6不熟悉,请先自己去学习es6的语法,声明一个Subject
表示主题observers
表示订阅主题的观察者数组,在声明添加、删除和通知的常用方法,再声明一个Observer
表示观察者,定义一个方法表示主动订阅主题,之后再进行相关的new操作,我们可以实现一个简单的观察者模式。
四、MVVM单向绑定
MVVM(Model-View-ViewModel)是一种将数据与UI分离的设计模式,M是model表示数据,比如一个用户账号的信息(名字、头像等),V表示视图,是与用户交流的桥梁,viewModel表示数据转化器,将model的信息转化为view的信息,或将view的命令传递到model。
假设有如下图代码,data里的name会和视图中的{{name}}
产生映射,修改data的值会直接引起视图中的数据变化,那么应该怎么进行书写呢
回想一下我们之前接触的数据劫持和观察者模式,我们该怎么实现上述代码呢?
- 谁是观察者呢?
- 谁是主题呢?
- 观察者何时订阅主题呢?
- 主题何时通知更新呢?
答:认真分析了之后我们可以明确,观察者是视图中的
{{name}}
,主题是data中的name
,观察者应该在mvvm初始化的时候去解析模板发现{{name}}
的时候就去订阅主题,当data.name
发生变化时,则通知观察者进行更新,所以再一开始劫持数据的时候,当用户再set
函数里面就可以调用主题的subject.notify
因此我们可以完成一个初步代码:
步骤一
function Monitor(data) {
if (!data || typeof data !== "object") return
for (let key in data) {
let value = data[key]
let subject = new Subject();//data里每个数据都是一个主题
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(` get ${value}`);
return value;
},
set: function (val) {
console.log(`change ${value}=>${val}`);
value = val;
subject.notify()//一旦当修改了数据主题就通知所有的观察者进行更新
}
})
if (typeof value === 'object') {
Monitor(value)
}
}
}
let id = 0;
class Subject {
constructor() {
this.id = id++
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(function (observer) {
observer.update()
})
}
}
class Observer {
constructor() {
}
update() {
console.log("...update");
}
subscribeTo(subject) {
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
}
}
对以上代码进行简单的解释:我们可以借鉴之前学习数据劫持的代码,根据之前的分析,我们劫持的数据,每一个都相当于是一个主题,观察者都需要去订阅对吧,所以在我们劫持数据的循环里写入let subject = new Subject()
没有任何问题对吧,由于劫持后的数据进行set后,也就是修改了之前的数据,主题应该通知观察者对吧,所以写入代码subject.notify()
,其余的代码,我们都可以借鉴之前学习观察者模式的代码对吧,接下来我们慢慢进行完善。
步骤二
我们还没有创建我们的mvvm的函数对吧,一进来我们就进行了new mvvm
的操作里面包含了挂载哪个dom元素以及数据data,所以在mvvm中应该包含哪些基本的步骤呢,1、数据劫持 2、对dom进行编译(你需要获取到dom元素中的样子长什么样子对吧,之后再将诸如{{name}}
的东西替换成数据劫持后对应的name)所以有了如下代码:
class mvvm {
constructor(opts) {
this.init(opts)
Monitor(this.$data)
this.compile()
}
init(opts) {
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
compile() {
this.traverse(this.$el)
}
traverse(node) {
if (node.nodeType === 1) {//是元素节点
node.childNodes.forEach(childNode => {
this.traverse(childNode)
})
} else if (node.nodeType === 3) {//文本节点
this.renderText(node)
}
}
renderText(node) {
let reg = /{{(.+?)}}/g
let match
while (match = reg.exec(node.nodeValue)) {
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.$data[key])
//在这里创建观察者
new Observer()
}
}
}
对以上代码进行简单的解释:我们会new mvvm
,所以在初始化的时候会获取dom,并且将数据放在其实例上,之后再对其进行数据劫持,劫持完之后才进行模板编译并渲染对吧,这个顺序不能有问题,因为我们在渲染数据时比如:{{name}} is {{age}}
发现一个花括号的内容就得进行一次主题订阅对吧,如果没有先劫持数据,对数据进行有响应式的修改,我们发起的订阅就无效了。
步骤三
到这里我们已经实现了将dom元素里面的花括号内容替换成了对应的数据,但是还无法实现修改数据后反应在视图上,因为我们还没有实现订阅,当我们遇到对应的模板时,我们进行了new Observer
的操作,所以我们需要对class Observer
进行内容补充,认真分析,当我们更新数据后,我们的观察者也是要更新对应数据的,所以在new Observer
至少应该包含一个更新的回调函数,还需要什么呢?还需要一个vm实例对吧,我们劫持的数据在这儿上面对吧,我们需要拿着对应的key去找对应的数据对吧,所以一共需要三个参数,分别时vm的实列,key还有一个回调函数,所以class mvvm
的renderText
方法完整的创建观察者如下:
new Observer(this, key, function (val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
我们的class Observer
怎么书写呢?
class Observer {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.getValue();
}
update() {
console.log("...update");
}
subscribeTo(subject) {
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
}
getValue() {
currentObserver = this
let value = this.vm.$data[this.key]
currentObserver = null;
}
}
应该是如上代码对吧,我们在new Observer
的时候就该去让当前对应的观察者去发起订阅主题,因此声明一个getValue
方法,将当前的观察者实例赋值给一个全局变量,通过这个全局变量去访问劫持的数据,到这里我们是不是该去修改数据劫持的get
方法之前的get只是一个单纯的访问后输出值和返回值,因此完整的get方法如下:
get: function () {
console.log(` get ${value}`);
if (currentObserver) {
console.log('has currentObserver');
currentObserver.subscribeTo(subject);
}
return value;
}
访问get确保当前是有一个观察者是全局最高观察者,我们就可以让当前观察者去订阅对应的主题。
步骤四
我们还有最后的更新方法没有实现了,
update() {
let oldValue = this.value;
let newValue = this.getValue();
if (newValue != oldValue) {
this.value = newValue;
this.cb.bind(this.vm)(newValue, oldValue)
}
}
mvvm单向绑定的最终版本
当然要记得添加id是app的dom结构,否则就没有用了。
function Monitor(data) {
if (!data || typeof data !== "object") return
for (let key in data) {
let value = data[key]
let subject = new Subject();//data里每个数据都是一个主题
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(` get ${value}`);
if (currentObserver) {
console.log('has currentObserver');
currentObserver.subscribeTo(subject);
}
return value;
},
set: function (val) {
console.log(`change ${value}=>${val}`);
value = val;
subject.notify()//一旦当修改了数据主题就通知所有的观察者进行更新
}
})
if (typeof value === 'object') {
Monitor(value)
}
}
}
let id = 0;
let currentObserver = null;
class Subject {
constructor() {
this.id = id++
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(function (observer) {
observer.update()
})
}
}
class Observer {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.getValue();
}
update() {
let oldValue = this.value;
let newValue = this.getValue();
if (newValue != oldValue) {
this.value = newValue;
this.cb.bind(this.vm)(newValue, oldValue)
}
}
subscribeTo(subject) {
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
}
getValue() {
currentObserver = this
let value = this.vm.$data[this.key]
currentObserver = null;
return value;
}
}
class mvvm {
constructor(opts) {
this.init(opts)
Monitor(this.$data)
this.compile()
}
init(opts) {
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
compile() {
this.traverse(this.$el)
}
traverse(node) {
if (node.nodeType === 1) {//是元素节点
node.childNodes.forEach(childNode => {
this.traverse(childNode)
})
} else if (node.nodeType === 3) {//文本节点
this.renderText(node)
}
}
renderText(node) {
let reg = /{{(.+?)}}/g
let match
while (match = reg.exec(node.nodeValue)) {
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.$data[key])
//在这里创建观察者
new Observer(this, key, function (val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
}
let vm = new mvvm({
el: '#app',
data: {
name: 'jxm',
age: 18
}
})
五、MVVM双向绑定
假设dom结构发生了变化如下图:
我们该怎么实现这种双向绑定呢?其实就是在编译的时候对模板进行区分,有花括号的是一种编译方式,有
v-model
是另外一种命令,由于在class mvvm
上再添加方法会显得比较臃肿所以我们再声明一个class Compile
,修改后大致代码如下:
这是mvvm的代码,看起来就比较简洁先进行初始化,之后就劫持数据,然后new Compile(this)
为什么要传一个this
呢,这个this
实际上是vm实例,这上面有我们劫持后的数据以及相关内容。
class mvvm {
constructor(opts) {
this.init(opts)
Monitor(this.$data)
new Compile(this)
}
init(opts) {
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
}
以下代码是class Compile
的内容,将原本放在mvvm的方法迁移到这儿,则需要做出一些调整:
class Compile {
constructor(vm) {
this.vm = vm;
this.node = vm.$el
this.compile();
}
compile() {
this.traverse(this.node)
}
traverse(node) {
if (node.nodeType === 1) {//是元素节点
node.childNodes.forEach(childNode => {
this.traverse(childNode)
})
} else if (node.nodeType === 3) {//文本节点
this.renderText(node)
}
}
renderText(node) {
let reg = /{{(.+?)}}/g
let match
while (match = reg.exec(node.nodeValue)) {
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key])
//在这里创建观察者
new Observer(this.vm, key, function (val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
}
将传进来的vm实例和dom放到其自身的vm上方便我们进行操作,这个没有问题对吧,在renderText
方法的内部node.nodeValue.replace(raw, this.$data[key])
变为node.nodeValue.replace(raw, this.vm.$data[key])
,以前是在mvvm内部,this就是vm实例,现在则需要改变一下,下方的new Observer
也是同样的意思,接下来我们应该思考在我们traverse
遍历节点的时候遇到input
节点怎么处理的问题,所以可以在class Compile
中写如下一个方法:
renderNode(node) {
let attr = [...node.attributes]//将节点的属性转化成一个数组
attr.forEach(item => {
if (this.isDirective(item.name)) {
let key = item.value;
node.value = this.vm.$data[key];
new Observer(this.vm, key, function (newVal) {
node.value = newVal
})
node.oninput = (e) => {
this.vm.$data[key] = e.target.value;
}
}
})
}
//判断属性名是否是指令
isDirective(attrName) {
return attrName === 'v-model'
}
上图是将节点属性转化为一个数组输出后的内容,所以当使用方法判断属性含有
v-model
的时候,我们给input输入框的值可以赋值为this.vm.$data[key];
,vm实例上去找对应的数据赋值,同时,我们也得创建观察者,因为编译到这儿发现input框,为了实现响应必须得new Observer
然后去订阅相关主题,同时注册一个输入框事件oninput
,当输入完成就将输入的值赋值给vm实例上被劫持修改过得数据,这样便实现了从view
视图更新内容响应到数据的过程。
六、完结撒花!最终的所有代码
<!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>手写一个简易的vue框架</title>
</head>
<body>
<div>手写一个简易的vue框架</div>
<div id="app">
<input v-model="name" type="text">
<h1>{{name}} 's age is {{age}}</h1>
</div>
<script>
function Monitor(data) {
if (!data || typeof data !== "object") return
for (let key in data) {
let value = data[key]
let subject = new Subject();//data里每个数据都是一个主题
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(` get ${value}`);
if (currentObserver) {
console.log('has currentObserver');
currentObserver.subscribeTo(subject);
}
return value;
},
set: function (val) {
console.log(`change ${value}=>${val}`);
value = val;
subject.notify()//一旦当修改了数据主题就通知所有的观察者进行更新
}
})
if (typeof value === 'object') {
Monitor(value)
}
}
}
let currentObserver = null;
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(function (observer) {
observer.update()
})
}
}
class Observer {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.getValue();
}
update() {
let oldValue = this.value;
let newValue = this.getValue();
if (newValue != oldValue) {
this.value = newValue;
this.cb.bind(this.vm)(newValue, oldValue)
}
}
subscribeTo(subject) {
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
}
getValue() {
currentObserver = this
let value = this.vm.$data[this.key]
currentObserver = null;
return value;
}
}
class Compile {
constructor(vm) {
this.vm = vm;
this.node = vm.$el
this.compile();
}
compile() {
this.traverse(this.node)
}
traverse(node) {
if (node.nodeType === 1) {//是元素节点
this.renderNode(node)//解析节点上的属性
node.childNodes.forEach(childNode => {
this.traverse(childNode)
})
} else if (node.nodeType === 3) {//文本节点
this.renderText(node)
}
}
renderNode(node) {
let attr = [...node.attributes]//将节点的属性转化成一个数组
console.log("attr----", attr);
attr.forEach(item => {
if (this.isDirective(item.name)) {
let key = item.value;
node.value = this.vm.$data[key];
new Observer(this.vm, key, function (newVal) {
node.value = newVal
})
node.oninput = (e) => {
this.vm.$data[key] = e.target.value;
}
}
})
}
renderText(node) {
let reg = /{{(.+?)}}/g
let match
while (match = reg.exec(node.nodeValue)) {
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key])
//在这里创建观察者
new Observer(this.vm, key, function (val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
//判断属性名是否是指令
isDirective(attrName) {
return attrName === 'v-model'
}
}
class mvvm {
constructor(opts) {
this.init(opts)
Monitor(this.$data)
new Compile(this)
}
init(opts) {
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
}
let vm = new mvvm({
el: '#app',
data: {
name: 'jxm',
age: 18
}
})
</script>
</body>
</html>
来个结果动图展示吧!
转载自:https://juejin.cn/post/7179993840458154039