像React,Vue这类的框架,响应式是其最外围的个性之一。通过响应式能够实现当扭转数据的时候,视图会主动变动,反之,视图变动,数据也随之更新。防止了繁琐的dom操作,让开发者在开发的时候只须要关注数据自身,而不须要关注数据如何渲染到视图。
实现原理
2.x
在vue2.0中通过Object.defineProperty办法实现数据拦挡,也就是为每个属性增加get和set办法,当获取属性值和批改属性值的时候会触发get和set办法。
let vue = {}let data = { msg: 'foo'}Object.defineProperty(vue, 'msg', { enumerable: true, configurable: true, get() { console.log('正在获取msg属性对应的值') return data.msg }, set(newValue) { if(newValue === data.msg) { return } console.log('正在为msg属性赋值') data.msg = newValue }})console.log(vue.msg)vue.msg = 'bar'
Object.defineProperty增加的数据拦挡在针对数组的时候会呈现问题,也就是当属性值为一个数组的时候,如果进行push,shift等操作的时候,尽管批改了数组,但不会触发set拦挡。
为了解决这个问题,vue在外部重写了原生的数组操作方法,以反对响应式。
3.x
在vue3.0版本中应用ES6新增的Proxy对象替换了Object.defineProperty,不仅简化了增加拦挡的语法,同时也能够反对数组。
let data = { msg: 'foo'}let vue = new Proxy(data, { get(target, key) { console.log('正在获取msg属性对应的值') return target[key] }, set(target, key, newValue) { if(newValue === target[key]) { return } console.log('正在为msg属性赋值') target[key] = newValue }})console.log(vue.msg)vue.msg = 'bar'
依赖的开发模式
在vue实现响应式的代码中,应用了观察者模式。
观察者模式
观察者模式中,蕴含两个局部:
- 观察者watcher
观察者蕴含一个update办法,此办法示意当事件发生变化的时候须要做的事件
class Watcher { update() { console.log('执行操作') }}
- 指标dep
指标蕴含一个属性和两个办法:
- subs属性:用于存储所有注册的观察者。
- addSub办法: 用于增加观察者。
- notify办法: 当事件变动的时候,用于轮询subs中所有的观察者,并执行其update办法。
class Dep { constructor() { this.subs = [] } addSub(watcher) { if (watcher.update) { this.subs.push(watcher) } } notify() { this.subs.forEach(watcher => { watcher.update() }) }}
- 应用形式
// 创立观察者和指标对象const w = new Watcher()const d = new Dep()// 增加观察者d.addSub(w)// 触发变动d.notify()
公布订阅模式
与观察者模式很类似的是公布订阅模式,该模式蕴含三个方面:
- 订阅者
订阅者相似观察者模式中的观察者,当事件发生变化的时候,订阅者会执行相应的操作。
- 发布者
发布者相似观察者模式中的指标,其用于公布变动。
- 事件核心
在事件核心中存储着事件对应的所有订阅者,当发布者公布事件变动后,事件核心会告诉所有的订阅者执行相应操作。
与观察者模式相比,公布订阅模式多了一个事件核心,其作用是隔离订阅者和发布者之间的依赖。
vue中的on和emit就是实现的公布订阅模式,因为其和响应式原理关系不大,所以此处不再具体阐明。
自实现简版vue
简化版的vue外围蕴含5大类,如下图:
通过实现这5大类,就能够一窥Vue外部如何实现响应式。
vue
vue是框架的入口,负责存储用户变量、增加数据拦挡,启动模版编译。
Vue类:
- 属性
$options
存储初始化Vue实例时传递的参数$data
存储响应式数据$methods
存储传入的所有函数$el
编译的模版节点
- 办法
_proxyData
公有办法,负责将data中所有属性增加到Vue实例上。
_proxyMethods
公有办法,遍历传入的函数,将非申明周期函数增加到Vue实例上。
directive
静态方法,用于向Vue注入指令。
- 实现
// 所有申明周期办法名称const hooks = ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed']class Vue { constructor(options) { this.$options = Object.assign(Vue.options || {}, options || {}) this.$data = options.data || {} this.$methods = options.methods || {} if (options && options.el) { this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el } this._proxyData(this.$data) this._proxyMethods(this.$methods) // 实现数据拦挡 // 启动模版编译 } _proxyMethods(methods) { let obj = {} Object.keys(methods).forEach(key => { if (hooks.indexOf(key) === -1 && typeof methods[key] === 'function') { obj[key] = methods[key].bind(this) } }) this._proxyData(obj) } _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 } }) }) } // 用于注册指令的办法 static directive(name, handle) { if (!Vue.options) { Vue.options = { directives: {} } } Vue.options.directives[name] = { bind: handle, update: handle } }}
observer
observer类负责为data对象增加数据拦挡。
- 办法
walk
轮询对象属性,调用defineReactive
办法为每个属性增加setter和getter。defineReactive
增加setter和getter。
- 实现
class Observer { constructor(data) { this.walk(data) } // 轮询对象 walk(data) { // 只有data为object对象时,才轮询其属性 if (data && typeof data === 'object') { Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } } // 增加拦挡 defineReactive(data, key, val) { const that = this // 如果val是一个对象,为对象的每一个属性增加拦挡 this.walk(val) Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { return val }, set(newValue) { if (val === newValue) { return } // 如果赋值为一个对象,为对象的每一个属性增加拦挡 that.walk(newValue) val = newValue } }) }}
在Vue的constructor构造函数中增加Observer:
constructor(options) { this.$options = Object.assign(Vue.options || {}, options || {}) this.$data = options.data || {} this.$methods = options.methods || {} if (options && options.el) { this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el } this._proxyData(this.$data) this._proxyMethods(this.$methods) // 实现数据拦挡 new Observer(this.$data) // 启动模版编译 new Compiler(this)}
directive
因为在compiler编译模版的时候,须要用到指令解析,所以此处模仿一个指令初始化办法,用于向vue实例增加内置指令。
在此处模仿实现了四个指令:
// v-textVue.directive('text', function (el, binding) { const { value } = binding el.textContent = value})// v-modelVue.directive('model', function (el, binding) { const { value, expression } = binding el.value = value // 实现双向绑定 el.addEventListener('input', () => { el.vm[expression] = el.value })})// v-htmlVue.directive('html', function (el, binding) { const { value } = binding el.innerHTML = value})// v-onVue.directive('on', function (el, binding) { const { value, argument } = binding el.addEventListener(argument, value)})
compiler
compiler负责html模版编译,解析模版中的插值表达式和指令等。
- 属性
el
保留编译的指标元素vm
保留编译时用到的vue上下文信息。
- 办法
compile
负责具体的html编译。
- 实现
class Compiler { constructor(vm) { this.vm = vm this.el = vm.$el // 构造函数中执行编译 this.compile(this.el) } compile(el) { if (!el) { return } const children = el.childNodes Array.from(children).forEach(node => { if (this.isElementNode(node)) { this.compileElement(node) } else if (this.isTextNode(node)) { this.compileText(node) } // 递归解决node上面的子节点 if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } compileElement(node) { const directives = this.vm.$options.directives Array.from(node.attributes).forEach(attr => { // 判断是否是指令 let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text // 获取指令的相干数据 let attrNames = attrName.substr(2).split(':') let name = attrNames[0] let arg = attrNames[1] let key = attr.value // 获取注册的指令并执行 if (directives[name]) { node.vm = this.vm // 执行指令绑定 directives[name].bind(node, { name: name, value: this.vm[key], argument: arg, expression: key }) } } }) } compileText(node) { // 利用正则表达式匹配插值表达式 let reg = /\{\{(.+?)\}\}/ const value = node.textContent if (reg.test(value)) { let key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) } } // 判断元素属性是否是指令,简化vue原来逻辑,当初默认只有v-结尾的属性是指令 isDirective(attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode(node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode(node) { return node.nodeType === 1 }}
批改vue的构造函数,启动模版编译。
constructor(options) { this.$options = Object.assign(Vue.options || {}, options || {}) this.$data = options.data || {} this.$methods = options.methods || {} if (options && options.el) { this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el } this._proxyData(this.$data) this._proxyMethods(this.$methods) // 实现数据拦挡 new Observer(this.$data) // 启动模版编译 new Compiler(this)}
dep
dep负责收集某个属性的所有观察者,当属性值发生变化的时候,会顺次执行观察者的update办法。
- 属性
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() }) }}
当初的问题是何时增加观察者,何时触发更新?
从上图能够看出,应该在Observer中触发拦挡的时候对Dep进行操作,也就是get的时候增加观察者,set时触发更新。
批改observer的defineReactive
办法:
defineReactive(data, key, val) { const that = this // 创立dep对象 const dep = new Dep() // 如果val是一个对象,为对象的每一个属性增加拦挡 this.walk(val) Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { // 增加依赖 // 在watcher中,获取属性值的时候,会把相应的观察者增加到Dep.target属性上 Dep.target && dep.addSub(Dep.target) return val }, set(newValue) { if (val === newValue) { return } // 如果赋值为一个对象,为对象的每一个属性增加拦挡 that.walk(newValue) val = newValue // 触发更新 dep.notify() } })}
watcher
watcher是观察者对象,在vue对象的属性发生变化的时候执行相应的更新操作。
- 办法
update
执行具体的更新操作
- 实现
class Watcher { // vm: vue实例 // key: 监控的属性键值 // cb: 回调函数,执行具体更新 constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 指定在这个执行环境下的watcher实例 Dep.target = this // 获取旧的数据,触发get办法中Dep.addSub this.oldValue = vm[key] // 删除target,期待下一次赋值 Dep.target = null } update() { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) this.oldValue = newValue }}
因为须要数据双向绑定,在compiler编译模版的时候,创立Watcher实例,并指定具体如何更新页面。
compileElement(node) { const directives = this.vm.$options.directives Array.from(node.attributes).forEach(attr => { // 判断是否是指令 let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text // 获取指令的相干数据 let attrNames = attrName.substr(2).split(':') let name = attrNames[0] let arg = attrNames[1] let key = attr.value // 获取注册的指令并执行 if (directives[name]) { node.vm = this.vm // 执行指令绑定 directives[name].bind(node, { name: name, value: this.vm[key], argument: arg, expression: key }) new Watcher(this.vm, key, () => { directives[name].update(node, { name: name, value: this.vm[key], argument: arg, expression: key }) }) } } }) }