mini-vue
实现一个简单的 Vue.js。用于理解 Vue响应式原理,妈妈再也不用担心我不会用 Vue了!
技术尚未成熟,只实现小部分功能。 --2020/08/27
技术实现参考拉勾教育「大前端高薪训练营」3天体验课
完整版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)
}
}
复制代码
最后
完整版思维导图
DEMO仓库地址
对于数组的监听
这里直接把数组的每一项都添加上了getter
和setter
,所以vm.items[1] = 'x'
也是响应式的。
Vue
中为什么没这样做呢?参考 为什么vue没有提供对数组属性的监听
转自https://juejin.cn/post/6868257054676090887