Vue响应式原理
ExMaterial 人气:0前置知识
数据驱动
数据响应式——Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。
双向绑定——数据改变,视图改变;视图改变,数据也随之改变
数据驱动是Vue最独特的特性之一 —— 开发者只需关注数据本身,而无需关心数据如何渲染到视图
数据响应式的核心原理
Vue 2.x
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</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删除 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.getElementById('app').textContent = data.msg } }) // 测试 vm.msg = 'Hello World' console.log(vm.msg) </script> </body> </html>
当data中有多个对象时,需要对其进行遍历,此时需要对上述代码进行一些改造。
let data = { msg: 'hello', count: 10 } let vm = {} proxyData(data) function proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(vm, key, { enumerable: true, configurable: true, get() { return data[key] }, set(newValue) { if (newValue === data[key]) return data[key] = newValue document.querySelector('#app').textContent = data[key] } }) }) }
Vue 3.x
步入Vue3,尤小右使用Proxy对其进行了改造,不仅抛弃了如 $delete 之类的鸡肋API(因为Proxy可以监听删除属性),还提升了性能。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app">hello</div> <script> let data = { msg: 'hello', count: 0 } let vm = new Proxy(data, { get(target, key) { return target[key] }, set(target, key, newValue) { if (target[key] === newValue) return target[key] = newValue document.querySelector('#app').textContent = target[key] } }) </script> </body> </html>
发布订阅和观察者模式
发布/订阅模式
注:为简便起见,代码实现并未加入对传参的考虑。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> class EventEmitter { constructor() { 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') </script> </body> </html>
观察者模式
注:为简便起见,代码实现并未加入对传参的考虑。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>观察者模式</title> </head> <body> <script> // 发布者-目标 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() </script> </body> </html>
Vue响应式原理模拟实现
Vue
功能
- 接收初始化参数
- 将data中的数据注入实例并转换成getter/setter
- 调用observer监听data中属性的变化
- 调用compiler解析指令/插值表达式
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对象,监听数据的变化 new Observer(this.$data) // 4. 调用compiler对象,解析指令和插值表达式 new Compiler(this) } _proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { return data[key] }, set(newValue) { if (newValue === data[key]) return data[key] = newValue } }) }) } }
Observer对data中的属性进行监听
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) { let that = this // 负责收集依赖 let dep = new Dep() // 如果val是对象,会将其内部的对象也变成响应式数据 this.walk(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依赖 Dep.target && dep.addSub(Dep.target) return val }, set(newValue) { if (newValue === val) { return } val = newValue that.walk(newValue) // 发送通知 dep.notify() } }) } }
Compiler
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.comipleElement(node) } // 判断node节点是否有子节点 if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 编译元素节点,处理指令 comipleElement(node) { 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 this.update(node, key, attrName) } }) } 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 = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater(node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } // 编译文本节点,处理插值表达式 compileText(node) { // console.dir(node); 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 }) } } // 判断元素属性是否是指令 isDirective(attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode(node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode(node) { return node.nodeType === 1 } }
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] Dep.target = null } // 当数据发生变化的时候更新视图 update() { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } }
Dep
class Dep { constructor() { this.subs = [] } // 添加观察者 addSub(sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发送通知 notify() { this.subs.forEach(sub => { sub.update() }) } }
测试代码
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Mini Vue</title> </head> <body> <div id="app"> <h1>差值表达式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./js/dep.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compiler.js"></script> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 100 } }) vm.test = 'abc' </script> </body> </html>
加载全部内容