前情提要
各位好,最近在复习完 Vue 的响应式原理后,做一篇学习笔记。
Vue 一直都是热门的前端框架,那么 Vue 究竟是怎么实现的呢?今天我们通过手写一个简单版 Vue 带大家了解一下。
今天讲解的是 Vue 2.x 也就是通过 Object.defineProperty 实现响应式的相关知识,Vue 3 通过 Proxy 实现的相关知识我们放在下篇文章中讲。
完整项目地址:github.com/zhtzhtx/Ter…
Vue
我们都知道使用 Vue 2.x 是通过 new Vue() 来初始化 Vue 实例的,所以我们先来实现 Vue 这个类,它的功能是:
- 负责接收初始化的参数(选项)
- 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
- 负责调用 observer 监听 data 中所有属性的变化
- 负责调用 compiler 解析指令/插值表达式
Vue 这个 class 接受一些参数,用来初始化 Vue 的属性
class Vue {
constructor (options) {
// 1. 保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
const el = options.el
this.$el = typeof options.el === 'string' ? document.querySelector(el) : el
}
}
接下来,我们需要遍历 data 中的属性并将它们挂载到 Vue 的实例上,因为我们在 Vue 中是通过 this.xx 来读取 data 中的数据的。
我们先定义一个_proxyData 方法用来遍历 data 的所有属性并通过 Object.defineProperty 方法将它们挂载到 Vue 实例上。
_proxyData (data) {
// 遍历 data 的所有属性的 key
Object.keys(data).forEach(key => {
// 这里的 this 指向 Vue 实例
Object.defineProperty(this, key, {
get () {
return data[key]
},
set (newValue) {
// 如果新值和旧值一样直接返回
if (data[key] === newValue) return
data[key] = newValue
}
})
})
}
好了,我们在 Vue 的构造函数中调用_proxyData 方法
class Vue {
constructor (options) {
// 1. 保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
const el = options.el
this.$el = typeof options.el === 'string' ? document.querySelector(el) : el
// 2. 把data中的成员转换成getter和setter,注入到vue实例中
this._proxyData(this.$data)
}
}
这样,Vue 这个类就初步构造完成了。
Observer
接下来,我们来构建 Observer 这个类,它的作用是将 data 中的数据改成响应式数据。在 Vue 中我们通过 this.xx 修改数据时,data 中的数据也会自动修改。
当调用 Observer 这个类时,我们传入 Vue 实例的 data 属性,然后定义一个 walk 方法,用来将 data 属性所对应的对象转化为响应式对象。
class Observer {
constructor(data) {
// 我们希望构造类之后,立即将传入的data中的属性转换成getter/setter,所以在构造函数中调用walk()
this.walk(data)
}
walk(data) {
// 判断传入值是否为对象,对象才能转化为响应式
if (!data || typeof data !== "object") return
Object.keys(data).forEach(key => {
// 对各个属性进行数据拦截
this.defineReactive(data, key, data[key])
})
}
}
然后,我们来写 defineReactive 方法,它和之前写的_proxyData 方法类似
defineReactive(obj, key, value) {
const _this = this
// 如果val是对象,把val内部的属性转换成响应式数据
this.walk(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 这里不能直接使用data[key],因为会调用get方法,陷入死循环
return value
},
set(newValue) {
if (newValue === value) return
// 这里形成一个闭包,一直储存value值
value = newValue
// 判断传入的值是不是对象,如果是,转化为响应式对象
// 这里的this指向obj,而不是Observer实例,所以设置_this
_this.walk(newValue)
}
})
}
好了,这样 Observer 这个类就初步完成了,我们需要在 Vue 的构造函数调用 Observer 创建实例
class Vue {
constructor (options) {
// 1. 通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
// 2. 把data中的成员转换成getter和setter,注入到vue实例中
this._proxyData(this.$data)
// 3. 调用observer对象,监听数据的变化
new Observer(this.$data)
}
}
Compiler
下面我们来编写 Compiler 这个类,它的作用是根据 Vue 中的 api, data 中的数据转化成文本节点。比如下面的例子中,msg 和 count 就是 data 中的数据,那么它们如何在初始化 Vue 实例后,转化为文本节点呢?这就体现了 Compiler 的作用。
好了,我们来看看 Compiler 这个类, 首先它接受 Vue 实例作为参数,因为所有的 data 数据都是挂载在 Vue 实例上的。
class Compiler {
constructor(vm) {
// 挂载Vue实例
this.vm = vm
// 挂载根节点
this.el = vm.$el
// 编译模板
this.compile(this.el)
}
}
然后,我们来看 compile 方法,首先获取根节点下所有子节点,其次遍历所有子节点,根据是文本节点还是 DOM 节点来编译数据
// 编译模板
compile(node) {
// 获取根节点下所有子节点
const childNodes = node.childNodes
Array.from(childNodes).forEach(childNode => {
if (this.isTextNode(childNode)) {
// 判断是否为文本节点
this.compileTextNode(childNode)
} else if (this.isElementNode(childNode)) {
// 判断是否为元素节点
this.compileElementNode(childNode)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (childNode.childNodes && childNode.childNodes.length) {
this.compile(childNode)
}
})
}
我们可以根据 node.nodeType 来判断是文本节点还是 DOM 节点
// 判断是否为文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断是否为元素节点
isElementNode(node) {
return node.nodeType === 1
}
在用于编译文本节点的 compileTextNode 方法中,使用正则表达式获取双花括号中的 data 数据的 key,将其转化为 data 数据
// 编译文本节点
compileTextNode(node) {
let value = node.textContent
let reg = /\{\{(.+?)\}\}/
if (reg.test(value)) {
// 获取{{}}中的文本
const key = RegExp.$1.trim()
// 替换{{}}中的文本
node.textContent = value.replace(reg, this.vm[key])
}
}
在用于编译 DOM 节点的 compileElementNode 方法中,先获取 DOM 节点的所有属性,遍历然后判断是否是 Vue 的指令,比如 "v-text" ,根据不同的指令将相应的数据加载到 DOM 节点上。
// 编译元素节点
compileElementNode(node) {
Array.from(node.attributes).forEach(attr => {
// 获取属性的名称
let attrName = attr.name
// 判断是否为指令
if (this.isDirective(attrName)) {
// 如"v-text"截取"text"
attrName = attrName.substring(2)
const key = attr.value
this.update(node, key, attrName)
}
})
}
我们可以通过是否以 "v-" 开头判断是否为 Vue 的指令
// 判断是否为指令
isDirective(attrName) {
return attrName.startsWith("v-")
}
由于是简化版 Vue,这里我们只编译 v-text 和 v-model 两种指令
// 编译指令
update(node, key, attrName) {
// 这里指 textUpdater 方法和 modelUpdater 方法
const updateFn = this[attrName + "Updater"]
// 将this指向Compiler实例
updateFn && updateFn.call(this, node, this.vm[key], key)
}
先看编译 "v-text" 的 textUpdater 方法,直接将 DOM 节点的文本内容替换。
textUpdater(node, value, key) {
node.textContent = value
}
再来看编译 "v-model" 的 modelUpdater 方法,将 input 中的值替换同时监听 input 输入的值,将其同步到 Vue 的 data 数据中
// 编译v-model
modelUpdater(node, value, key) {
node.value = value
node.addEventListener("input", () => {
this.vm[key] = node.value
})
}
好了,这样 Compiler 这个类就完成了,记得在 Vue 中生成它的实例
class Vue {
constructor(options) {
// 1. 通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el
// 2. 把data中的成员转换成getter和setter,注入到vue实例中
this._proxyData(this.$data)
// 3. 调用observer对象,监听数据的变化
new Observer(this.$data)
// 4. 调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
}
Dep
接着,我们来看 Dep 这个类,因为 Vue 强调的是响应式数据,也就说当 data 中数据变化后自动同步到页面中,那么谁来发布更新呢?这就是 Dep 的作用,它是发布/订阅模式中的事件中心。
在 Dep 的构造函数中初始化一个数组,用于存储所有的监听器
class Dep{
constructor(){
// 存储所有的监听器
this.subs=[]
}
}
定义一个 addSubs 方法用于将监听器存储在数组,如果一个数据包含 update 方法,我们就判定它是监听器
// 添加监听器
addSubs(sub){
if(sub&&sub.update){
this.subs.push(sub)
}
}
定义一个 notify 方法用于当有 data 数据更新时,通知所有的监听器更新
notify(){
this.subs.forEach(sub=>{
sub.update()
})
}
好了,这样 Dep 这个类就完成了,我们需要在 Observer 这个类的 defineReactive 方法中进行依赖收集,当数据更新时通知所有监听器更新
defineReactive(obj, key, value) {
const _this = this
// 负责收集依赖,并发送通知
const dep = new Dep()
// 如果val是对象,把val内部的属性转换成响应式数据
this.walk(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newValue) {
if (newValue === value) return
// 这里形成一个闭包,一直储存value值
value = newValue
// 判断传入的值是不是对象,如果是,转化为响应式对象
// 这里的this指向obj,而不是Observer实例,所以设置_this
_this.walk(newValue)
// 通知依赖更新
dep.notify()
}
})
}
Watcher
最后,我们来看 Watcher 这个类,它就是我们在 Dep 类中提到的监听器。它接受 Vue 实例、监听数据对应的 key还有回调函数作为参数,由于它需要在遍历 data 数据时添加到 Dep 类的数组中,所以我们将它挂载到全局对象上,这里如果挂载在 Window 上也可以,为了方便理解我们将它挂载在 Dep 类上
class Watcher {
constructor(vm, key, callback) {
// 挂载Vue实例
this.vm = vm
// data中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = callback
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSubs
this.oldValue = vm[key]
Dep.target = null
}
}
我们在 Watcher 中定义一个 update 方法,用于当收到 Dep 类通知时调用回调函数来更新视图
// 当数据发生变化的时候更新视图
update() {
let newValue = this.vm[this.key]
if(this.oldValue === newValue) return
// 调用回调函数
this.cb(newValue)
}
Watcher 类是在 Compiler 类中的compileTextNode 方法、textUpdater 方法和 modelUpdater 方法中初始化实例并获取对应的更新视图的回调函数
// 编译文本节点
compileTextNode(node) {
let value = node.textContent
let reg = /\{\{(.+?)\}\}/
if (reg.test(value)) {
// 获取{{}}中的文本
const key = RegExp.$1.trim()
// 替换{{}}中的文本
node.textContent = value.replace(reg, this.vm[key])
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
}
// 编译v-text
textUpdater(node, value, key) {
node.textContent = value
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
// 编译v-model
modelUpdater(node, value, key) {
node.value = value
node.addEventListener("input", () => {
this.vm[key] = node.value
})
new Watcher(this.vm, key, newValue => {
node.value = newValue
})
}
在 Observer 类的 defineReactive 方法中,需要将其添加到 Dep 实例存储监听器的数组中
defineReactive(obj, key, value) {
const _this = this
// 负责收集依赖,并发送通知
const dep = new Dep()
// 如果val是对象,把val内部的属性转换成响应式数据
this.walk(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 这里不能直接使用data[key],因为会调用get方法,陷入死循环
const watcher = Dep.target
watcher && dep.addSubs(watcher)
return value
},
set(newValue) {
if (newValue === value) return
// 这里形成一个闭包,一直储存value值
value = newValue
// 判断传入的值是不是对象,如果是,转化为响应式对象
// 这里的this指向obj,而不是Observer实例,所以设置_this
_this.walk(newValue)
// 通知依赖更新
dep.notify()
}
})
}
总结
好了,最后让我们总结一下 各个类的功能
-
Vue
- 记录传入的选项,设置 el
- 把 data 的成员注入到 Vue 实例
- 负责调用 Observer 实现数据响应式处理(数据劫持)
- 负责调用 Compiler 编译指令/插值表达式等
-
Observer
-
数据劫持
- 负责把 data 中的成员转换成 getter/setter
- 负责把多层属性转换成 getter/setter
- 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
-
添加 Dep 和 Watcher 的依赖关系
-
数据变化发送通知
-
-
Compiler
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染过程
- 当数据变化后重新渲染
-
Dep
- 收集依赖,添加订阅者(watcher)
- 通知所有订阅者
-
Watcher
- 自身实例化的时候往dep对象中添加自己
- 当数据变化dep通知所有的 Watcher 实例更新视图