原文转自 https://const_white.gitee.io/gitee-blog/blog/vue/mini-vue/
Vue响应式原理
图片引自 孟思行 - 图解 Vue 响应式原理

乞丐版 mini-vue
实现mini-vue之前,先看看官网的描述。在Vue官网,深入响应式原理中,是这样说明的:
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

起步
技术原因,这里不做
Virtual DOM、render部分,而选择直接操作DOM
简单来说,mini vue在创建Vue实例时
Vue类负责把data中的属性注入到Vue实例,并调用Observer类和Compiler类。Observer类负责数据劫持,把每一个data转换成getter和setter。其核心原理是通过Object.defineProperty实现。Compiler类负责解析指令和插值表达式(更新视图的方法)。Dep类负责收集依赖、添加观察者模式。通知data对应的所有观察者Watcher来更新视图。在Observer类把每一个data转换成getter和setter时,会创建一个Dep实例,用来负责收集依赖并发送通知。在每一个data中在getter中收集依赖。在setter中通知依赖,既通知所有Watcher实例新视图。Watcher类负责数据更新后,使关联视图重新渲染。

实现代码都添加了详细的注释,无毒无害,可放心查看
Vue类
class Vue {
constructor(options) {
// 1. 保存 options的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. 为方便调用(vm.msg),把 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类
class Observer {
constructor(data) {
this.walk(data)
}
// 遍历 data($data)中的属性,把属性转换成响应式数据
walk(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
})
}
// 定义响应式数据
defineReactive(obj, key, value) {
const that = this
// 负责收集依赖并发送通知
let dep = new Dep()
// 利用递归使深层(内部)属性转换成响应式数据
this.walk(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
if (value === newValue) {
return
}
value = newValue
// 如果新设置的值为对象,也转换成响应式数据
that.walk(newValue)
// 发送通知
dep.notify()
}
})
}
}
Compiler类
class Compiler {
constructor(vm) {
this.vm = vm
this.el = vm.$el
this.compiler(this.el)
}
// 编译模板,处理文本节点和元素节点
compiler(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compilerText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compilerElement(node)
}
// 判断 node节点是否有子节点。如果有,递归调用 compile
if (node.childNodes.length) {
this.compiler(node)
}
})
}
// 编译元素节点,处理指令
compilerElement(node) {
// 遍历所有属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否 v-开头指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// 为了更优雅的处理不同方法,减去指令中的 v-
attrName = attrName.substr(2)
const 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
// 创建 Watcher对象,当数据改变时更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理 v-model指令
modelUpdater(node, value, key) {
node.value = value
// 创建 Watcher对象,当数据改变时更新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理插值表达式
compilerText(node) {
const reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
// 只考虑一层的对象,如 data.msg = 'hello world',不考虑嵌套的对象。且假设只有一个插值表达式。
const 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
}
}
Dep类
class Dep {
constructor() {
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
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() {
const newValue = this.vm[this.key]
// 数据没有发生变化直接返回
if (this.oldValue === newValue) {
return
}
// 更新视图
this.cb(newValue)
}
}
完整版思维导图

对于数组的监听
这里直接把数组的每一项都添加上了getter和setter,所以vm.items[1] = 'x'也是响应式的。
Vue中为什么没这样做呢?参考 为什么vue没有提供对数组属性的监听
