模拟 Vue.js响应式原理
- 基本概念
- 数据驱动
- 响应式的核心原理
- Vue 2.x
- Vue 3.x
- Object.defineProperty和Proxy的区别
- 发布订阅模式和观察者模式
- 发布订阅者模式
- 模拟Vue自定义事件的实现(发布/订阅的实现)
- 观察者模式
- 模拟观察者模式实现
- 总结
- Vue响应式原理模拟-分析
- Vue基本结构
- 整体流程
- Vue构造函数的实现
- Vue构造函数options选项挂载到Vue实例上
- Observer
- defineReactive上=>为什么要传第三个参数
- defineReactive下
- 问题一: $data中有属性是对象
- 问题二: $data中属性重新赋值,且赋值为一个新的对象,那么这个对象的成员是否是响应式的呢
- Compiler(最重要的两个参数el:渲染的dom,vm:渲染所需要的数据)
- isDirective(attrName),isTextNode(node), isElementNode(node)三个判断方法
- compile(el)方法
- compileText (node)
- compileElement (node)
- Dep => 订阅者
- Watcher => 观察者
- 创建Watcher对象1
- 创建Watcher对象2
- v-text指令
- v-model指令
- 双向数据绑定
- Vue响应式原理代码
基本概念
- 数据驱动
- 响应式的核心原理
- 发布订阅模式和观察者模式
数据驱动
- 数据响应式,双向绑定,数据驱动
- 数据响应式
1.1 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率 - 双向绑定
2.1 数据改变,视图改变;视图改变,数据也随之改变
2.2 我们可以使用v-model在表单元素上创建双向绑定 - 数据驱动,Vue最独特的特性之一
3.1 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
响应式的核心原理
Vue 2.x
- 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter
- Vue2.x数据响应式实现原理是Object.defineProperty,数据劫持set get
Vue 3.x
- Vue3.x数据响应式实现原理是Proxy
Object.defineProperty和Proxy的区别
- Object.defineProperty代理的是单个属性,如果要代理整个对象,那么要对整个对象的属性做循环遍历
- Proxy代理的是整个目标对象(
更优
)
发布订阅模式和观察者模式
发布订阅者模式
- 订阅者
- 发布者
- 信号中心
- 我们规定,存在一个信号中心,某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscrible)这个信号,从而知道什么时候自己可以开始执行
- 实例:Vue中的自定义事件=>兄弟组件之间的通信过程
- vm:信号中心
- vm.$on=>订阅
- vm.$emit=>发布
// eventBus.js
// 信号中心
let eventHub = new Vue()
// {"click":[fn1,fn2],"change":[fn]} // new Vue实例中有一个对象
// componentA.vue
// 发布者
addTodo: function () {
// 发布消息(事件)
eventHub.$emit("add-todo", { text: this.newTodoText })
this.newTodoText = ""
}
// componentB.vue
// 订阅者
cteated: function () {
// 订阅消息(事件)
eventHub.$on("add-todo", this.addTodo)
}
模拟Vue自定义事件的实现(发布/订阅的实现)
- 基本概念
- $on=>注册事件,在vm(信号中心)中定义一个变量对象存贮这些注册的事件及事件处理函数
1.1 键: 事件名称
1.2 值: 事件处理函数
1.3 可以注册多个事件名称,也可以给同一个事件添加多个处理函数 - $emit =>执行对应事件的处理函数
- 信号中心(类)有 o n 和 on和 on和emit方法,还有一个对象存储$on注册的事件
- 核心代码
class EventEmitter {
constructor() {
// {"click":[fn1,fn2],"change":[fn]}
this.subs = {};
}
// 注册事件
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || [];
this.subs[eventType].push(handler);
}
// 触发事件
$emit(eventType) {
this.subs[eventType].forEach((handler) => {
handler();
});
}
}
- 测试
let em = new EventEmitter();
// 为同一个事件名称添加多个处理函数
em.$on("click", () => {
console.log("click1");
});
em.$on("click", () => {
console.log("click2");
});
em.$emit("click");
// click1
// click2
观察者模式
- 观察者(订阅者)–watcher
- update(): 当事件发生时,具体要做的事情(
可以更新视图或者其他的操作
)
- update(): 当事件发生时,具体要做的事情(
- 目标(发布者)–Dep (depdence依赖)
- subs 数组: 存储所有的观察者
- addSub(): 添加观察者
- notify(): 当事件发生,调用
所有观察者
的update()方法 - 原理:
观察者模式没有信号中心,而是将订阅者(观察者)添加到发布者中,让发布者记录下所有的订阅者
模拟观察者模式实现
- 核心代码
// 发布者-目标
class Dep {
constructor() {
// 记录所有的订阅者(观察者)watcher
this.subs = [];
}
addSub(sub) {
// 传递的对象必须有update方法,我们才能将该对象当成观察者
if (sub.update) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
// 订阅者-观察者
class Watcher {
update() {
console.log("update");
}
}
- 测试
let dep = new Dep();
let watcher = new Watcher();
// 将订阅者添加到发布者中,让发布者记录下所有的订阅者
dep.addSub(watcher);
// 通知所有的订阅者,调用订阅者的update()方法
dep.notify(); // update
总结
-
观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的
-
发布/订阅者模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在,减少发布者和订阅者之间的依赖
Vue响应式原理模拟-分析
Vue基本结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
</div>
</body>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
},
});
console.log(vm);
</script>
</html>
- 我们观察打印的vm数据结构
-
Vue构造函数内部是将data中的成员转换成get/set注入到Vue实例上,
1.1 目的:在其他地方使用的时候我们可以直接通过this.name和this.age来
使用,比较方便
-
Vue构造函数中的data选项成员记录到了$date中,并且转换成getter/setter
2.1 目的 $data中的setter是真正监视数据变化的地方
2.2 $data和_data指向的是同一个对象,_开头的是私有成员,$开头的是公共成员
-
把Vue构造函数的参数记录到Vue实例的$options中
-
$el对应Vue构造函数选项中的el,可以是选择器或者dom对象
整体流程
Vue构造函数的实现
- 功能
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/插值表达式
- 结构
- $option
- $el
- $data
- _proxyData();私有方法,将Vue构造函数data选项转换成getter/setter注入到Vue实例的$data中
Vue构造函数options选项挂载到Vue实例上
- Vue构造函数核心代码 vue.js
class Vue {
constructor(options) {
// 1:通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
// 2:把data中的成员转换成getter和setter,注入到Vue实例中
this._proxyDate(this.$data)
// 3:调用observer对象,监听数据的变化
// 4:调用compiler对象,解析指令和插值表达式
}
_proxyDate (data) {
// 遍历data中的所有属性,因为真正监控数据变化触发视图更新是$data的职责,这里只要Vue数据发生变化,通过set告诉了$data,就可以,所以Vm上的属性如果是对象,不需要对象的成员也是响应式的
Object.keys(data).forEach(key => {
// 把data的属性注入到Vue实例中
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get () {
return data[key]
},
set (newValue) {
// newValue如果是对象,通知到$data就好,$data内部处理
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
- Vue实例化对象
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
},
});
console.log(vm);
Observer
-
功能
- 负责把data选项中的属性转换成响应式数据=>将$data中的数据转换成getter和setter
- data中的某个属性也是对象,把该属性也转换成响应式数据
- 数据变化发送通知(调用数据的set方法时候发送通知)=>观察者模式来实现
-
结构:两个方法
- walk(data) 遍历data中所有属性
- defineReactive(data,key,value) 定义响应式数据
-
Observer构造函数核心代码 observer.js
class Observer {
constructor(data) {
this.walk(data)
}
walk (data) {
// 1:判断data是否是对象
if (!data || typeof data !== "object") {
return
}
// 2:遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
return val
},
set (newValue) {
if (newValue === val) {
return
}
val = newValue
}
})
}
}
- 在vue.js中调用Observer=>将$data中的数据转换成getter和setter
class Vue {
constructor(options) {
// 1:通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
// 2:把data中的成员转换成getter和setter,注入到Vue实例中
this._proxyDate(this.$data)
// 3:调用observer对象,监听数据的变化
new Observer(this.$data)
// 4:调用compiler对象,解析指令和插值表达式
}
_proxyDate (data) {
// 遍历data中的所有属性
Object.keys(data).forEach(key => {
// 把data的属性注入到Vue实例中
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get () {
return data[key]
},
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
- 在index.html中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./observer.js"></script>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
},
});
console.log(vm);
</script>
</body>
</html>
- 现象
- this.defineReactive(data, key, data[key])有前两个值就够了,为什么还要传第三个值呢?后面解释
defineReactive上=>为什么要传第三个参数
-
为什么defineReactive 要传第三个参数val=>现在解释,当我们不传递第三个参数参数的时候
-
我们在控制台打印vm.name,实际的流程是
- this.name=>this.$data.name
- this.$data.name=>val
defineReactive下
问题一: $data中有属性是对象
- 之前的name:“szy” age:20 data中的name和age属性都是原始值,这些属性是响应式的,那么data中有属性是对象的时候,这个对象里面的属性是否是响应式的呢
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
// person属性是对象
person: {
name: "zjl",
age: 30,
},
},
});
console.log(vm);
- d a t a . p e r s o n 中 的 n a m e 和 a g e 没 有 对 应 的 g e t t e r 和 s e t t e r , 所 以 data.person中的name和age没有对应的getter和setter,所以 data.person中的name和age没有对应的getter和setter,所以data.person中的name和age不是响应式的,但我们调用defineReactive方法是将对象中所有的属性都转换成响应式的
defineReactive (obj, key, val) {
// 如果Val是对象,把Val内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
return val
},
set (newValue) {
if (newValue === val) {
return
}
val = newValue
}
})
问题二: $data中属性重新赋值,且赋值为一个新的对象,那么这个对象的成员是否是响应式的呢
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
person: {
name: "zjl",
age: 30,
},
},
});
// 重新赋值,那么test这个属性是否是响应式的呢?
vm.name= { test: "hello" };
console.log(vm);
- 改进:新增的属性进行响应式处理
defineReactive (obj, key, val) {
const that = this
// 如果Val是对象,把Val内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
return val
},
set (newValue) {
if (newValue === val) {
return
}
// 新增的属性也要进行处理变成响应式的
that.walk(newValue)
val = newValue
}
})
- 完整代码
class Observer {
constructor(data) {
this.walk(data)
}
walk (data) {
// 1:判断data是否是对象
if (!data || typeof data !== "object") {
return
}
// 2:遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive (obj, key, val) {
const that = this
// 如果Val是对象,把Val内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
return val
},
set (newValue) {
if (newValue === val) {
return
}
// 新增的属性也要进行处理变成响应式的
that.walk(newValue)
val = newValue
}
})
}
}
Compiler(最重要的两个参数el:渲染的dom,vm:渲染所需要的数据)
-
功能:
操作虚拟dom,为了方便演示,我们下面的实例中直接操作dom
- 负责翻译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据发生变化后重新渲染视图
-
结构
- 属性
- el: 模板对象
- vm: Vue实例
- 方法:下面方法都是在操作dom,当我们把所有的dom操作写在一个方法里,代码量会非常多,所以需要把dom操作的代码进行拆分,方便以后的维护
- compile(el) : 遍历 dom 对象的所有节点(node),并且判断节点类型,如果是文本节点,则解析插值表达式,如果是元素节点,则解析指令
- compileElement(node) : 编译元素节点,处理指令
- compileText(node) : 编译文本节点,处理插值表达式
- isDirective(attrName) : 判断当前属性是否是指令,在compileElement(node) 方法中调用
- isTextNode(node) : 判断当前节点是否是文本节点 nodeType===3
- isElementNode(node) ; 判断当前节点是否是元素节点 nodeType===1
- 属性
-
创建Compiler类 => compiler.js
isDirective(attrName),isTextNode(node), isElementNode(node)三个判断方法
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
}
// 编译模板,处理文本节点和元素节点
compile (el) {
}
// 编译元素节点,处理指令
compileElement (node) {
}
// 编译文本节点,处理插值表达式
compileText (node) {
}
// 判断元素属性是否是指令
isDirective (attrName) {
// 因为在vue中,所有的指令都是以v-开头
return attrName.startsWith("v-")
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}
compile(el)方法
- 遍历 dom 对象的所有节点(node),并且判断节点类型,如果是文本节点,则解析插值表达式,如果是元素节点,则解析指令
// 编译模板,处理文本节点和元素节点
compile (el) {
// 因为最终el会整体替换,只对它的子节点做处理,不对el本身处理,所以处理的是el.childNodes
const childNodes = el.childNodes // children是元素节点
// childNodes是伪元素
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
compileText (node)
- 编译文本节点,处理插值表达式
- 在compiler.js中打印文本节点
constructor(vm) {
this.el = vm.$el
this.vm = vm
// 调用this.compile方法
this.compile(this.el)
}
// 编译文本节点,处理插值表达式
compileText (node) {
// 以对象的形式打印
console.dir(node)
}
- 在index.html中导入compiler.js类
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<h1>插值表达式</h1>
<h3>{{name}}</h3>
<h3>{{age}}</h3>
<h1>v-text</h1>
<div v-text="name"></div>
<h1>v-model</h1>
<input type="text" v-model="name" />
<input type="text" v-model="age" />
</div>
<script src="./compiler.js"></script>
<script src="./observer.js"></script>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
person: {
name: "zjl",
age: 30,
},
},
});
// 重新赋值,那么test这个属性是否是响应式的呢?
vm.name = { test: "hello" };
console.log(vm);
</script>
</body>
</html>
- 在Vue类中调用compiler对象
class Vue {
constructor(options) {
// 1:通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
// 2:把data中的成员转换成getter和setter,注入到Vue实例中
this._proxyDate(this.$data)
// 3:调用observer对象,监听数据的变化
new Observer(this.$data)
// 4:调用compiler对象,解析指令和插值表达式
new Compiler(this)
}
_proxyDate (data) {
// 遍历data中的所有属性
Object.keys(data).forEach(key => {
// 把data的属性注入到Vue实例中
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get () {
return data[key]
},
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
- 页面显示内容
- 控制台打印内容,通过textContent判断文本节点的类型(换行,普通文本,插值表达式)
- 正则匹配-替换
// 编译文本节点,处理插值表达式
compileText (node) {
// console.dir(node)
// 匹配,提取插值 以 {{ name }} 为例
// node.textContent不仅能获取还能赋值
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim() //去除前后空格,此时的key的值就是 "name"
node.textContent = value.replace(reg, this.vm[key])
// 为什么不能用node.textContent=this.vm[key],因为插值表达式中可能有其他非变量字符
}
}
compileElement (node)
- 编译元素节点,处理指令(此处我们以v-text和v-model为例)
- 打印所有的属性节点node.attributes,观察结构
// 编译元素节点,处理指令
compileElement (node) {
// 获取所有的属性节点
console.log(node.attributes)
}
- 核心代码
// 编译元素节点,处理指令
compileElement (node) {
// 获取所有的属性节点
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
// 属性节点的属性名
attrName = attrName.substr(2)
// 属性节点的值也是vm对象的属性(key)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
/*
因为下面的方法中的value是vm中属性对象的值,所以需要key,
不同的指令需要不同的处理函数,所以需要attrName
*/
let updateFn = this[attrName + "Updater"]
//updateFn有值(对应指令的处理函数存在)才调用
updateFn && updateFn(node, this.vm[key])
}
// 处理v-text指令
textUpdater (node, value) {
node.textContent = value
}
// 处理v-model指令
modelUpdater (node, value) {
// 该节点必须是表单元素
node.value = value
}
- 引入update函数的好处,以后添加一个自定义指令,添加其对应的处理方法即可,不影响其他的模块
- 完整代码
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile (el) {
const childNodes = el.childNodes // children是元素节点
// childNodes是伪元素
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// 获取所有的属性节点
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
// 属性节点的属性名
attrName = attrName.substr(2)
// 属性节点的值也是vm对象的属性(key)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
/*
因为下面的方法中的value是vm中属性对象的值,所以需要key,
不同的指令需要不同的处理函数,所以需要attrName
*/
let updateFn = this[attrName + "Updater"]
//updateFn有值(对应指令的处理函数存在)才调用
updateFn && updateFn(node, this.vm[key])
}
// 处理v-text指令
textUpdater (node, value) {
node.textContent = value
}
// 处理v-modle指令
modelUpdater (node, value) {
// 该节点必须是表单元素
node.value = value
}
// 编译文本节点,处理插值表达式
compileText (node) {
// console.dir(node)
// 匹配,提取插值
// node.textContent不仅能获取还能赋值
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
}
}
// 判断元素属性是否是指令
isDirective (attrName) {
// 因为在vue中,所有的指令都是以v-开头
return attrName.startsWith("v-")
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}
Dep => 订阅者
-
功能
- 收集依赖,添加观察者(Watcher)
- 通知所有观察者
-
结构
- subs : 数组,存储Dep中所有的watcher(观察者)
- addsub(sub) : 添加watcher(观察者)
- notify() : 当数据发生变化的时候通知所有的观察者
-
创建 Dep类 => dep.js
class Dep {
constructor() {
// 存储所有观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
// 判断sub是否是观察者(具有update方法)
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
// 遍历subs中所有的观察者,调用没有个观察者的update方法来更新视图
this.subs.forEach(sub => {
sub.update()
})
}
}
- 核心原理:为每一个响应式数据创建一个dep对象,在使用响应式数据的时候,收集依赖(创建观察者对象),在数据发生变化的时候,通着每一个观察者,调用观察者的update()方法来更新视图
- 因为Observer类中创建了响应式数据($data中的每个成员都有getter和setter),在Observer类中使用Dep
defineReactive (obj, key, val) {
/*
为$date中的每一个成员属性创建一个dep对象,在get方法中收集依赖(调用dep的addsub()方法),
在set方法中发送通知(调用dep的notify()方法)
*/
// 负责收集依赖,并发送通知
const dep = new Dep
const that = this
this.walk(val)
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
// Dep.target为观察者对象
Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {
if (newValue === val) {
return
}
that.walk(newValue)
val = newValue
// 发送通知
dep.notify()
}
})
}
- 上面只实现了Dep(订阅者),观察者Watcher(观察者)还没实现,下面实现Watcher
Watcher => 观察者
-
功能
- 当数据发生变化的时候,dep通知所有的Watcher实例更新视图
自身实例化的时候往dep对象中添加自己
-
结构
- vm : Vue实例
- key : Sdata中的成员属性
- cb :不同的watcher对象更新视图的时候所做的事情是不一样的,所以需要回调函数(cb),指明如何更新视图
- oldValue : update()方法中可以获取到newValue,
两者相等,不更新视图
,有了vm和key,可以得到oldValue - update() : 更新视图
-
创建Watcher类 => watcher.js
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中属性的名称
this.key = key
// 回到函数负责视图更新
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
this.oldValue = vm[key] // 此处因为用到了$date属性触发了该属性的get方法,此时将该wtacher实例添加到了dep的subs依赖数组中,完成了添加watcher(观察者)的功能
// 防止重复添加watcher,将Dep.target的指针指向空
Dep.target = null
}
update () {
const newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
// 将变化的属性传到cb中,局部更新
this.cb(newValue)
}
}
- 下面介绍何时创建Watcher的实例化对象
创建Watcher对象1
- watcher实例的update方法(更新视图)实际上是在操作dom,而dom的操作是在Compiler类中完成的
- 更新视图的操作实际上就是在操作dom,而我们所有的dom操作,都在Compiler中,在Compiler我们要找到把数据渲染到dom的位置,也就是处理指令(compileElement)和插值表达式(compileText)的位置
- 指令和插值表达式都是依赖数据的,而所有视图中依赖数据的位置我们都要创建一个watcher对象,当数据发生改变的时候,dep对象会通知所有的watcher对象去重新渲染视图,所以我们要在compileText,textUpdater,modelUpdater方法中创建watcher对象
- 首先在compileText()方法中添加watcher
// 编译文本节点,处理插值表达式
compileText (node) {
// console.dir(node)
// 匹配,提取插值
// node.textContent不仅能获取还能赋值
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据发生变化更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
- 在index.html中导入依赖,因为Watcher类中使用了Dep类,所以dep.js在前,且注意注释掉
vm.name = { test: "hello" };
影响代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<h1>插值表达式</h1>
<h3>{{name}}</h3>
<h3>{{age}}</h3>
<h1>v-text</h1>
<div v-text="name"></div>
<h1>v-modle</h1>
<input type="text" v-model="name" />
<input type="text" v-model="age" />
</div>
<script src="./dep.js"></script>
<script src="./watcher.js"></script>
<script src="./compiler.js"></script>
<script src="./observer.js"></script>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
person: {
name: "zjl",
age: 30,
},
},
});
// 重新赋值,那么test这个属性是否是响应式的呢?
// vm.name = { test: "hello" };
console.log(vm);
</script>
</body>
</html>
- 观察视图
创建Watcher对象2
- 在指令中创建watcher对象
v-text指令
// 处理v-text指令
textUpdater (node, value) {
node.textContent = value
new watcher(this.vm, key, (newvalue) => {
node.textContent = newvalue
})
}
- 但是我们在textUpdater函数中没有key变量,z在update方法中把key变量传过来
update (node, key, attrName) {
/*
因为下面的方法中的value是vm中属性对象的值,所以需要key,
不同的指令需要不同的处理函数,所以需要attrName
*/
let updateFn = this[attrName + "Updater"]
//updateFn有值(对应指令的处理函数存在)才调用
updateFn && updateFn(node, this.vm[key],key) //新增key变量
}
- textUpdater内部this指向问题,因为textUpdater在update中是直接调用的,所以内部的this指向window=>call()方法
update (node, key, attrName) {
/*
因为下面的方法中的value是vm中属性对象的值,所以需要key,
不同的指令需要不同的处理函数,所以需要attrName
*/
let updateFn = this[attrName + "Updater"]
//updateFn有值(对应指令的处理函数存在)才调用
updateFn && updateFn.call(this, node, this.vm[key], key)
}
v-model指令
// 处理v-modle指令
modelUpdater (node, value, key) {
// 该节点必须是表单元素
node.value = value
new Watcher(this.vm, key, (newvalue) => {
node.value = newvalue
})
}
- 上面只是实现了数据发生变化,视图更新,但还没实现视图发生变化,数据更新
- 接下的双向数据绑定处理(视图发生变化,数据更新)
双向数据绑定
-概念
-
数据发生变化,更新视图
-
视图发生变化,更新数据
-
原理:当视图发生变化的时候,触发了元素的input事件,给Sdata绑定的元素注册input事件
// 处理v-modle指令
modelUpdater (node, value, key) {
// 该节点必须是表单元素
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener("input", () => {
this.vn[key] = node.value
})
}
Vue响应式原理代码
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<h1>插值表达式</h1>
<h3>{{name}}</h3>
<h3>{{age}}</h3>
<h1>v-text</h1>
<div v-text="name"></div>
<h1>v-modle</h1>
<input type="text" v-model="name" />
<input type="text" v-model="age" />
</div>
<script src="./dep.js"></script>
<script src="./watcher.js"></script>
<script src="./compiler.js"></script>
<script src="./observer.js"></script>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "szy",
age: 20,
person: {
name: "zjl",
age: 30,
},
},
});
// 重新赋值,那么test这个属性是否是响应式的呢?
// vm.name = { test: "hello" };
console.log(vm);
</script>
</body>
</html>
- dep.js
class Dep {
constructor() {
// 存储所有观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
// 判断sub是否是观察者(具有update方法)
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
// 遍历subs中所有的观察者,调用没有个观察者的update方法来更新视图
this.subs.forEach(sub => {
sub.update()
})
}
}
- watcher.js
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中属性的名称
this.key = key
// 回到函数负责视图更新
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
this.oldValue = vm[key] // 此处因为用到了$date属性触发了该属性的get方法,此时将该wtacher实例添加到了dep的subs依赖数组中,完成了添加watcher(观察者)的功能
// 防止重复添加watcher
Dep.target = null
}
update () {
const newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
// 将变化的属性传到cb中,局部更新
this.cb(newValue)
}
}
- compiler.js
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile (el) {
const childNodes = el.childNodes // children是元素节点
// childNodes是伪元素
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// 获取所有的属性节点
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
// 属性节点的属性名
attrName = attrName.substr(2)
// 属性节点的值也是vm对象的属性(key)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
/*
因为下面的方法中的value是vm中属性对象的值,所以需要key,
不同的指令需要不同的处理函数,所以需要attrName
*/
let updateFn = this[attrName + "Updater"]
//updateFn有值(对应指令的处理函数存在)才调用
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 处理v-text指令
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-modle指令
modelUpdater (node, value, key) {
// 该节点必须是表单元素
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener("input", () => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理插值表达式
compileText (node) {
// console.dir(node)
// 匹配,提取插值
// node.textContent不仅能获取还能赋值
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据发生变化更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令
isDirective (attrName) {
// 因为在vue中,所有的指令都是以v-开头
return attrName.startsWith("v-")
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}
- observer.js
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile (el) {
const childNodes = el.childNodes // children是元素节点
// childNodes是伪元素
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// 获取所有的属性节点
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text 方便为每一个指令定义一个处理函数,减少if条件判断
// 属性节点的属性名
attrName = attrName.substr(2)
// 属性节点的值也是vm对象的属性(key)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
/*
因为下面的方法中的value是vm中属性对象的值,所以需要key,
不同的指令需要不同的处理函数,所以需要attrName
*/
let updateFn = this[attrName + "Updater"]
//updateFn有值(对应指令的处理函数存在)才调用
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 处理v-text指令
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-modle指令
modelUpdater (node, value, key) {
// 该节点必须是表单元素
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener("input", () => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理插值表达式
compileText (node) {
// console.dir(node)
// 匹配,提取插值
// node.textContent不仅能获取还能赋值
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据发生变化更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令
isDirective (attrName) {
// 因为在vue中,所有的指令都是以v-开头
return attrName.startsWith("v-")
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}
- vue.js
class Vue {
constructor(options) {
// 1:通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
// 2:把data中的成员转换成getter和setter,注入到Vue实例中
this._proxyDate(this.$data)
// 3:调用observer对象,监听数据的变化
new Observer(this.$data)
// 4:调用compiler对象,解析指令和插值表达式
new Compiler(this)
}
_proxyDate (data) {
// 遍历data中的所有属性
Object.keys(data).forEach(key => {
// 把data的属性注入到Vue实例中
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get () {
return data[key]
},
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}