Vue响应式原理/双向数据绑定
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue响应式原理/双向数据绑定相关的知识,希望对你有一定的参考价值。
参考技术A 一句话概括:采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。
具体实现:
1.为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;
2.然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,也会,v-bind也会;
3.接着为input会添加监听事件,修改值就等于为该属性赋值,则会触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者4.数组循环调用各订阅者的update方法更新视图。
Vue 响应式实现原理
准备工作
- 数据驱动
- 响应式的核心原理
- 发布订阅模式和观察者模式
数据驱动
- 数据响应式、双向绑定、数据驱动
- 数据响应式
- 数据模型仅仅是普通的 JS 对象,而当我们修改数据时,试图回进行更新,避免了繁琐的 DOM 操作,提高开发效率
- 双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用 v-model 在表单元素上创建双向数据绑定
- 数据驱动是 Vue 最独特的特性之一
- 开发过程中仅需关注数据本身,不需要关心数据是如何渲染到视图
- 数据响应式
响应式的核心原理
vue2
浏览器兼容 IE8 以上(不兼容 IE8)
- 单个属性
// 模拟 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
)
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
- 多个属性
// 模拟 vue 中的 data 选项
let data =
msg: 'hello',
count: 10
// 模拟 vue 的实例
let vm =
proxyData(data)
function proxyData(data)
// 遍历 data 对象的所有属性
Object.keys(data).forEach(key =>
// 把 data 中的属性,转换成 vm 的 setter/getter
Object.defineProperty(vm, key,
enumerable: true,
configurable: true,
get ()
console.log('get', data.msg)
return data[key]
,
set (newValue)
console.log('set', newValue)
if (newValue === data[key]) return
data[key] = newValue
document.querySelector('#app').textContent = data[key]
)
)
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
vue3
直接监听对象,而非属性。
ES 6中新增,IE 不支持,性能由浏览器优化
// 模拟 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]
)
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
发布订阅模式和观察者模式
发布订阅
- 订阅者
- 发布者
- 信号中心
什么是 “发布/订阅模式” (publish-subscribe pattern)
- 我们假定,存在一个 “信号中心”,某个任务执行完成,就向信号中心 “发布”(publish) 一个信号,其他任务可以向信号中心 “订阅”(subscribe) 这个信号,从而知道什么时候自己可以开始执行
一家超市,存在一个公众号,或者小程序,也就是上述的 “信号中心”
每当这家超市出现折扣,就向小程序或者公众号发布活动信息,也就是上述的 “向信号中心 发布 一个信号”
这家超市的会员可以关注小程序或者公众号,这样在超市发布活动时,自己就知道什么时候去薅羊毛,也就是上述的 “其他任务 向 信号中心 订阅 这个信号,从而知道自己什么时候可以开始执行”
Vue 的自定义事件
let vm = new Vue()
vm.$on('dataChange', () =>
console.log('dataChange')
)
vm.$on('dataChange', () =>
console.log('dataChange1')
)
vm.$emit('dataChange')
- 这段代码中我们无法直观地感受到事件发布与订阅,所以我们可以借助下边的场景来更直观的感受发布与订阅模式
兄弟组件通信过程
// eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue --- 发布者
addTodo: function ()
// 发布消息(事件)
eventHub.$emit('add-todo', text: this.newTodoText)
this.newTodoText = ''
// ComponentB.vue --- 订阅者
created: function ()
// 订阅消息
eventHub.$on('add-todo', this.addTodo)
- 通过 eventBus 我们就可以感受到事件的订阅与发布
- 这段代码中 ComponentA 就相当于上述举例的 超市的小程序或者公众号 ,ComponentB 就相当于上述举例的 关注了这家超市公众号的会员
- 当 发布者 通过 $emit 触发了一个叫做 add-todo 的事件后,那么通过 $on 订阅了 add-todo 的订阅者,就会执行对应的事件
模拟 Vue 自定义事件的实现
class EventEmitter
constructor ()
// eventType: [handler1, handler2]
this.subs =
// 订阅通知
$on (eventType, handler)
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
// 发布通知
$emit (eventType)
if (this.subs[eventType])
this.subs[eventType].forEach(handler =>
handler()
)
// 测试
var bus = new EventEmitter()
// 注册事件
bus.$on('click', function ()
console.log('click')
)
bus.$on('click', function ()
console.log('click1')
)
// 触发事件
bus.$emit('click')
观察者
- 观察者(订阅者)— Watcher
- update():当事件发生时,具体要做的事情
- 目标(发布者)— Dep
- subs 数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生,调用所有观察者的 update() 方法
- 没有事件中心
// 目标(发布者)
// Dependency
class Dep
constructor ()
// 存储所有的观察者
this.subs = []
// 添加观察者
addSub (sub)
if (sub && 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)
dep.notify()
总结
- 观察者模式 是由具体目标调度,比如当事件触发,Dep 就回去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的
- 发布/订阅模式 是由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在
Vue 响应式原理模拟
整体分析
- Vue 基本结构
- 打印 Vue 实例观察
- 整体结构
- Vue
- 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
- Observer
- 能够把数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
- Compiler
- 解析每个元素中的指令/插值表达式,并替换成相应的数据
- Dep
- 添加观察者(watcher),当数据变化通知所有观察者
- Watcher
- 数据变化更新视图
Vue 类
- 功能
- 接收初始化的参数(选项)
- 把 data 中的属性注入到 Vue 实例,转换成 getter/setter
- 调用 observer 监听 data 中所有属性的变化
- 调用 compiler 解析指令/插值表达式
- 结构
- $options
- $el
- $data
- _proxyData() 静态方法不对外暴露
- 代码
class Vue constructor (options) // 1. 保存选项的数据 this.$options = options || this.$data = options.data || const el = options.el this.$el = typeof options.el === 'string' ? document.querySelector(el) : el // 2. 负责把 data 注入到 Vue 实例 this._proxyData(this.$data) // 3. 负责调用 Oberver 实现数据劫持 // 4. 负责调用 Compiler 解析指令/插值表达式等 _proxyData (data) // 遍历 data 的所有属性 Object.keys(data).forEach(key => Object.defineProperty(this, key, enumerable: true, configurable: true, get () return data[key] , set (newValue) if (data[key] == newValue) return data[key] = newValue ) )
Observer 类
- 功能
- 把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
- 结构
- walk(data)
- defineReactive(data, key, value)
- 代码
// 负责数据劫持 // 把 $data 中的成员转换成 getter/setter class Observer constructor (data) this.walk(data) // 1. 判断数据是否是对象,如果不是对象返回 // 2. 如果是对象,遍历对象的所有属性,设置为 getter/setter walk (data) if (!data || typeof data !== 'object') return // 遍历 data 的所有成员 Object.keys(data).forEach(key => this.defineReactive(data, key, data[key]) ) // 定义响应式成员 defineReactive (data, key, val) const that = this // 如果 val 是对象,继续设置它下面的成员为响应式数据 this.walk(val) Object.defineProperty(data, key, configurable: true, enumerable: true, get () return val , set (newValue) if (newValue === val) return // 如果 newValue 是对象,设置 newValue 的成员为响应式 this.walk(newValue) val = newValue )
Compiler 类
- 功能
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
- 结构
- 属性
- el
- vm
- 方法
- compile(el)
- compileElement(node)
- compileText(node)
- isDirective(attrName)
- isTextNode(node)
- isElementNode(node)
- 属性
- 代码
- compile
- 负责解析指令/插值表达式
// 负责解析指令/插值表达式 class Compiler constructor (vm) this.vm = vm this.el = vm.$el // 编译模板 this.compile(this.el) // 编译模板 // 处理文本节点和元素节点 compile (el) const nodes = el.childNodes Arrat.from(nodes).forEach(node => // 判断是文本节点还是元素节点 if (this.isTextNode(node)) this.compileText(node) else if (this.isElementNode(node)) this.compileElement(node) if (node.childNodes && node.childNodes.length) // 如果当前节点中还有子节点,递归编译 this.compile(node) ) // 判断是否是文本节点 isTextNode (node) return node.nodeType === 3 // 判断是否是节点属性 isElementNode (node) return node.nodeType === 1 // 判断是否是以 v- 开头的指令 isDirective (attrName) return attrName.startswith('v-') // 编译文本节点 compileText (node) // 编译属性节点 compileElement (node)
- compileElement
- 负责编译元素的指令
- 处理 v-text 的首次渲染
- 处理 v-model 的首次渲染
// 编译属性节点 compileElement (node) // 遍历元素节点中的所有属性,找到指令 Array.form(node.attributes).forEach(attr => // 获取元素属性的名称 let attrName = attr.name // 判断当前的属性名称是否是指令 if (this.isDirective(attrName)) // attrName 的形式 v-text v-model // 截取属性的名称,获取 text model attrName = attrName.substr(2) // 获取属性的名称,属性的名称就是我们数据对象的属性 v-text='name',获取的是 name const key = attr.value // 处理不同的指令 this.update(node, key, attrName) ) // 负责更新 DOM // 创建 watcher update (node, key, dir) // node 节点,key 数据的属性名称,dir 指令的前半部分 const updaterFn = this[dir + 'Updater'] updaterFn && updaterFn(node, this.vm[key]) // v-text 指令的更新方法 textUpdater (node, value) node.textContent = value // v-model 指令的更新方法 modelUpdater (node, value) node.value = value
- compileText
- 负责编译插值表达式
// 编译文本节点 compileText (node) const reg = /\\\\(.+?)\\\\/ // 获取文本节点的内容 const value = node.textContent if (reg.test(value)) // 插值表达式中的值就是我们要的属性名称 const key = RegExp.$1.trim() // 把插值表达式替换成具体的值 node.textContent = value.replace(reg, this.vm[key])
- isDirective
- 判断元素属性名是否为指令,以 v- 开头都是指令
isDirective(attrName) return attrName.startsWith('v-')
- isTextNode
- 判断是否是文本节点
isTextNode (node) return node.nodeType === 3
- isElementNode
- 判断是否是元素节点
isElementNode (node) return node.nodeType === 1
- compile
Dep(Dependency) 类
- 功能
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
- 结构
- subs
- addSub(sub)
- notify
- 代码
class Dep
constructor ()
// 存储所有的观察者
this.subs = []
// 添加观察者
addSub (sub)
if (sub && sub.update)
this.subs.push(sub)
// 通知所有观察者
notify ()
this.subs.forEach(sub =>
Sub.update()
)
- 在 compiler.js 中收集依赖,发送通知
// defineReactive 中
// 创建 dep 对象收集依赖
const dep = new Dep()
// getter 中 --- get 的过程中收集依赖
Dep.target && dep.addSub(Dep.target)
// setter 中 --- 当数据变化制后,发送通知
dep.notify()
Watcher 类
- 功能
- 当数据变化触发依赖,dep通知所有的 Watcher 实例更新视图
- 自身实例化的时候往 dep 对象中添加自己
- 结构
- vm
- key
- cb
- oldValue
- update()
- 代码
class watcher
co以上是关于Vue响应式原理/双向数据绑定的主要内容,如果未能解决你的问题,请参考以下文章