数据驱动
##数据响应式、双向绑定、数据驱动##
数据响应式
数据响应式-数据响应式中的数据指的是数据模型,数据模型仅仅是普通的js对象,当我们修改数据的时候,视图会进行更新,避免了繁琐的dom操作,提高了开发效率
双向绑定
数据、视图其中一个发生改变,另一个也随之改变 我们可以使用v-model在表单元素上创建双向绑定,包含了数据响应式
数据驱动-Vue最独特的特性之一
开发过程中只需要关注数据的本身,而不需要关心数据是如何渲染到视图上的
数据响应式的核心原理
vue2.0使用的是object.defineproperty遍历监听对象的每一个属性,这里暂时不展开,后面会详细说明 vue3.0使用的是proxy直接监听对象,而不是属性,是es6中新增的,ie不支持,性能由浏览器优化,比object.defineproperty要好的多
两者里面都有get和set方法,不过object.defineproperty
中的get和set是不需要传参的get()
set(newValue)
,因为他只处理一个属性的读取和写入,而proxy
中是要处理对象中的所有属性,所以需要传入值
get(target,key)
set(target,key,newValue)
发布订阅模式
发布订阅模式
订阅者
发布者
信号中心
vue中的自定义事件,以及node中的事件机制都是基于发布订阅模式的,
自定义事件的注册
- 首先创建一个vue实例
- 然后通过$on注册事件,同一个事件可以注册多个事件处理函数
通过代码模拟实现自定义事件的实现机制
// 事件触发器
class EventEmitter {
constructor () {
// {'click': [fn1, fn2]}
this.subs = Object.create(null) // 记录所有的事件和事件对应的处理函数
}
// 注册事件
$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()
});
}
}
}
// 测试
let em = new EventEmitter()
em.$on('click', () => {
console.log('click1');
})
em.$on('click', () => {
console.log('click2');
})
em.$emit('click')
这段代码并没有体现出发布者和订阅者,只体现出了事件中心,也是实现了发布订阅模式,我们可以通过兄弟组件传值的方式来体会
观察者模式
vue的响应式机制中,使用了观察者模式
观察者模式和发布订阅模式的区别,没有事件中心,只有发布者和订阅者,并且发布者需要知道订阅者的存在
- 观察者(订阅者)--Watcher
- update(),当事件发生的时候,会调用update方法,内部就是更新视图,数据发生变化的就会触发
- 目标(发布者)-- Dep
- sub数组:存储所有的观察者
- addSub():添加观察者
- notify(): 当事件发生,调用所有观察者的update()方法
- 没有事件中心
手写一个简单的不传参的观察者模式
// 发布者-目标
class Dep {
constructor() {
// 记录所有的订阅者
this.subs = []
}
addSub (sub) { // 把订阅者添加到订阅者的数组中
// sub 对象,存在且必须有update方法
if(sub && sub.update){
this.subs.push(sub)
}
}
notify () { // 当事件发生的时候,通知所有的订阅者,调用订阅者的update方法
this.subs.forEach((sub) => {
sub.update()
})
}
}
// 订阅者-观察者
class Watcher {
update () { // 当事件发生的时候,由发布者来调用,更新视图等操作
console.log('update');
}
}
// 测试
let dep = new Dep() // 发布者对象
let watcher = new Watcher() // 订阅者对象
dep.addSub(watcher)
// 当事件发生的时候调用notify方法
dep.notify()
观察者模式和发布订阅模式的区别
观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的
发布订阅模式由统一调度中心调用,一次发布者和订阅者不需要知道对方存在
vue响应式原理简单实现
- 功能
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/差值表达式
class Vue {
constructor(options) {
// 通过属性保存选项中的数据
this.$options = options || {}
this.$data = options.data || {} // $data中的setter是真正监视数据变化的地方
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 是string,那么就是一个选择器,使用queryselector获取对应的dom对象,如果是dom对象那么就直接返回
// 把data中的成员转换成setter和getter,注入到vue实例中
this._proxyData(this.$data)
// 调用observer对象,监听数据的变化
// 调用compiler对象,解析指令和差值表达式
}
_proxyData(data) { // 代理数据
// 遍历data中的所有属性,这里没有考虑属性的递归
Object.keys(data).forEach(key => {
// 因为通过this来调用的_proxyData,所以函数内部的this就是构造函数的this,就是vue实例
// 把data的属性注入到vue的实例中,所以第一个参数就是要定义属性的对象,就是this,就是vue实例
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中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
- 结构 observer
- walk(data)--遍历data中的所有属性
- defineReactive(data, key, value)-定义响应式数据
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.defineporperty把属性转换成getter和setter
// 如果对象的key所对应的值(也就是val)是对象,就把其内部的属性转换成响应式的
this.walk(val)
let that = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () { // 这里会形成一个闭包,this.$data会引用到这里的get方法,get中又使用了val,所以val不会被回收
return val
// 为什么不是obj[key],因为当我们实时访问obj[key]的时候就会触发defineReactive的get方法,get方法里面又调用了obj[key],这样的话形成了一个死循环
},
set (newValue) {
if(newValue === val) {
return
}
// 这里this指向data
val = newValue
that.walk(newValue) // 处理当前一个属性赋值成一个新的对象的时候,让这个对象内部的属性是响应式的
// 发送通知
}
})
}
}
同时在vue.js中调用observer对象,监听数据的变化
Compiler
- 功能--一句话就是操作dom
- 负责编译模板,解析指令和插值表达式
- 负责页面的首次渲染
- 当前数据变化后重新渲染视图
- 结构
- el-- vue构造函数传过来的options.el,已经转换成了dom对象,模板
- vm-- vue实例
- compile(el)--遍历dom对象的所有节点,文本节点解析插值表达式,元素节点解析指令
- compileElement(node) 解析指令
- compileText(node) 解析插值表达式
- isDirective(attrName) compileElement中调用,判断当前属性是否是指令
- 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 // 子节点,伪数组
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
attrName = attrName.substr(2)
let key = attr.value // v-text=msg 的msg
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);
// 正则匹配
let reg = /\{\{(.+?)\}\}/ // 可以匹配到插值表达式
let value = node.textContent // {{ text }}
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
}
}
同时在vue.js中调用compiler对象,解析指令和差值表达式
Dep
- 功能-每一个响应式属性都会创建一个dep对象,负责收集所有依赖该属性的地方,所有依赖该属性的位置都会创建一个watcher对象,所以dep收集的就是依赖于该属性的watcher对象,当属性发生变化的时候,通过dep的notify发送通知,调用watcher的update方法
- 收集依赖添加观察者
- 通知所有的观察者
- 结构
- subs-数组,存储dep中的所有的watcher
- addSub(sub)-添加watcher
- notify-通知
class Dep {
constructor() {
this.subs = [] // 存储所有的观察者
}
// 添加观察者
addSub (sub) {
if(sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
- 功能
- 当数据发生变化触发依赖,dep通知所有的Watcher实例更新视图
- 当去自身实例化创建一个watcher对象的时候,内部需要把自己添加到watcher对象的subs数组中(往dep对象中添加自己)
- 结构
- update()--更新视图
- cb--callback指明如何更新视图
- key--data中的属性名称,结合vm获取属性的值
- oldValue--更新前的视图
- vm--vue实例
// 作用,创建watcher对象的时候,需要把watcher对象放到
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中的属性名称
this.key = key
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target中
Dep.target = this
// 触发get方法,在get方法中会调用addSub方法
this.oldValue = vm[key] // vm[key]这么访问的时候,是可以直接触发get方法的
Dep.target = null // 防止重复添加
}
// 当数据发生变化的时候,更新视图
update () {
let newValue = this.vm[this.key]
if(this.oldValue === newValue) { // 没有变化就什么都不做
return
}
this.cb(newValue) // 不等的话,更新视图,需要新值,所以需要参数newValue
}
然后同时需要在compiler中,所有视图中依赖数据的位置,创建一个watcher对象,当数据发生改变时,更新视图