模拟Vue.js响应式原理
数据驱动
- 数据响应式
- 数据模型是普通的JavaScript对象,当我们修改数据时,视图会进行相应的更新,避免了繁琐的DOM操作,提高开发效率
- 双向绑定
- 数据改变,视图发生相应变化,视图变化,数据发生相应的变化
- 可以使用v-model指令在表单元素上创建双向数据绑定(自定义组件也可以自己实现v-model)
- 数据驱动
- 数据驱动是Vue最独特的特性之一,开发过程只需要关心数据,而不需要关心数据如何被渲染到视图
数据响应式核心原理
Vue2.x
- 遍历对象中的属性,并通过
Object.defineProperty
方法,将对象中的属性,转换成getter/setter方法 -
Object.defineProperty
是ES5中新增的,不支持IE8以下浏览器
Vue3.x
- 基于ES6新增的
Proxy
来实现 -
Proxy
代理的是整个对象,而不是对象的属性,因此不需要对对象的属性进行遍历 -
Proxy
的性能由浏览器优化,要优于Object.defineProperty
- 同样不支持IE8以下浏览器
发布订阅模式和观察者模式
发布订阅模式
发布订阅模式包含发布者、订阅者、消息中心,发布者与订阅者相互之间不知道彼此存在,通过消息中心进行消息转发
发布者
订阅者
消息中心broker
-
模拟实现代码
class EventEmitter { constructor() { this.subs = Object.create(null) } // 订阅 - 注册事件 $on (topic, handler) { this.subs[topic] = this.subs[topic] || [] this.subs[topic].push(handler) } // 发布 - 触发事件 $emit (topic, ...args) { this.subs[topic]?.forEach(handler => { typeof handler === 'function' && handler.apply(null, args) }); } }
观察者模式
观察者模式包含观察者(订阅者)和目标(发布者),不存在消息中心,被观察的目标需要知道观察者的存在
-
观察者(订阅者)Watcher
- update方法:处理函数
-
目标(发布者)
- notify方法:通知观察者,调用所有观察者的update方法
- subs数组:储存所有的观察者
- addSub:添加观察者
-
模拟实现代码
export class Target { constructor() { this.subs = [] } addSub(sub) { sub && typeof sub.update === 'function' && this.subs.push(sub) } notify(...args) { this.subs.forEach(sub => sub.update.apply(null, args)) } } export class Watcher { update(...args) { console.log(args) } }
发布订阅模式 vs 观察者模式
模拟Vue.js响应式原理
Vue类的简单实现
-
功能
- 负责接收初始化的参数(选项)
- 负责把data中的属性转换成getter/setter,并注入到Vue实例中
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/差值表达式
-
属性
- $options
- 保存传入的选项
- $data
- 保存传入的data
- $el
- 保存挂载的DOM元素
- $options
-
方法
- _proxyData
- 把data中的属性转换成getter/setter,并注入到Vue实例中
- _proxyData
-
实现代码
export default class Vue { // 1. 通过属性保存选项的数据 constructor(options) { this.$options = options || {} this.$data = options.data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el this._proxyData(this.$data) } // 2. 把data中的属性转换成getter/setter注入到vue实例中 _proxyData(data) { // Vue2.x处理方式 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 }, }) }) } // 3. 调用observer对象,监听数据变化 new Observer(this.$data) // 4. 调用compiler对象,解析指令和差值表达式 new Compiler(this) }
Observer的简单实现
-
功能
- 负责把data中的数据转换成响应式数据
- data中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
-
方法
- walk(data)
- 遍历data中的所有属性
- defineReactive(data, key, value)
- 转换属性为getter/setter
- walk(data)
-
实现代码
export default class Observer { constructor(data) { this.walk(data) } walk(data) { // 1. 判断data是否是对象 // 2. 遍历data的所有属性 if (!data || typeof data !== 'object') { return } Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(data, key, value) { // 为什么需要第三个参数传递value,而不直接使用data[key]? // 在get中使用data[key]会循环触发getter,导致栈溢出 // 为什么value属性在defineProperty执行完成后还可以被访问 // 外部对内部属性存在引用,形成了闭包 // 如果value是对象,则将对象的属性也转换成响应式数据 const _this = this // 创建Dep对象收集依赖,发送通知 let dep = new Dep() this.walk(value) Object.defineProperty(data, key, { configurable: true, enumerable: true, get() { // 收集依赖 Dep.target && dep.addSub(Dep.target) return value }, set(newValue) { if (value === newValue) { return } value = newValue // 当属性被重新赋值为一个对象,将对象属性也转换为响应式数据 _this.walk(newValue) dep.notify() } }) } }
Compiler的简单实现
-
功能
- 负责编译模板,解析指令/差值表达式
- 负责页面的首次渲染
- 当数据变化后,重新渲染视图
-
属性
- el
- DOM对象
- vm
- vue实例
- el
-
方法
compiler的方法主要用来进行DOM操作
- compile(el)
- 编译解析入口
- compileElement(node)
- 解析元素节点的指令
- compileText(node)
- 解析文本节点的差值表达式
- isDirective(attrName)
- 判断属性是否是指令
- isElementNode(node)
- 判断是否是元素节点
- isTextNode(node)
- 判断是否文本节点
- compile(el)
-
实现代码
import Watcher from "./watcher.js" export default class Compiler { constructor(vm) { this.el = vm.$el this.vm = vm this.compile(this.el) } // 编译模板,处理文本节点和元素节点 compile (el) { [...el.childNodes].forEach(node => { if (this.isTextNode(node)) { this.compileText(node) } if (this.isElementNode(node)) { this.compileElement(node) } // 判断childNodes并递归调用compile if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 编译元素节点,处理指令 compileElement (node) { // 获取并遍历元素的属性节点 [...node.attributes].forEach(attr => { const { name, value } = attr if (this.isDirective(name)) { this.update(node, name.slice(2), value) } }) } // 处理指令 update (node, name, value) { const fn = this[`${name}Updater`] typeof fn === 'function' && fn.call(this, node, value) } // 处理v-text指令 textUpdater(node, value) { node.textContent = this.vm[value] // 创建watcher对象,当数据改变时更新视图 new Watcher(this.vm, value, newValue => { node.textContent = newValue }) } // 处理v-model指令 modelUpdater(node, value) { node.value = this.vm[value] // 创建watcher对象,当数据改变时更新视图 new Watcher(this.vm, value, newValue => { node.value = newValue }) // 注册表单input事件实现双向绑定 node.addEventListener('input', () => { this.vm[value] = node.value }) } // 处理其他指令 // ... // 编译文本节点,处理差值表达式 compileText (node) { // 使用正则表达式匹配差值表达式 {{ xxx }},并提取获取表达式内容 let reg = /\{\{(.+?)\}\}/ const text = node.textContent node.textContent = text.replace(reg, (word, key) => { key = key.trim() if (key in this.vm) { // 创建watcher对象,当数据改变时更新视图 new Watcher(this.vm, key, newValue => { node.textContent = text.replace(reg, (w, k) => this.vm[k.trim()]) }) return this.vm[key] } return word }) } // 判断属性是否为指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判断是否为元素节点 isElementNode (node) { return node.nodeType === 1 } // 判断是否为文本节点 isTextNode (node) { return node.nodeType === 3 } }
Dep的简单实现
-
功能
- 收集依赖,添加观察者(watcher)
- notify通知所有观察者
-
属性
- subs
- 储存所有的观察者
- subs
-
方法
- addSubs(sub)
- 添加观察者
- notify()
- 通知观察者,调用所有观察者的update方法
- addSubs(sub)
-
实现代码
export default class Dep { constructor() { this.subs = [] } addSub(sub) { sub && typeof sub.update === 'function' && this.subs.push(sub) } notify(...args) { this.subs.forEach(sub => sub.update(...args)) } }
Watcher的简单实现
-
功能
- 数据变化触发依赖,接收dep通知更新视图
- 自身实例化的时候向Dep中添加自己
-
属性
- vm
- vue实例
- key
- 观察的属性名称
- cb
- 更新时的回调处理函数
- oldValue
- 观察的属性数据更新之前的值
- vm
-
方法
- update()
- 更新处理函数
- update()
-
实现代码
import Dep from "./dep.js" export default class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 把watcher对象记录到Dep类的静态属性target Dep.target = this // 触发get方法,在get中调用addSub this.oldValue = vm[key] // 清空Dep.target,避免重复添加 Dep.target = null } // 数据变化时更新视图 update(...args) { const newValue = this.vm[this.key] if (newValue !== this.oldValue) { this.cb(newValue) } } }
总结
问题
- 将属性重新赋值成对象,是否是响应式的?
- 是响应式的,重新赋值成对象时会调用属性的set方法,此时会将新赋值的内容转换为响应式数据
- 为vue实例添加新的属性时,此属性是否是响应式的?
- 不是响应式的
- 可以通过Vue.set()或vm.$set()方法设置新的响应式属性