上一篇:实现一个简单版的vueRouter【history模式+hsah模式】
模拟实现的有vm实例对象中的data、$options、把data中的成员注入到vm实例对象中
实现模拟vue响应式的整体结构图:
- Vue
把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter - Observer
能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep - Compiler
解析每个元素中的指令/插值表达式,并替换成相应的数据 - Dep
添加观察者(watcher),当数据变化通知所有观察者 - Watcher
数据变化更新视图
项目的目录
1、vue.js文件内实现的功能
负责接收初始化的参数(选项)
负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
负责调用 observer 监听 data 中所有属性的变化
负责调用 compiler 解析指令/插值表达式
代码如下:
class Vue {
constructor(options) {
// 1、保存vue实例传递过来的数据
this.$options = options // options是vue实例传递过来的对象
this.$data = options.data // 传递过来的data数据
// el 是字符串还是对象
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2、把this.$data中的成员转换成getter 和setter ,并且注入到vue实例中,使vue实例中有data里面的属性
// 但是this.$data自身内部成员并没有实现在自身内部属性的gettter和setter,需要通过observer对象来实现
this._proxyData(this.$data)
// 3、调用observer对象,监视data数据的变化
new Observer(this.$data)
// 4、调用compiler对象,解析指令和差值表达式
new Compiler(this) // this是vue实例对象
}
_proxyData (data) {
// 遍历传递过来的data对象的数据,key是data对象中的属性名
Object.keys(data).forEach((key) => {
// 使用js的Object.defineProperty(),把数据注入到vue实例中,this就是vue实例
Object.defineProperty(this, key, {
configurable: true, // 可修改
enumerable: true, // 可遍历
// get 是 Object.defineProperty()内置的方法
get () {
return data[key]
},
// set 是 Object.defineProperty()内置的方法
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
2、oberver.js文件实现的功能
负责把 data 选项中的属性转换成响应式数据
data 中的某个属性也是对象,把该属性转换成响应式数据
数据变化发送通知
代码如下:
/**
* Observer类:作用是把data对象里面的所有属性转换成getter和setter
* data 是创建vue实例的时候,传递过来的对象里面的data,data也是个对象
*/
class Observer {
// constructor 是创建实例的时候,立刻自动执行的
constructor(data) {
this.walk(data)
}
// 遍历data对象的所有属性
// data 是创建vue实例的时候,传递过来的对象里面的data,data也是个对象
walk (data) {
// 判断data是否是对象
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
})
}
// 把data对象里面的所有属性转换成getter和setter
defineReactive (obj, key, val) {
// 解决this的指向问题
let that = this
// 为data中的每一个属性,创建dev对象,用来收集依赖和发送通知
// 收集依赖:就是保存观察者
let dep = new Dep()
// 如果val也是对象,就把val内部的属性也转换成响应式数据,
/// 也就是调用Object.defineProperty()的getter和setter
that.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// Dep.target就是观察者对象,调用dev对象的addSub方法,把观察者保存在dev对象内
// target是Dep类的静态属性,但是却是在Watcher类中声明的
Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {
if (newValue === val) {
return
}
val = newValue
// 对vue实例初始化后,传入的data数据的值进行修改,由字符串变成对象
// 也要把新赋值的对象内部的属性,转成响应式
that.walk(newValue)
// data里面的数据发生了变化,调用dev对象的notify方法,通知观察者去更新视图
dep.notify()
}
})
}
}
3、compiler.js文件实现的功能
负责编译模板,解析指令/插值表达式
负责页面的首次渲染
当数据变化后重新渲染视图
代码如下:
/**
* 主要就是用来操作dom
* 负责编译模板,解析指令/插值表达式
* 负责页面的首次渲染
* 当数据变化后重新渲染视图
*/
class Compiler {
constructor(vm) {
this.el = vm.$el // vue实例下的模板
this.vm = vm // vm就是vue实例
this.compile(this.el) // compiler实例对象创建后,会立即调用这个方法
}
// 编译模板,处理文本节点和元素节点
compile (el) {
let childNodes = el.childNodes // 是个伪数组
Array.from(childNodes).forEach((node) => {
if (this.isTextNode(node)) {
// 编译文本节点,处理差值表达式{{}}
this.compileText(node)
} else if (this.isElementNode(node)) {
// 编译元素节点,处理指令
this.compileElement(node)
}
// 递归调用compile,把所有的子节点都处理掉,也就是嵌套的节点都处理掉
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令,这里只处理v-text和v-model
compileElement (node) {
// console.dir(node.attributes)
Array.from(node.attributes).forEach((attr) => {
// console.log(attr.name)
let attrName = attr.name // 指令属性名 v-model\v-text\type\v-count
// 判断是否是vue指令
if (this.isDirective(attrName)) {
// v-text ==> text
attrName = attrName.substr(2) // text\model\on:click\html
let key = attr.value // 指令属性值 // msg\count\text\clickBtn()
// 处理v-on指令
if (attrName.startsWith('on')) {
const event = attrName.replace('on:', ''); // 获取事件名
// 事件更新
this.onUpdater(node, key, event);
} else {
this.update(node, key, attrName);
}
}
})
}
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater'] // textUpdater(){} 或者 modelUpdater(){}
// this 是compiler对象
updateFn && updateFn.call(this, node, this.vm[key], key) // updateFn的名字存在才会执行后面的函数
}
// 处理v-text指令
textUpdater (node, value, key) {
// console.log(node)
node.textContent = value
// 创建watcher对象,当数据改变去更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-html指令
htmlUpdater (node, value, key) {
// console.log(node)
node.innerHTML = value
// 创建watcher对象,当数据改变去更新视图
// this.vm: vue的实例对象 key:data中的属性名称 ()=>{}: 回调函数,负责更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-model指令
modelUpdater (node, value, key) {
// console.log(node, value)
node.value = value
// console.log(node.value)
// 创建watcher对象,当数据改变去更新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向数据绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 处理v-on指令
onUpdater (node, key, event) {
// console.log(node ,key, event)
node.addEventListener(event, () => {
// 判断函数名称是否有()
if (key.indexOf('(') > 0 && key.indexOf(')') > 0) {
this.vm.$options.methods[key.slice(0,-2)]()
} else {
this.vm.$options.methods[key]()
}
})
}
// 编译文本节点,处理差值表达式{{ msg }}
compileText (node) {
// console.dir(node)
let reg = /\{\{(.+?)\}\}/
let value = node.textContent // 获取文本节点内容:{{ msg }}
if (reg.test(value)) {
let key = RegExp.$1.trim() // 把差值表达式{{ msg }}中的msg提取出来
// 把{{ msg }}替换成 msg对应的值,this.vm[key] 是vue实例对象内的msg
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变去更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是vue指令
isDirective (attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点(元素节点1\属性节点2\文本节点3)
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点(元素节点1\属性节点2\文本节点3)
isElementNode (node) {
return node.nodeType === 1
}
}
4、dep.js文件实现的功能 Dep(Dependency)
收集依赖,添加观察者(watcher)
通知所有观察者
代码如下:
/**
* 收集依赖,添加所有观察者(watcher)
* 通知观察者,数据发生改变了,去更新视图,通过watcher对象里面的update方法来实现
*/
class Dep {
constructor() {
this.subs = [] // 保存所有的观察者
}
// 把观察者添加到this.subs中去
addSub (sub) {
// 约定,每一个观察者对象中,必须要有一个update方法
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知给观察者,告诉观察者,数据发生改变了,你要去更新视图
notify () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
5、watcher.js文件实现的功能 Dep(Dependency)
当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
自身实例化的时候往 dep 对象中添加自己
代码如下:
/**
* 当data数据发生变化,dep对象中的notify方法内通知所有的watcher对象,去更新视图
* Watcher类自身实例化的时候,向dep对象中addSub方法中添加自己(1、2)
*/
class Watcher {
constructor(vm, key, cb) {
this.vm = vm // vue的实例对象
this.key = key // data中的属性名称
this.cb = cb // 回调函数,负责更新视图
// 1、把watcher对象记录到Dev这个类中的target属性中
Dep.target = this // this 就是通过Watcher类实例化后的对象,也就是watcher对象
// 2、触发observer对象中的get方法,在get方法内会调用dep对象中的addSub方法
// this.oldValue = this.vm[this.key] //更新之前的页面数据
// Dep.target = null
this.oldValue = vm[key] //更新之前的页面数据
Dep.target = null
}
// 当data中的数据发生变化的时候,去更新视图
update () {
const newValue = this.vm[this.key]
if (newValue === this.oldValue) {
return
}
this.cb(newValue)
}
}
6、index.html文件内容:
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Mini Vue</title>
</head>
<body>
<div id="app">
<h1>标题:差值表达式</h1>
<h3>{{ msg }}</h3>
<h3>{{ count }}</h3>
<h1>标题:v-text</h1>
<div v-text="msg"></div>
<h1>标题:v-model</h1>
<input type="text" v-model="msg">
<input type="text" v-model="count">
<h1>标题:v-html 指令</h1>
<div v-html="count"></div>
<h1>标题:v-on 指令</h1>
<button v-on:click="clickBtn">点击触发v-on命令方法名不带()</button>
<button v-on:click="clickBtn2()">点击触发v-on命令方法名带()</button>
</div>
<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue1',
count: 100,
person: { name: 'zs' },
dog: {},
},
methods: {
clickBtn() {
alert('改变了页面msg的值')
vm.msg = 'clickBtn'
},
clickBtn2() {
alert('改变了页面count的值')
vm.count = '1000000000'
}
}
})
// vm.dog.name = 'dog'
// vm.msg = { test: 'Hello' }
// console.log(vm.msg)
</script>
</body>
</html>
7、直接打开index.html文件就可以查看效果
总结
问题一:给属性重新赋值成对象,是否是响应式的? 是
问题二:给 Vue 实例新增一个成员是否是响应式的? 不是
通过下图回顾整体流程:
-
Vue 记录传入的选项,设置 el
把 data 的成员注入到 Vue 实例
负责调用 Observer 实现数据响应式处理(数据劫持)
负责调用 Compiler 编译指令/插值表达式等 -
Observer 数据劫持
-----负责把 data 中的成员转换成 getter/setter
-----负责把多层属性转换成 getter/setter
如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
添加 Dep 和 Watcher 的依赖关系
数据变化发送通知 -
Compiler 负责编译模板,解析指令/插值表达式
负责页面的首次渲染过程
当数据变化后重新渲染 -
Dep 收集依赖,添加订阅者(watcher)
通知所有订阅者 -
Watcher 自身实例化的时候往dep对象中添加自己
当数据变化dep通知所有的 Watcher 实例更新视图