共计 7942 个字符,预计需要花费 20 分钟才能阅读完成。
像 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-text | |
Vue.directive('text', function (el, binding) {const { value} = binding | |
el.textContent = value | |
}) | |
// v-model | |
Vue.directive('model', function (el, binding) {const { value, expression} = binding | |
el.value = value | |
// 实现双向绑定 | |
el.addEventListener('input', () => {el.vm[expression] = el.value | |
}) | |
}) | |
// v-html | |
Vue.directive('html', function (el, binding) {const { value} = binding | |
el.innerHTML = value | |
}) | |
// v-on | |
Vue.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 | |
}) | |
}) | |
} | |
} | |
}) | |
} |