Vue进阶之响应式原理
数据驱动
- 数据响应式、双向绑定、数据驱动
- 数据响应式
- 数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
- 双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用 v-model 在表单元素上创建双向数据绑定
- 数据驱动是 Vue 最独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
响应式原理的简单演示
Vue2.x 利用Object.defineProperty
数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
<!DOCTYPE html>
<html lang="cn">
<head>
<title>defineProperty</title>
</head>
<body>
<div id="app">
hello
</div>
<script>
// 模拟 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
}
})
setTimeout(() => {
// 测试
vm.msg = 'Hello World'
}, 3000)
console.log(vm.msg)
</script>
</body>
</html>
Vue3.x 利用new Proxy(对象代理器)
<!DOCTYPE html>
<html lang="en">
<head>
<title>Proxy</title>
</head>
<body>
<div id="app">
hello
</div>
<script>
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 0
}
// 模拟 Vue 实例
let vm = new Proxy(data, {
// 执行代理行为的函数
// 当访问 vm 的成员会执行
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 当设置 vm 的成员会执行
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
setTimeout(()=>{
// 测试
vm.msg = 'Hello World'
},3000)
console.log(vm.msg)
</script>
</body>
</html>
模拟发布订阅模式
发布/订阅模式
- 订阅者 (在事件中心注册事件)
- 发布者 (发布事件)
- 信号中心或者事件中心
vue 的发布订阅模式
// Vue 自定义事件
// Vue 自定义事件
let vm = new Vue()
// 注册事件(订阅消息)
vm.$on('dataChange', () => {
console.log('dataChange')
})
vm.$on('dataChange', () => {
console.log('dataChange1')
})
// 触发事件(发布消息)
vm.$emit('dataChange')
模拟发布订阅
<!DOCTYPE html>
<html lang="en">
<head>
<title>发布订阅模式</title>
</head>
<body>
<script>
class EventCenter {
constructor() {
// {'click': [fn1,fn2], 'click2': [fn] }
this.subs = {} // 数据中心
}
$on(eventType, handler) { // 订阅者入口
this.subs[eventType] = this.subs[eventType] || [];
this.subs[eventType].push(handler);
}
$emit(eventType) { // 发布者入口
this.subs[eventType] && this.subs[eventType].forEach(handler => { handler()});
}
}
let vm = new EventCenter();
// 测试
vm.$on('dataChange', () => {
console.log('dataChange')
})
vm.$on('dataChange', () => {
console.log('dataChange1')
})
// 触发事件(发布消息)
vm.$emit('dataChange')
</script>
</body>
</html>
观察者模式
- 观察者(订阅者) – Watcher update():当事件发生时,具体要做的事情
- 目标(发布者) – Dep
- subs 数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生,调用所有观察者的 update() 方法
- 没有事件中心
简单的观察者模式
// 发布者 Dep
class Dep {
constructor() {
// 观察者列表
this.subs = []
}
// 添加观察者方法
addsubs(sub) {
sub && sub.update && this.subs.push(sub)
}
// 事件触发调用观察者
notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
// 观察者(订阅者)
class Watcher {
update() {
console.log('watcher_update')
}
}
let dep = new Dep();
let wat = new Watcher();
dep.addsubs(wat);
setTimeout(() => {
dep.notify();
},3000)
总结
- 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
- 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在,发布/订阅模式隔离了发布者和订阅者,使用更加灵活;
实现简单Vue
实现的几个模块儿
- Vue — 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
- Observer — 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
- Compiler — 解析每个元素中的指令/插值表达式,并替换成相应的数据
- Dep — 添加观察者(watcher),当数据变化通知所有观察者
- Watcher — 数据变化更新视图
首先Vue模块,功能
- 负责接收初始化的参数(选项);
- 负责把data中的属性注入到Vue实例中,转换成getter/setter;
- 负责调用 observer 监听 data 中所有属性的变化;
- 负责调用 compiler 解析指令/差值表达式
/* 类图
+ Vue
----------
+ $options
+ $el
+ $data
----------
- _proxyData() //代理data中的属性,转化为getter和setter,注入到vue实例中
*/
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) {
// 遍历获取属性,利用Vue实例做数据劫持
Object.keys(data).forEach(key => {
// 这里this就是Vue实例
Object.defineProperty(this, key, {
enumerable: true, //可遍历
configurable: true, //可枚举
get() {
return data[key]
},
set(newval) {
if(newval === data[key])return;
data[key] = newval
}
})
})
}
}
Observer模块 (数据劫持)
功能
- 负责把data选型中的属性转换成响应式数据,getter/setter
- data中的某个属性也是对象的话,也要把该属性转换成响应式数据;
- 数据变比后发送通知(观察者模式)
/*
Observer
--------------
+ walk(data) //遍历data中的值,并筒处理
+ defineReactive //给属性添加getter和setter
*/
class Observer{
constructor(data){
this.walk(data)
}
walk(data) {
// 1.判断数据类型兼容错误类型
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;
this.walk(val); //这里walk会判断是否对象,对象的话继续递归的往下遍历,这个设计很好。
Object.defineProperty(obj, key, {
enumerable: true, //可遍历
configurable: true, //可枚举
get() { // 注意这里不可使用obj[key], 使用obj[key]就是调用get方法会陷入死循环
// 收集依赖添加观察者
Dep.target && dep.addSub(Dep.target)
return val
},
set(newval) {
if(newval === val) return;
val = newval;
that.walk(newval) //当属性新赋的值对对象时,也要添加getter和setter方法
// 发送通知
}
})
}
}
Compiler模块 (DOM渲染)
功能
- 负责编译模板,解析指令/差值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
这里先简化不使用虚拟DOM
/*
Compiler
-------------
+ el
+ vm vue实例
-------------
+ compile(el) 变了所有节点
+ compileElement(node) 标签 - 解析指令
+ compileText(node) 文本 - 解析差值表达式
+ isDirective(attrName) 判断此属性是否为指令
+ isTextNode(node) 是否文本
+ isElementNode(node) 是否标签
*/
class Compiler{
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes;
childNodes.forEach(node => {
if(this.isTextNode(node)){ //文本
this.compileText(node)
}else if(this.isElementNode(node)){ //元素
this.compileElement(node)
}
// 判断node节点,是否有子节点,有就递归
if(node.childNodes && node.childNodes.length) this.compile(node);
});
}
// 编译元素节点,处理指令
compileElement(node) {
// console.dir(node.attributes);
// 遍历所有属性,判断是否是指令 node.attributes是伪数组需要使用Array.from转换一下
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if(this.isDirective(attrName)){
attrName = attrName.substr(2)
let key = attr.value
this.updata(node, key, attrName)
}
})
}
updata(node, key, attrName) {
let updatefn = this[attrName+'Updater']
updatefn && updatefn(node, this.vm[key])
}
//处理各种指令,各自去处理各个指令 v-text
textUpdater(node, val) {
node.textContent = val
}
//处理各种指令 v-model
modelUpdater(node, val) {
node.value = val
}
// 编译文本,处理差值表达式
compileText(node) {
// 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-')
}
// 判断是否文本
isTextNode(node) {
return node.nodeType === 3
}
// 判断是否标签
isElementNode(node) {
return node.nodeType === 1
}
}
Dep模块 (发布者)
功能
- 收集依赖,收集观察者(watcher)
- 通知所有观察者
/*
Dep
----------
+ subs
----------
+ addSub(sub)
+ notify()
*/
class Dep{
constructor() {
this.subs = []
}
// 添加观察者
addSub(sub) {
sub && sub.updata && this.subs.push(sub)
}
// 发布通知观察者
notify() {
this.subs.forEach(sub => {
sub.updata()
})
}
}
Watcher模块 (观察者)
功能
- 当时数据变化是触发依赖,dep通知所有watcher实例更新视图
- 自身实例化的时候添加到dep对象的watcher列表中
/*
Watcher
-----------
+ vm
+ key
+ cb
+ oldValue
-----------
+ upData
*/
class Watcher{
constructor(vm, key, cb) {
this.vm = vm // vue实例
this.key = key // 监听的data属性
this.cb = cb //回调函数负责跟新视图
// 将Watcher对象记录到Dep类的tatget静态属性中
Dep.target = this
// 触发get方法,在get方法中调用addsub
this.oldValue = vm[key] //触发get
// 设置完成后将target至为空
Dep.target = null
}
upData() { //数据变化跟新
let newValue = this.vm[this.key]
if(newValue === this.oldValue) return;
this.oldValue = newValue //替换oldValue
this.cb && this.cb(newValue)
}
}
Watcher模块的调用注意this指向
// Compiler.js
...
// 编译元素节点,处理指令
compileElement(node) {
// console.dir(node.attributes);
// 遍历所有属性,判断是否是指令 node.attributes是伪数组需要使用Array.from转换一下
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if(this.isDirective(attrName)){
attrName = attrName.substr(2)
let key = attr.value
this.updata(node, key, attrName)
}
})
}
updata(node, key, attrName) {
let updatefn = this[attrName+'Updater']
updatefn && updatefn.call(this, node, this.vm[key], key)
}
//处理各种指令,各自去处理各个指令 v-text
textUpdater(node, val, key) {
// 这里注意this,textUpdater是在updata中调用的,这里的this指向了undefined,所以调用时,要通过call传递this
node.textContent = val
// 创建Watcher对象,监听数据变化,跟新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
//处理各种指令 v-model
modelUpdater(node, val, key) {
// 这里注意this,modelUpdater是在updata中调用的,这里的this指向了undefined,所以调用时,要通过call传递this
node.value = val
// 创建Watcher对象,监听数据变化,跟新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
}
// 编译文本,处理差值表达式
compileText(node) {
// 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])
// 创建Watcher对象,监听数据变化,跟新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
...
双向数据绑定
监听v-model的数据变化,变化后赋值给data数据
在之前的基础之上
// Compiler.js
...
//处理各种指令 v-model
modelUpdater(node, val, key) {
// 这里注意this,modelUpdater是在updata中调用的,这里的this指向了undefined,所以调用时,要通过call传递this
node.value = val
// 创建Watcher对象,监听数据变化,跟新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue //这里不会触发input事件所以不会死循环
})
// 数据绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
...
技巧:去除所有断点,sources-> breakpoints->右键移除所有断点
总结
问题
给 Vue 实例新增一个成员是否是响应式的? 不是
给属性重新赋值成对象,是否是响应式的? 是的
- Vue
- 记录传入的选项,设置 $data/$el
- 把 data 的成员注入到 Vue 实例
- 负责调用 Observer 实现数据响应式处理(数据劫持)
- 负责调用 Compiler 编译指令/插值表达式等
- Observer 数据劫持
- 负责把 data 中的成员转换成 getter/setter
- 负责把多层属性转换成 getter/setter
- 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
- 添加 Dep 和 Watcher 的依赖关系
- 数据变化发送通知
- Compiler
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染过程
- 当数据变化后重新渲染
- Dep
- 收集依赖,添加订阅者(watcher)
- 通知所有订阅者
- Watcher
- 自身实例化的时候往dep对象中添加自己
- 当数据变化dep通知所有的 Watcher 实例更新视图