准备工作(前置知识):
Vue中经常出现3个词:数据响应式、双向绑定、数据驱动。
数据响应式中的“数据”,指的是数据模型。
基于Vue开发时,数据模型就是普通的 JavaScript 对象。
数据响应式的核心是:当修改数据时,试图会自动进行更新,避免了繁琐的 DOM 操作,提高开发效率。
对比JQuery,JQuery的使用就是进行DOM操作。
双向绑定指的是:当数据发生改变,视图会跟着改变;当视图发生改变,数据也随之改变。
双向绑定的概念中,包含了数据响应式。
因为双向绑定包含视图变化,所以它针对的是可以和用户进行交互的表单元素。
可以使用v-model在表单元素上创建双向数据绑定。
数据驱动就是一种开发的过程。
它指的是:开发过程中只需要关注数据本身(即业务本身),不需要关心数据是如何渲染到视图(DOM)上的。
它是 MVVM框架 (如Vue) 最独特的特性之一,因为主流的MVVM框架内部已经实现了 数据响应式 和 双向绑定。
Vue 2.x 和 Vue 3.0 实现数据响应式的方式不同。
Vue2.x 的响应式原理基于ES5的 Object.defineProperty 实现的
官方文档
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项, Vue会遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。 Object.defineProperty 是 ES5 中一个无法 shim(降级处理)的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
MDN - Object.defineProperty
shim指的是可以使用es[x]-shim使低版本浏览器可以使用es[x]的新特性。
一些特性无法shim,例如ES5的Object.defineProperty和ES6的Proxy
数据劫持:访问或修改对象的某个属性时,除了执行基本的数据获取和修改操作之外,还基于数据的操作行为,以数据为基础去执行额外的操作。
vue的数据劫持:当访问或设置 vue 实例的成员的时候,做一些干预操作。
例如修改 vue 实例成员的值,将新的值渲染到DOM,整个DOM操作不希望在赋值的时候手动去做,所以需要使用数据劫持。
具体通过Object.defineProperty方法向vue实例对象中添加具有get/set描述符的成员属性。
语法:Object.definePorperty(obj, prop, descriptor)
当访问属性时调用get(getter访问器)方法,当修改属性值时,调用set(setter设置器)方法。
<input type="text" oninput="inputHandle(event)" />
<div id="app">
hello
div>
<script>
// 表单输入事件,用于测试修改vm的属性,是否实现双向绑定
function inputHandle(e) {
vm.msg = e.target.value // 触发set
console.log(vm.msg) // 触发get
}
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello'
}
// 模拟 Vue 实例
let vm = {}
// 数据劫持:当访问或设置 vm 中的成员的时候,做一些干预操作
Object.defineProperty(vm, 'msg', {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以delete删除,可以通过 defineProperty 重新定义)
configurable: true,
// 访问器:当获取值时执行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 设置器:当设置值时执行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
// 更新数据的值
data.msg = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data.msg
}
})
script>
当一个对象拥有多个属性,使用Object.defineProperty实现对这个对象的数据劫持,需要遍历对象中的每一个属性,为它们添加getter/setter。
可通过Object.keys获取所有属性,然后遍历。
msg:<input type="text" oninput="inputHandle(event, 'msg')" />
count:<input type="text" oninput="inputHandle(event, 'count')" />
<div id="app">
<span class="msg">hellospan>
<span class="count">10span>
div>
<script>
// 表单输入事件,用于测试修改vm的属性,是否实现双向绑定
function inputHandle(e, key) {
vm[key] = e.target.value // 触发set
console.log(vm[key]) // 触发get
}
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 10
}
// 模拟 Vue 实例
let vm = {}
// 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('get: ', key, data[key])
return data[key]
},
// 设置器:当设置值时执行
set (newValue) {
console.log('set: ', key, newValue)
if (newValue === data[key]) {
return
}
// 更新数据的值
data[key] = newValue
// 数据更改,更新 DOM 的值
document.querySelector(`.${key}`).textContent = data[key]
}
})
})
script>
Vue 3.0的响应式(数据劫持)是基于ES6新增的Proxy(代理对象)实现的。
MDN - Proxy
Proxy 监听的是对象,而非属性。
因此在把多个属性转化成getter或setter时,不需要循环遍历对象的全部属性。
Proxy是ES6新增,且不能被polyfill磨平,无法shim,所以IE不支持,性能比Object.defineProperty高,速度快。
Proxy是一个类,通过new创建一个代理对象。
new Proxy(target, handler)
访问和修改,操作的都是代理对象。
Proxy构造函数接收两个参数:
拦截器:执行代理行为的函数,Proxy有13种拦截器。
当访问代理对象的属性时,执行get拦截器。
当修改代理对象的属性时,执行set拦截器。
msg:<input type="text" oninput="inputHandle(event, 'msg')" />
count:<input type="text" oninput="inputHandle(event, 'count')" />
<div id="app">
<span class="msg">hellospan>
<span class="count">10span>
div>
<script>
// 表单输入事件,用于测试修改vm的属性,是否实现双向绑定
function inputHandle(e, key) {
vm[key] = e.target.value // 触发set
console.log(vm[key]) // 触发get
}
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 10
}
// 模拟 Vue 实例
let vm = new Proxy(data, {
// 拦截器:执行代理行为的函数
// 访问代理对象(vm)的属性时执行
get (target, key) {
console.log('get: ', key, target[key])
return target[key]
},
// 修改代理对象(vm)的属性时执行
set (target, key, newValue) {
console.log('set: ', key, newValue)
if (newValue === target[key]) {
return
}
// 更新数据的值
target[key] = newValue
// 数据更改,更新 DOM 的值
document.querySelector(`.${key}`).textContent = newValue
}
})
script>
可以看到,Proxy设置数据劫持,比Object.defineProperty简洁的多,并且由于Proxy监听的是整个对象,所以对每个属性的访问修改,都会触发相应的拦截器,省去了遍历的工作。
发布/订阅模式 和 观察者模式 是两种设计模式。
在Vue中有各自的应用场景。
两种模式的本质是相同的,它们经常被混为一谈,但是二者是有区别的。
Wrong:观察者模式还有一个名字叫发布-订阅模式。
我们假定,存在一个“信号中心”。
某个任务执行完成,就向信号中心“发布”(publish)一个信号。
其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。
这就叫做“发布/订阅模式”(publish-subscribe pattern)
举例:
老婆喜欢看BL网站的小说,作者更新的慢,每天都要打开网页查看是否更新了。
有一天网站升级了一个功能,可以让用户订阅小说的更新。
当作者发布新章节后,网站会给订阅了这个小说的用户发送短信提醒。
这样老婆只需要当收到短信后追更即可,剩下更多的时间留给刷微博、逛淘宝。
这个例子中,老婆就是【订阅者】,小说作者就是【发布者】,BL网站就是【信号中心】。
Vue 中的自定义事件 以及 node 中的事件机制 都是基于发布/订阅模式.
官方文档参考自定义事件如何使用:$dispatch 和 $broadcast 替换
$on
方法注册(订阅)自定义事件
$emit
方法触发(发布)事件$off
方法取消注册(订阅)事件var vm = new Vue()
// 注册/订阅事件
vm.$on('dataChange', () => {
console.log('dosomething')
})
vm.$on('dataChange', () => {
console.log('dosomething2')
})
// 触发/发布事件
vm.$emit('dataChange')
通过Vue兄弟组件的通信过程,更清晰的认识 订阅者、发布者、信号中心(事件中心)。
// eventBus.js
// 事件中心 / 信息中心
let eventHub = new Vue()
// ComponentA.vue
// 发布者
methods: {
// 发布一条待办消息
addTodo: function () {
// 发布消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
}
// ComponentB.vue
// 发布者
created: function () {
// 订阅消息(事件)
eventHub.$on('add-todo', this.renderTodoText)
},
methods: {
// 把消息渲染到界面中
renderTodoText(newTodoText) {
// ...
}
}
首先分析Vue自定义事件如何实现:
$on
注册事件(订阅消息)
$on
仅仅注册事件,事件处理函数并不立即执行{ 'click': [fn1, fn2], 'change': [fn3] }
$emit
触发事件(发布消息)
$emit
接收的第一个参数是事件的名称// 事件触发器
class EventEmitter {
constructor () {
// subs 存储事件及处理函数
// { 'click': [fn1, fn2], 'change': [fn3] }
// this.subs = {}
// 使用Object.create(null)创建的对象没有原型属性
// 因为subs只需要存储键值对形式的数据,不需要原型
// 使用Object.create(null)可以提高性能
this.subs = Object.create(null)
}
// 注册事件
$on (eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 触发事件
$emit (eventType, ...args) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler.apply(this, args)
})
}
}
}
// 测试
// 创建一个事件中心
let em = new EventEmitter()
// 订阅
em.$on('click', msg => {
console.log('click1', msg)
})
em.$on('click', msg => {
console.log('click2', msg)
})
// 发布
em.$emit('click', '触发事件')
// click1 触发事件
// click2 触发事件
观察者模式 和 发布订阅模式 的区别是:
观察者模式:定义对象间一种一对多的依赖(Dependency)关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
下面模拟一个观察者模式(未考虑update传参)
// 发布者 - 目标
// Vue 响应式机制中内部使用的“Dep”命名
class Dep {
constructor () {
// 记录所有的订阅者
this.subs = []
}
// 添加订阅者
addSub (sub) {
// 确保这是一个拥有update方法的订阅者对象
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 订阅者 - 观察者
class Wathcer {
// 当事件发生时,由发布者调用update方法
// update内部可以更新视图或做一些其他操作
update () {
console.log('update')
}
}
// 测试
let dep = new Dep()
let watcher = new Wathcer()
let watcher2 = new Wathcer()
dep.addSub(watcher)
dep.addSub(watcher2)
dep.notify()
当目标对象数据发生变化(事件发生)时,目标对象(发布者)会调用它的notify方法。
notify方法会通知所有的观察者(订阅者),调用观察者(订阅者)的update方法,处理各自的业务。
所以如果对目标对象的变化有兴趣,就要调用目标对象的addSub方法,把自己订阅到目标对象里。
目标对象内部记录了所有的观察者。
目标对象(发布者)和观察者(订阅者)之间存在相互依赖的关系。
发布订阅模式中多了一个**“事件中心”**。
通过**“事件中心”**隔离了 发布者 和 订阅者。
结合兄弟组件的传值来理解,假设发布者 和 订阅者 分别是两个不相关的组件(发布者:组件A;订阅者:组件B)。
组件A的作用是添加待办事项,组件B的作用是把新增的待办事项渲染到页面。
当组件A中新增了一个待办事项,会发布一个事件(命名为add)。
此时会调用 事件中心 的 $emit 方法,触发add事件。
$emit 方法中会找到事件中心中注册的add事件对应的处理函数并执行。
而事件处理函数是由组件B提供的。
组件B想要知道add事件是否发生了变化,就需要通过$on方法订阅事件中心的add事件。
事件中心的作用是 隔离订阅者和发布者,去除它们之间的依赖。
这里模拟一个最小版本的Vue。
准备工作:
<div id="app">
<h1>插值表达式h1>
<h3>{{ msg }}h3>
<h3>{{ count }}h3>
<h1>指令h1>
<h2>v-texth2>
<div v-text="msg">div>
<h2>v-modelh2>
<input type="text" v-model="msg">
<input type="text" v-model="count">
div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 20
}
})
script>
以上是要实现的功能。
打印发现Vue实例中除了包含 msg 和 count 外,还包含它们对应的 getter(get msg、get count) 和 setter(set msg 、set count)。
这是通过Object.definePorperty设置了get和set的效果,打印它们的描述符:
// msg的描述符
{
configurable: true,
enumerable: true,
get: proxyGetter,
set: proxySetter
}
所以Vue构造函数内部需要把data中的成员转换成getter和setter 注入到Vue实例上。
这样做的目的是,在其他地方使用的时候,可以直接通过this.msg
和 this.count
使用。
接着看到data中的成员被记录到了$data属性中,并且也传换成了getter和setter。
// $data.msg的描述符
{
configurable: true,
enumerable: true,
get: reactiveGetter,
set: reactiveSetter
}
$data中的setter是真正监视数据变化的地方
$options可以简单认为把构造函数的参数记录到了这个属性中。
_data和$data指向的是同一个对象。
下划线_
开头的是私有成员,$
开头的是公共成员。
这里只需要模拟$data即可。
$el对应选项中的el设置的DOM对象。
设置el选项的时候,可以是一个选择器,也可以是一个DOM对象。
如果是一个选择器,Vue构造函数内部会把这个选择器转换成相应的DOM对象。
最小版本的Vue中要模拟vm(Vue实例)中的成员:
模拟的最小版本的Vue由下面5个类组成:
功能:
结构:
代码:
// js/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._proxyData(this.$data)
// 3. 调用 observer 对象,监听数据的变化
// 4. 调用 compiler 对象,解析指令和插值表达式
}
_proxyData (data) {
// 遍历 data 中的所有属性
// 注意遍历回调内部需要使用vue实例,所以这里使用箭头函数,使this指向vue实例
Object.keys(data).forEach(key => {
// 把 data 中的属性注入到 Vue 实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
if (data[key] === newValue) {
return
}
data[key] = newValue
}
})
})
}
}
<div id="app">
<h1>插值表达式h1>
<h3>{{ msg }}h3>
<h3>{{ count }}h3>
<h1>指令h1>
<h2>v-texth2>
<div v-text="msg">div>
<h2>v-modelh2>
<input type="text" v-model="msg">
<input type="text" v-model="count">
div>
<script src="./js/vue.js">script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 20
}
})
console.log(vm)
script>
功能(数据劫持):
结构:
Observer类中有两个方法(方法名与Vue源码中一致):
代码:
// js/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) {
// 注意获取和设置属性的值,使用的是参数 val
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// return obj[key] // 死递归
return val
},
set (newValue) {
if (val === newValue) {
return
}
val = newValue
// 发送通知
}
})
}
}
// js/vue.js
class Vue {
constructor (options) {
// ...
// 3. 调用 observer 对象,监听数据的变化
new Observer(this.$data)
// 4. 调用 compiler 对象,解析指令和插值表达式
}
// ...
}
<script src="./js/observer.js">script>
<script src="./js/vue.js">script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 20
}
})
console.log(vm.msg)
script>
为什么向defineReactive()传递一个val参数,并在getter/setter中使用它,而不是使用obj[key]?
这是因为当访问vm.msg
时:
_proxyData
方法转化的msg
属性的getter
方法。getter
方法最后return
的data[key]
,其中data
指向的this.$data
Observer
类中,defineReactive
方法转化this.$data
的属性时,定义的getter
方法。obj[key]
,此时obj
同样指向的this.$data
,就又会触发这个getter
方法。val
变量存储this.$data.msg
的值。defineReactive()方法中接收的value参数为什么没有在方法执行完后释放?
因为defineReactive
方法内部转化obj的属性时,设置了getter/setter方法,这些方法内部使用了val
,这样就行成了闭包,扩展了val
的作用域,所以val
不会被释放。
问题1:
当前定义的Observer只会将data中的属性转化成响应式数据(getter/setter)。
当data中的属性的值也是一个对象时,这个对象中的属性并没有被转换成响应式数据(getter/setter)。
所以需要修改一下defineReactive,使data中的对象类型的属性,内部也是响应式的。
只需要在一开始,调用一个walk,walk内部会判断如果属性是对象,就执行遍历转化。
问题2:
如果将data中的属性,重新赋值为一个对象,该对象内部的属性也应该是响应式的。
所以需要在触发this.$data中属性的setter方法时,调用walk方法转化新值,它会判断新的值是否是对象,如果是则转化。
问题总结:
// js/observer.js
defineReactive(obj, key, val) {
let that = this
// 如果 val 是对象,把 val 内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// return obj[key] // 死递归
return val
},
set (newValue) {
if (val === newValue) {
return
}
val = newValue
that.walk(newValue)
// 发送通知
}
})
}
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 20,
person: {
name: 'Tom',
info: {
age: 18
}
}
}
})
console.log(vm.person)
vm.msg = { test: 'Yeah' }
console.log(vm)
script>
功能(操作DOM):
结构:
当前模拟直接操作DOM,没有使用虚拟DOM。
属性:
方法:
一系列DOM操作的方法。
DOM操作:
代码:
// js/compiler.js
class Compiler {
constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compiler(this.el)
}
// 编译模板,处理文本节点和元素节点
compiler (el) {
let childNodes = el.childNodes
// childNodes是一个伪数组,通过Array.from将其转化为数组
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}else if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node)
}
// 判断如果有node有子节点,递归调用compiler编译子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compiler(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn(node, this.vm[key])
}
// 处理v-text指令
textUpdater (node, value) {
// 更新节点文本
node.textContent = value
}
// 处理v-model指令
modelUpdater (node, value) {
// 更新表单元素的值
node.value = value
}
// 编译文本节点,处理指令
compileText (node) {
// console.log(node)
// console.dir会将内容以对象形式打印
// console.dir(node)
// {{ msg }}
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) {
return attrName.startsWith('v-')
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
}
// js/vue.js
constructor (options) {
// ...
// 4. 调用 compiler 对象,解析指令和插值表达式
new Compiler(this)
}
Dep:目标 / 依赖 / 发布者
功能:
Dep类的作用是在getter方法中收集依赖。
每个响应式的属性,最终都会创建一个对应的Dep对象。
它负责收集所有依赖于该属性的地方。
所有依赖该属性的位置,都会创建一个Watacher对象。
所以Dep收集的就是依赖于该属性的Watcher对象。
setter方法中会通知依赖。
当属性发生变化,会调用Dep对象的notify发送通知,进而调用Watcher对象的update方法。
总结,Dep的作用就是:
结构:
代码:
// js/dep.js
class Dep {
constructor () {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
// js/observer.js
defineReactive(obj, key, val) {
let that = this
// 负责收集依赖,并发送通知
let dep = new Dep()
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 收集依赖
// Dep.target指向的是一个观察者,在实例化Watcher对象时定义这个静态属性
Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {
if (val === newValue) {
return
}
val = newValue
that.walk(newValue)
// 发送通知
dep.notify()
}
})
}
Dep类中并没有定义 target 这个静态属性,这个属性是在 Watcher类中定义的,它用来向dep对象的subs中添加观察者对象。
在Data属性的getter方法中,通过Dep对象收集依赖。
在Data属性的setter方法中,通过Dep对象触发依赖。
所以Data中的每个属性都要创建一个对应的Dep对象。
在收集依赖的时候,把依赖该数据的所有Watcher(观察者对象)添加到Dep对象的subs数组中。
在setter方法中,触发依赖(发送通知),会调用Dep的notify方法,通知所有关联的Watcher对象。
Watcher对象负责更新对应的视图。
功能:
结构:
代码:
// js/watcher.js
class Watcher {
constructor (vm, key, cb) {
// vue实例
this.vm = vm
// data中的属性名称
this.key = key
// 回调函数,负责更新视图
this.cb = cb
// 把 watcher对象记录到Dep类的静态属性target中
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
// 添加完后重置target,防止重复添加
Dep.target = null
}
// 当数据发生变化的时候,更新视图
update () {
// 调用update时数据已经发生变化,直接获取就是最新的值
let newValue = this.vm[this.key]
if (newValue === this.oldValue) {
return
}
this.cb(newValue)
}
}
Watcher的作用之一是,当数据改变的时候更新视图。
数据改变发送通知,是在Observer中的setter方法中通过调用dep对象的notify方法实现。
notify方法中会遍历所有的watcher对象,调用它们的update方法。
update内部是通过调用cb回调函数来更新视图的。
cb函数是在Watcher构造函数中传递的(创建watcher对象时)。
更新视图其实就是操作DOM,而所有的DOM操作都在Compiler中。
在Complier中找到把数据渲染到DOM的位置,即:
这3个方法都是最终把数据更新到DOM元素上。
这3个方法都是在页面首次加载的时候执行的。
指令和插值表达式都是依赖于数据的,而所有视图中依赖数据的位置,都应该创建一个watcher对象。
当数据发生改变的时候,dep对象会通知所有的watcher对象,重新渲染视图。
所以要在这3个方法中创建watcher对象。
调整代码:
// js/compiler.js
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater']
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-model指令
modelUpdater (node, value, key) {
// 更新表单元素的值
node.value = value
new Watcher(this.vm, key, newValue => {
node.value = newValue
})
}
// 编译文本节点,处理指令
compileText (node) {
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
})
}
}
调整的位置:
<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 Vue',
count: 20,
person: {
name: 'Tom',
info: {
age: 18
}
}
}
})
console.log(vm)
setTimeout(() => {
vm.msg = '变更后的msg'
}, 1000)
script>
以上代码实现了,改变vue的数据改变时更新视图。
但是更新表单元素的值,并没有更新绑定的vue中的数据,即双向绑定。
双向绑定机制包括两点:
实现方法:
当文本框内容发生变化时,触发一个事件(Vue中使用的是input事件)。
当input事件发生的时候,要把文本框的值取出来,重新赋给绑定的vm的属性。
也就是给包含v-model指令的文本框元素绑定input事件。
// js/compiler.js
// 处理v-model指令
modelUpdater (node, value, key) {
// 更新表单元素的值
node.value = value
new Watcher(this.vm, key, newValue => {
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
以上就实现了双向绑定:
至此模拟Vue的响应式原理的代码就结束了。
代码只是为了了解响应式机制和双向绑定的原理,所以只模拟了最简单的内容。
开发人员工具 - Sources-调试快捷键:
添加断点:
删除断点:
查看断点:
上面代码的实现中已知,当给一个vm中data的属性重新赋值为一个对象时,会触发这个属性的setter方法。
setter方法内部调用walk方法将新赋予的这个对象及对象下的属性转化为响应式。
但如果给vm添加一个新的属性时,这个属性并没有转化为响应式。
因为将vm中data的属性转化为响应式是在new初始化vue实例的时候,所以新增属性并不会被转化。
Vue官方文档 【检测变化的注意事项】提供了如果将新增的属性转化成响应式数据的方法。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。
但是,可以使用
Vue.set(object, propertyName, value)
方法向嵌套(下一级)对象添加响应式 property。
// 静态访问方法
Vue.set(vm.someObject, 'b', 2)
// or
// 实例方法
this.$set(this.someObject,'b',2)
可以推测到,Vue.set方法内部使用了Object.defineProperty将属性b
转换成了getter/setter。
通过下图回顾整体流程: