1 数据驱动
数据响应式、双向绑定、数据驱动
1 数据响应式
数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
2 双向绑定
数据改变,视图改变;视图改变,数据改变
使用 v-model 在表单元素上创建双向数据绑定
3 数据驱动是 Vue 最独特的特性之一
开发过程中仅需关注数据本身,不需要关心数据是如何渲染到视图
2 响应式核心Vue2
用Object.defineProperty 作数据劫持,不支持ie8及以下
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><div id="app"></div><script>// 模拟 vue 中的 datalet 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(value) {console.log('set: ', data.msg)if(data.msg === value) returndata.msg = value;// 更新domdocument.querySelector('#app').textContent = data.msg}})vm.msg = 'hello world';console.log(vm.msg)</script></body></html>
如果一个对象中有多个属性要转换,getter/setter怎么处理
...<div id="app"></div><script>// 模拟 vue 中的 datalet data = {msg: 'hello',count: 20}// 模拟 Vue 的实例let vm = {}proxyData(data)function proxyData(data) {Object.keys(data).forEach(key => {// 数据劫持:当访问或者设置 vm 中的成员时,做一些干预操作Object.defineProperty(vm, key, {// 可枚举(可遍历)enumerable: true,// 可配置(可以使用 delete 删除, 可以通过 defineProperty 重新定义)configurable: true,// 当获取值的时候执行get() {console.log('get: ', data[key])return data[key]},set(value) {console.log('set: ', data[key])if(data[key] === value) returndata[key] = value;// 更新domdocument.querySelector('#app').textContent = data[key]}})})}vm.msg = 'hello world';console.log(vm.msg)...
3 响应式核心vue3
用 proxy 对象,直接监听对象而非属性,ES6中新增,ie不支持,性能由浏览器优化
...<div id="app"></div><script>// 模拟 vue 中的 datalet data = {msg: 'hello',count: 20}// 模拟 Vue 的实例let vm = new Proxy(data, {// 执行代理行为的函数get(target, key) {console.log('get, key:', key, target[key])return target[key]},set(target, key, value) {console.log('set, key:', key, target[key])if(target[key] === value) returntarget[key] = valuedocument.querySelector('#app').textContent = target[key]}})vm.msg = 'hello world';console.log(vm.msg)...
4 发布订阅模式
- 订阅者
- 发布者
- 信号中心
假设疫情期间,各地的疾控中心每天统计确诊病例,然后发布到疫情发布平台,我们就可以通过订阅疫情发布平台去获取疫情的信息,从而就可以知道周边有没有确认病例
vue 的自定义事件
// 注册事件vm.$on('datachange', () => {console.log('data change')})vm.$on('datachange', () => {console.log('data change1')})// 触发事件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)}
模拟vue的事件机制
class EventEmitter {constructor() {this.subs = Object.create(null)}$on(eventType, handler) {this.sub[eventType] = this.sub[eventType] || [];this.sub[eventType].push(handler)}$emit(eventType) {if(this.sub[eventType]) {this.sub[eventType].forEach( handler => {handler()})} else {console.log('事件未挂载')}}}let vm = new EventEmitter()vm.$on('click', function(){console.log('click1')})vm.$on('click', function(){console.log('click2')})vm.$emit('click')
5 观察者模式
- 观察者(订阅者) watcher
update(): 当事件发生时,具体要做的事情
- 目标(发布者)Dep
- subs数组:存储所有的观察者
- addSub(): 添加观察者
- notify(): 当事件发生时,调用所有观察者的update 方法
没有事件中心 ```javascript // 目标-发布者 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();
总结<br />观察者模式:由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的发布者和订阅者是存在依赖关系的<br />发布订阅模式:由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在<a name="8uxVr"></a>#### 6 Vue 响应式原理模拟Vue类<br /> 功能- 负责接收初始化的参数- 负责把 data 中的属性注入到 Vue 的实例,转换成 getter/setter- 负责调用 observer 监听 data 中所有属性的变化- 负责调用 compiler 解析指令/插值表达式结构- 类名 vue- 属性 $options $el $data- 方法 _proxyData()```javascript// vue.jsclass 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 中的所有属性Object.keys(data).forEach(key => {// 把 data 的属性注入到 vue 实例中Object.defineProperty(this, key, {enumerable: true,configurable: true,get () {return data[key]},set(newValue) {if(newValue === data[key]) {return}data[key] = newValue}})})}}
Observer类
功能
- 负责把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象,把该属性也转换成响应式数据
- 数据变化发送通知
结构
- walk(data) 方法,遍历属性
defineReactive(data,key,value) 通过调用Object.defineProperty方法把属性转换成getter、setter ```javascript class Observer { constructor(data) {
this.walk(data);
}
walk(data) { // 1 判断data 是否对象 if(!data || typeof data !== ‘object’) {
return
} // 2 遍历对象的所有属性 Object.keys(data).forEach( key => {
this.defineReactive(data, key, data[key])
}) }
defineReactive(obj, key, value) { let that = this // 如果 value 是对象,把对象的属性转换为响应式数据 this.walk(value) Object.defineProperty(obj, key, {
enumerable: true,configurable: true,get () {// 此处不返回obj[key]的原因是 在vue.js中对obj有get方法,这样就会导致次递归循环调用,堆栈溢出// 此处在外部有调用就会形成闭包,所有可以拿到值return value},set (newValue) {if(newValue === value) {return}value = newValue// 赋值的是对象,把对象的属性也转换成响应式数据that.walk(newValue)// 发送通知}
}) } }
<a name="bXsDE"></a>#### 7 Compiler 类功能- 负责编译模板、解析指令、插值表达式- 负责页面的首次渲染- 当数据变化时重新渲染视图结构- el 存储模板- vm 存储实例- compiler(el)方法 编译模板、处理文本节点和元素节点- compilerElement(el)方法 编译元素节点、处理指令- compilerText(el)方法 编译文本节点、处理插值表达式- isDirective(attrName)方法 判断元素是否是指令- isTextNode(node)方法 判断节点是否是文本节点- isElementNode(node)方法 判断节点是否是元素节点在new Compiler 的时候,传入了 vue 的实例,在compiler 构造函数内部存储实例和实例的$el,然后调用编译的方法,在编译的方法中根据传入的$el,去遍历它的所有子节点,然后根据节点类型是元素节点还是文本节点去执行对应的编译方法,执行完毕后判断遍历的当前节点是否有子节点,再递归调用。编译文本节点,通过正则表达式,获取到插值表达式的name,然后根据name去找实例的属性对应的值,替换进节点中;编译元素节点,遍历节点的所有属性,判断属性是否是指令,根据不同指令执行不同的编译方法。```javascriptclass Complier {constructor(vm) {this.el = vm.$elthis.vm = vmthis.complier(this.el)}// 编译模板,处理文本节点和元素节点complier (el) {let childNodes = el.childNodesArray.from(childNodes).forEach( node => {if(this.isTextNode(node)) {this.compilerText(node)} else if(this.isElementNode(node)) {this.complierElement(node)}// 判断node 节点是否有子节点,如果有子节点,要递归调用compilerif(node.childNodes && node.childNodes.length) {this.complier(node)}})}// 编译元素节点,处理指令complierElement (node) {// console.log(node.attributes)// 遍历所有属性节点Array.from(node.attributes).forEach( attr => {// 判断是否是指令let attrName = attr.nameif(this.isDirective(attrName)) {// v-text => textattrName = attrName.substr(2)let key = attr.valuethis.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}// 编译文本节点,处理插值表达式compilerText (node) {// console.dir(node)let reg = /\{\{(.+?)\}\}/let value = node.textContentif(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}}
8 Dep(Dependency)类
功能
- 收集依赖,添加观察者
- 通知所有观察者
- subs 存储所有观察者 watcher
- addSub(sub)方法 添加watcher
notify() 数据变化时通知所有的观察者
class Dep {constructor () {// 存储所有的观察者this.subs = []}// 添加观察者addSub (sub) {if(sub.update) {this.subs.push(sub)}}// 发送通知notify () {this.subs.forEach( sub => {sub.update()})}}
9 Watcher 类
功能
当数据变化触发依赖, dep 通知所有的 watcher 实例更新视图
- 自身实例化的时候往 dep 对象中添加自己
结构
- vm 实例
- key 属性名称
- cb 回调函数,如何更新视图
- oldValue 记录数据变化之前的值
update()方法 更新视图,可以拿到最新的值,比对旧的值,发生变化的情况下再去更新视图
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]// 把target 设为空,防止重复渲染Dep.target = null}// 当数据发生变化的时候更新视图update () {let newValue = this.vm[this.key]if(this.oldValue === newValue) {return}this.cb(newValue)}}
添加 watcher 到文本节点编译方法
// compiler.js...compilerText (node) {...// 创建 watcher 对象,数据改变时更新视图new Watcher(this.vm, key, newValue => {node.textContent = newValue})...}...
添加 watcher 到元素节点
...// 调用指令的方法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 = valuenew Watcher(this.vm, key, newValue => {node.textContent = newValue})}// 处理 v-model 指令modelUpdater (node, value, key) {node.value = valuenew Watcher(this.vm, key, newValue => {node.value = newValue})}...
10 双向绑定
通过给model指令处理函数绑定事件来更新 data 数据
...// 处理 v-model 指令modelUpdater (node, value, key) {...// 双向绑定node.addEventListener('input', () => {this.vm[key] = node.value})}...
11 调试首次渲染
1 new Vue 处断点,进入vue.js 的构造函数中,存储选项中到属性,遍历data 设置为setter/getter存储到属性中
2 vue.js 实例化 Observer 对象,构造函数中传入data,observer 中遍历data 对象所有属性设为getter/setter,在每个属性遍历的时候新建 dep 实例,在getter 中如果 Dep 有target 就把 Dep 的target 添加到 dep 实例中的subs 中,在setter 中,递归遍历属性,并且调用 dep 的通知方法,使每个 subs 中的 watcher 都调用 update;
3 vue.js 实例化 Compiler 对象,构造函数中传入 vue 实例,调用 compiler 方法编译模板,根据 vue 实例的 el,递归遍历所有子节点,根据不同的节点类型执行不同的编译函数;
4 compiler.js 在节点编译函数中新建 Water 的实例,Watcher 的构造函数中传入当前节点,编译的 data 对应的 key,和数据更新后的回调函数;在 Watcher 构造函数中,把 watcher 的实例挂载到 Dep 对象的target 上,然后在存储旧值时,触发 vue 中data 某属性的 getter,接着会触发 observer 中 data 某属性的 getter;在 observer 的get 中,此时 Dep 有target,就会把 target 上的 watcher 放入 Dep实例的 subs 数组中; watcher 实例在构造函数的做完这些后销毁 target;
5 至此,首次渲染完成12 调试数据改变
1 在 observer 中的set 方法中 dep 发送通知的地方断点,此时dep 中的 notify 执行,变量 dep 实例中的watcher 组成的 subs 数组,调用每个 watcher 的 update 方法
2 watcher.js 中的update 方法会判断旧值和新值是否相等,不相等则调用watcher 的回调函数,回调函数是在 compiler 中定义的
3 compiler 中定义的回调函数执行,更新节点
4 至此,数据变化视图变化的逻辑完成13 总结
问题:
给属性重新赋值成对象,是否是响应式的
属性重新赋值会走 observer 的 set 方法,set 方法中对于新的值会递归设置成响应式的数据
- 给Vue 实例新增一个成员是否是响应式的
vue实例成员变成响应式是在new 一个 vue 实例时做的,在new 完实例以后,再去新增成员显然不会变成响应式的。在vue 官网文档中提及,vue 不允许添加根级别的响应式属性,但是可以使用 Vue.set(vm.someObject, ‘b’, 2)来添加嵌套对象的响应式属性
