目前的MVVM框架都解决了数据与视图直接维护的关系,在Vue中的一个特性就是数据驱动,Vue的学习过程中,经常会看到三个词:数据响应式、双向绑定、数据驱动
数据响应式
- 数据(即数据模型)是普通的 JS 对象,当修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用 v-model 在表单元素上创建双向数据绑定
数据驱动是 Vue 最独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
Vue2.x版本的数据响应式原理
采用Object.defindProperty来进行数据劫持
当把一个普通的JS对象在data中传入给Vue实例时,会遍历这个对象所有的property,并使用Object.defindProperty设置它的getter/setter,在访问对象属性的getter和setter时就可以进行额外操作
// 多个属性时 就用Object.keys()得到所有自身的key来遍历
Object.defineProperty(obj, 'count', {
enumerable: true,
configurable: true,
get() {
// 这里可以做其他处理
return obj[key]
},
set() {
if (obj[key] === newVal) {
return
}
obj[key] = newVal
// 简单地在这里进行dom操作
document.querySelector('#count-div').innerHTML = newVal
}
})
Vue3.x版本 使用ES6的Proxy 代理对象
Proxy创建的代理对象,是可以处理对象的所有属性,不需要每个属性处理
为原对象创建一个代理对象,在getter、setter中进行额外处理
代理对象是不会影响到原来的对象,需要使用代理对象才有额外处理的效果
// obj是没有变化 要使用vm
let vm = new Proxy(obj, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, newVal) {
if (newVal === Reflect.get(target, key)) {
return
}
Reflect.set(target, key, newVal)
// 简单地在这里进行dom操作
document.querySelector('#app').innerHTML = newVal
}
})
发布订阅模式
- 订阅者:向信号中心订阅一个信息号,当信号被触发后,执行自己的事件
- 发布者:在某个时机向信息中心发布一个信号,发布信号后会订阅者才执行它自己的事件
- 信号中心:存储订阅者的订阅,接收发布者的通知
信号中心隔绝了订阅者和发布者关系,双方不知道对方存在(订阅者不知道谁发布的,发布者不知道谁订阅了)
例如 Vue 中的eventBus
// vm是信号中心
// vm.$on 订阅者
vm.$on('dataChange' , () => {
console.log('订阅者工作')
})
// vm.$emit 发布者
vm.$emit('dataChange')
观察者模式
观察者(订阅者) -- Watcher
- update() 当事件发生时 要做的事情
目标(发布者) -- Dep
- subs数组 存放所有的观察者
- addSub() 添加观察者
- notify() 当事件发生 调用所有观察者update
没有事件中心
发布者需要知道观察者存在
// 发布者-目标
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()
模拟Vue响应式原理
模拟Vue响应式原理需要实现这5个内容
- Vue: 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
- Observer: 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
- Compiler: 解析每个元素中的指令/插值表达式,并替换成相应的数据
- Dep: 添加观察者(watcher),当数据变化通知所有观察者
- Watcher: 数据变化更新视图
Vue
-
constructor (options)构造器: 保存options、data、el
-
options.data的数据挂载到vue实例上
-
调用proxyData() 把data的数据挂载到vue实例上的
-
创建Observer对象,把data和data的子成员变成数据响应式
-
创建Compiler对象编译template
constructor (options) { // 保存接收的参数 // 保存data // 保存el this.options=options∣∣this.data = options.data || {} // todo el 没有判空 this.el=typeofoptions.el===′string′?document.querySelector(options.el):options.el//把options.data的数据挂载到vue实例上this.proxyData(this.data) new Observer(this.$data) new Compiler(this) }
// 遍历data的属性 挂载到this(Vue实例) proxyData (data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { configurable: true, enumerable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) }
Observer
-
constructor() 调用walk
-
walk 会先判断传入的参数是不是对象,不是就返回,是对象就遍历key调用defineReative
-
defineReative 这里进行响应式处理,用Object.defindProperty(),创建这个key的Dep对象,再调用walk(如果这个key的值是对象,就会在这里递归遍历全部子节点)
-
getter里面进行Dep.target判断,加入到subs,返回value
-
setter里面调用walk(传入newVal,如果传入是对象就再进行递归遍历),赋值,dep.notify发布更新
constructor (data) { this.walk(data) } walk (data) { if (!data || typeof data !== 'object') { return } // 这里是将 this.data的数据进行响应式处理 Object.keys(data).forEach(key => { this.defineReative(data, key, data[key]) }) } // 这里的data 就是 this.data // 这个value是作为get的返回值 避免死递归 defineReative (data, key, value) { let that = this let dep = new Dep() this.walk(value) Object.defineProperty(data, key, { configurable: true, enumerable: true, // 这个get方法是在访问 data.变量时就会触发 // 如果返回 data[key] 就会再次触发 data.key的get函数 // 导致死递归 // 所以用了一个value作为闭包 返回值 get () { // Dep.target只在创建Watcher时存放 // 存放后会立刻触发一次变量get到这里来 if (Dep.target) { dep.addSub(Dep.target) // 添加后要把这个target 置为null // 避免重复添加 Dep.target = null } return value }, set (newValue) { if (value === newValue) { return } value = newValue // 调用walk判断 如果新值是对象就把它的成员变成响应式 that.walk(newValue) // 发布更新信息 让订阅者更新 dep.notify() } }) }
Compiler
-
constructor() 保存el、实例vm、调用compiler编译模板
-
compiler 获取el的子节点,遍历子节点,区分是文本还是元素节点,对应调用两种处理方法,再遍历时判断当前子节点,是否还有子节点,递归遍历
-
compilerText编译文本节点 判断是不是插值表达式,是的话就在vm里面拿对应的值替到textContent,并且在这里添加一个Watcher,当这里插值表达式对应vm里面的值变化后,再次更新DOM
-
compilerElement 获取节点全部属性,遍历这些属性,找有没有是v-开头的指令,找到指令就用指令名拼接方式,调用对应的处理函数
-
v-text 指令处理类似插值表达式,替换文本,添加Watcher用于更新
-
v-model指令 找到key对应的value,替换value属性(用于表单元素),添加Watcher用于更新
-
v-html指令 拿到key对应的value,用innerHTML方式,插入HTML语法的字符串,添加Watcher用于更新
const onRE = /^on:/ class Compiler { constructor (vm) { this.el = vm.el this.vm = vm this.compiler(this.el) } // 编译模板 处理文本 元素 compiler (el) { let childNodes = el.childNodes Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) { // 文本节点 this.compilerText(node) } else if (this.isElementNode(node)) { this.compilerElement(node) } if (node.childNodes && node.childNodes.length > 0) { this.compiler(node) } }) } compilerElement (node) { Array.from(node.attributes).forEach(attr => { let attrName = attr.name if (this.isDirective(attrName)) { attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { if (onRE.test(attrName)) { // v-on const evnet = attrName.replace(onRE, '') this.onUpdate(node, this.vm[key], evnet) return } let updateFun = this[attrName + 'Update'] updateFun && updateFun.call(this, node, this.vm[key], key) } // 先考虑一个事件 onUpdate (node, value, key) { // value 里面放的是处理事件的函数 // key 里面放的是事件名 node.addEventListener(key, value, false) } htmlUpdate (node, value, key) { node.innerHTML = value new Watcher(this.vm, key, (newValue) => { node.innerHTML = newValue }) } textUpdate (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } modelUpdate (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) node.addEventListener('input', () => { this.vm[key] = node.value }) } compilerText (node) { let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.1.trim() node.textContent = value.replace(reg, this.vm[key]) new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } isDirective (attrName) { return attrName.startsWith('v-') } isTextNode (node) { return node.nodeType === 3 } isElementNode (node) { return node.nodeType === 1 } }
Dep
-
constructor() 初始化一个 subs 用来存放Watcher
-
addSub 当触发对应属性(一个属性就new 一个Dep)getter时,如果有Dep.target有值(Watcher),把这个观察者放入subs
-
notify 当触发对应属性setter时,遍历subs里面观察者的update()
class Dep { constructor () { this.subs = [] } addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } notify () { this.subs.forEach(sub => { sub.update() }) } }
Watcher
-
constructor () 存放实例的vm、属性的key、真正更新DOM的cb,给Dep.target赋值自己本身this,给oldValue赋值vm[key](这里触发了一次key的getter,会在那里添加到dep.subs,实现了订阅效果)
-
update 判断一下newVal是不是变化了,变化就调用cb来更新DOM
class Watcher { constructor (vm, key, cb) { this.vm = vm this.key = key this.cb = cb Dep.target = this this.oldValue = vm[key] } update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } }
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!