背景
一直在使用vue作为前端框架,预计将来很长一段时间,前端框架都将持续存在并发展,因此,更加深入了解vue便是一件非常重要的事。要想真正深入了解vue,除了根据问题去查询他人对vue的看法外,直接自行实现一个简版的vue,也是非常好的方式。
简要分析
-
vue 的特点:响应式。具体点:根据数据模型变动情况,自行更新视图。因此,实际上要实现两个基本的功能:①监听数据模型的变动;②更新视图。最后,还要有一个将两者联系起来的中间桥梁,去进行分发。
-
做到监听的前提条件,也就是采用es6约定的 Object.defineProperty() 中的 get 和 set。举个例子:
let responsiveObj = {name: '', age: '', intro: ''} Object.defineProperty(responsiveObj, 'name', { get () { console.log('someone is getting name now') return 'long' }, set (val) { console.log('someone is setting name now') this._name = val } })通过这种方式,就能够在每一次读取值的时候和更新值的时候进行一次处理,在这个处理中,就可以加入我们想要的动作。
-
那么,我们想要做什么呢?
如上所说,我们需要①对于每一个 ‘希望响应式更新’ 的实例,都对自己 ‘用过的值’ 进行监听。 ②数据模型中的每一个项,都要保存 ‘谁引用了自己’ 的列表。 ③数据模型中的每一项进行了值更新时,就循环通知 ‘引用了自己的实例’,并把新的值一块儿传过去。④每一个收到了通知的实例,都要根据更新的内容对自身做一些改变。在vue中,①就对应着我们写的 <template> 中的每个节点,在html中,也就直接对应着 ‘视图’,因为要具有‘监听’的功能,因此每个节点都要有一个 Watcher 实例; ②对应着一个类 Dependency,数据模型也就是 export default 中的部分内容,包括 data () {}、 computed () {},也就意味着数据模型中的每一项都要有一个Dependency实例; ③对应着我们在上面的例子中写的 set 后面要进行的处理。④对应着①中的每一个实例要对自己做的事,也就是做 ‘替换视图’ 的事,这是由节点中的另一个 Complier实例 去实现。
总结一下几个类
-
Watcher 订阅者
这是每一个节点具有的实例,用来监听自己需要的值的变化情况,依据监听的结果,对节点进行处理(complier)。 -
Observer 观察者
数据模型中的每一项都是一个Observer,实际就是使用 Object.defineProperty() 实现的。 -
Dependency 依赖项
数据模型中的每一项都具有此实例,用来收集对某个数据源有依赖的节点。 -
Complier 编译者
每一个节点具有的实例,用于进行 DOM 的更新。
探讨一下‘名字’
在上面,我们用了 Watcher 订阅者,Observer 观察者,Dependency 依赖项,Complier 编译者 这几个名字,之前看这一块儿内容的时候,总觉得名字起的很奇怪,尤其是不懂 Watcher 和 Observer 的区别,以自己二流的英语能力,会把 Watcher 翻译成观察者,而 Observer 似乎更像是 ‘被观察对象’, 把 Dependency 理解成 ‘在观察自己的实例的列表’
但其实,‘观察者’ 指的是 ‘更近距离’ 地接触到 ‘被观察对象’ 的实例,当这个观察者发现被观察的内容发生了变化(调用了 set , 例如 $data.name = ‘xxx’),这里的 $data.name 才是被观察的对象,而在 set 中所执行的一系列操作,都是 ‘观察者’ 在做自己该做的事(调用一些方法让节点知道值发生了变化)。
而 Watcher 实际是一个 ‘订阅者’ 的概念,就像一个读者在出版社的 ‘订阅表’ 上填上了自己的地址(添加到 数据模型的 Dependency 实例中),当出版社出新书的时候(值发生了变化),出版社就会拿着新书(新值),送到每一位订阅了的读者所填的地址去。
依赖项 Dependency,其实表示的是 ‘依赖了当前数据模型的节点’ ,而不是 ‘自己所依赖的数据’。
实现一下几个方法
// 用来控制 Watcher 加入到 Dependency 中的全局变量
this.globalWatcher = null
function defineObj (obj, key, val) { // 实际进行响应式改造的方法
let dep = new Set() // 依赖收集,可以把这个抽象成一个 Dependency 类
Object.defineProperty(obj, key, {
get () {
if (globalWatcher) {
dep.add(globalWatcher) // 把当前读取该值的 watcher 添加到依赖项中
globalWatcher = null
}
return val
},
set (newVal) {
val = newVal // 赋新值
observe(val) // 变成响应式
// 通知依赖项中的每一个watcher
Array.from(dep).forEach(item => {
item.update && item.update(val)
})
}
})
}
function observe (obj) { // 处理‘是否为对象’
let isObj = (tar) => Object.prototype.toString.call(tar) === '[object Object]'
if (isObj(obj)) {
Object.keys(obj).forEach(prop => {
if (isObj(obj[prop])) { // 子属性为对象,则往下遍历
observe(obj[prop])
} else {
defineObj(obj, prop, obj[prop])
}
})
}
}
// 网上大家基本都是用 es6 中的 class 关键字实现的
// 这里试着用原始版的 js类 实现方式
function Watcher (val, prop) { // 观察者类
globalWatcher = this
this.nameVal= val[prop]
}
Watcher.prototype.update = function (val) { // 接收更新后的处理
console.log('update this val in update')
// 这里可以做很多事情,编译器的调用也可以在这里
this.nameVal = val
}
// 举个例子,定义一个数据模型
let obj = {
name: 'long',
age: 18,
say () {
console.log('hello , I am ' + this.name + ', and I am ' + this.age + ' years old.')
}
}
// 把这个数据模型变成响应式的
observe(obj)
// 创建一个观察数据模型中的name的实例
let eleNameNode = new Watcher(obj, 'name')
// 之后改变 obj.name 的时候,都会触发到 eleNameNodede 原型上 update 方法
// 虽然现在 update 中只进行了 console.log ,但这里面可以添加一切想做的事
obj.name = 'longalong' // => 'update this val in update'
总结一下
上面的内容,还没有包含 Complier,这部分主要是和正则表达式进行字符串解析相关,我目前对正则表达式的掌握还远远不够,因此暂时省略了这部分内容,实现了响应式的前半部分,后半部分明天再弄一下。
要去做的
从上面写这篇文章的过程,能看出我至少在以下几个方面的熟练度是不够的,认识是不清晰的:
- 面向对象的思想。 => 总是在想要怎么‘一步步实现’,而不是‘实现这个功能的实例要具有什么方法’。
- 正则表达式。 => 嗯,只知道点皮毛,应用层面差得不是一点半点。
- DOM 对象实例。 => 对DOM对象的理解太浅,没有真正把它当做一个 对象 去看待。
- 数据结构与算法。 => 任何问题,最终都可以归结到数据结构上,捋清数据结构,就能比较清晰地明白用什么样的算法去实现。
本文深入探讨了Vue框架的响应式系统实现,通过手写简化版Vue,详细解析了Watcher、Observer、Dependency及Complier等核心组件的作用与交互机制。

被折叠的 条评论
为什么被折叠?



