1. 自己动手实现响应式
1.1. 原理
1.1.1. 两个问题
首先给你如下的一段代码,要实现响应式,你需要考虑什么问题?
<div>
{{ message }} <!--这个人是张三-->
{{ message }} <!--这个人是李四-->
{{ message }} <!--这个人时候王五-->
{{ name }}
</div>
<script>
const app = new Vue({
data: {
message: 'hello',
name: 'yu wan'
}
})
</script>
- app.message 修改后,vue 内部如何监听 message 发生改变?
- vue 监听到它发生改变,怎么通知页面中的使用者更新?
答案:Object.defineProperty() -> 监听对象属性的改变;通过发布-订阅模式 -> 让对应的使用者更新
1.1.2. Object.defineProperty()
下面看一下,Object.defineProperty 大致是怎么实现数据劫持的
let data = { // 假设这是 Vue.data 对象
message: 'hello',
name: 'yu wan'
}
Object.keys(data).forEach(key => {
let value = data[key]
Object.defineProperty(data, key, {
set(newVal) {
// 这里的逻辑应该是什么呢?既然外部通过 data.message = 'xxx' 来修改了它,那我就应该通知它的使用者更新数据塞
// 如上面的 message,是张三、李四、王五在用,就要通知他们仨,这就需要发布-订阅模式了
value = newVal
},
get() {
// 这里的逻辑又应该是什么呢?既然页面能够显示 message 的值,说明这里就要记录哪些人用的是 message,哪些人用的是 name 塞
return value
}
})
})
// 只要调用 data.name -> 就会触发 get()
// 只要调用 data.name = 'xxx' -> 就会触发 set()
1.1.3. 发布-订阅模式
那就先说说,发布-订阅模式这里面的一些属于吧(指的是 vue 源码中用到的名称)
- 发布者(Dep):它就像个公众号,有新的文章发出来了,就会推送给所有的用户
- 订阅者(Watcher):它就好比用户,都去关注某个公众号,有新的文章,用户就会去读这个文章
class Dep { // dependency-发布者
constructor() {
this.subs = [] // subscribes-存放它所有订阅者
}
addSub(watcher) {
this.subs.push(watcher) // 用户添加订阅
}
notify() { // 给所有订阅者推送消息
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 让所有的 message 用户修改它们的值
}
}
}
class Watcher { // 订阅者
constructor(name) {
this.name = name // 只是一个示例,区分不同订阅者
}
update() {
console.log(this.name + '更新了!');
}
}
// 相当于执行了上面 Object.keys(obj).forEach() 里的 get() 方法,就会让那几个人来订阅
const dep = new Dep()
const w1 = new Watcher('张三')
dep.addSub(w1)
const w2 = new Watcher('李四')
dep.addSub(w2)
const w3 = new Watcher('王五')
dep.addSub(w3)
// 一旦修改 data.message ,就在 set() 那里通知所有人更新
dep.notify() // 说明每个字段都有一个 dep,如 message 有一个 dep 记录着,name 有一个 dep 记录着
1.1.4. el
el 需要干啥呢?
- 正如第一段代码,我们传给了 Vue() 一个 el 属性,肯定要把 #app 里面的
{{}}
用法都解析出来,然后再展示里面的数据 - 还有一个问题,我们怎么知道 message 就刚好三个人用呢?,因此在解析的时候还需要根据
{{}}
来生成对应的 Watcher,并添加到相应的 Dep 中
1.1.5. 原理图
这个时候,把原理图拿出来,应该都能够明白了吧 🥰
- 把 data 交给 Observer,它来劫持数据,然后为每个字段(message、name)生成相应的 Dep 对象
- el 读取到 #app 的内容,把里面的字段({{name}}、{{message}})都生成 Watcher,然后分别订阅 Dep(即加入到相应的 Dep 对象中)
- Observer 里 set() 那里监听到变化,就通知对应的 dep,然后 dep.notify() 就会让所有使用 message 的人更新数据
1.2. 实践
说了这么多,下面就来写一个最简单的双向绑定,先准备好我们的 HTML🤗
<div id="app">
<!-- https://developer.mozilla.org/zh-CN/docs/Web/API/Node ,这里因为换行了,所以 app 有三个子节点 Text、input、{{username}} -->
<input type="text" v-model="username"/>{{ username }}
</div>
1.2.1. 所有的类写出来
先写出这五个必须用到的类:Vue
、Observer
、Dep
、Watcher
、Compiler
,其他的逻辑下一步再写
<script>
class Vue {
constructor(options) {
}
}
class Observer {
constructor() {
}
}
class Dep {
constructor() {
}
}
class Watcher {
constructor() {
}
}
class Compiler {
constructor() {
}
}
</script>
1.2.2. 基础逻辑
根据上面的原理图,我们知道 new Vue({}) 的时候,是分两步走的,一部分是去交给了 Observer,另一部分是把 el 拿去解析了,因此可以添加下面的一些代码
<script>
class Vue {
constructor(options) {
// 保存数据
this.$options = options
this.$el = options.el
this.$data = options.data
new Observer(this.$data) // 1.
new Compiler(this.$el, this) // 2.可想而知,我们需要在 $el 获取到页面元素后,也能够拿到 vm(this).data 的值才能够显示,因此需要传过去
}
}
class Observer {
constructor(data) { // 3.Observer 是用来进行数据劫持的,因此我们应该添加下面的代码
this.data = data
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, val) { // 4.封装了一下
const dep = new Dep() // 5.每个字段都会有自己的 Dep 嘛,所以这里 new 一个
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if(Dep.target){ // 13.这里要统计哪些人使用了 message/name 分别加入到他们的 Dep 中
dep.addSub(Dep.target) // 可以统计人数了
}
return val
},
set(newVal) {
if (val === newVal) return;
val = newVal // 这必须要噢,不然值修改不了,可以测试上面小节,如果不写是无法更新的
dep.notify() // 这里就应该是通知所有使用者更新了
}
})
}
}
const regex = /\{\{(.*)\}\}/
class Compiler {
constructor(el, vm) { // 7.
this.el = document.querySelector(el)
this.vm = vm
this.frag = this._createFragment() // 8.什么叫文档片段呢?我查了一下,就像个占位符,子节点会填充它;然后它存在于内存中,子节点填充它时不会引起页面回流(也叫重排,就是计算这部分元素的位置、尺寸等信息),因此性能更好
this.el.appendChild(this.frag) // 14.相当于把 #app 的内容创建一份文档片段,然后填充给它
}
_createFragment() { // 创建文档片段
const frag = document.createDocumentFragment()
let child;
while (child = this.el.firstChild) {
this._compile(child)
frag.appendChild(child) // 9.分别把 input Text 添加至 frag 里,最后 this.el.childNodes 就会为空,因此就退出循环啦
// https://segmentfault.com/a/1190000009912513 这里说明了 frag 添加一个现有节点,原节点会被删除(这也就是为什么打包后的文件会替换app内容的一种方式吧)
}
return frag
}
_compile(node) { // 解析每个节点
if (node.nodeType === 1) { // 元素节点
const attrs = node.attributes
if (attrs.hasOwnProperty('v-model')) {
const name = attrs['v-model'].nodeValue // 获取 v-model 的值 username
// 输入框绑定初值
node.value = this.vm[name] // 10.这个输入框类型是 HTMLInputElement,只能通过 value 设置它的值,无法通过 nodeValue 设置它的值 https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement
node.addEventListener('input', e => {
this.vm[name] = e.target.value // 捕获事件,绑定值到 vm 属性上
})
}
}
if (node.nodeType === 3) { // 文本节点
if (regex.test(node.nodeValue)) {
const name = RegExp.$1.trim() // {{ username }} ,$1就是正则表达式 (a)(b) 第一组括号的值,然后去掉两边的空格,就得到 username
new Watcher(node, name, this.vm) // 11.然后就可以把这个 {{username}} 生成一个 Watcher 了
}
}
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
const subs = this.subs.slice() // slice 会返回一个新数组
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
class Watcher {
constructor(node, name, vm) {
// 因为它需要某个设置某个节点的值为 vm[name] ,所以要这三参数
this.node = node
this.name = name
this.vm = vm
Dep.target = this
this.update() // 给 {{message}} 绑定初值
Dep.target = null // 因为当 input 改变,它就会触发 get() 这个时候不置为空就会把最后这个watcher 又给添加到它对应的 dep 里了,这显然是不对的呀
}
update(){
this.node.nodeValue = this.vm[this.name] // 12.this.vm[this.name] 就相当于调用 data.username 因此会触发 get() 方法,所以就可以在那里统计人数了
}
}
</script>
1.2.3. 代理
现在上面的代码,执行会发现如下结果,其实原因很简单 绑定初值:node.value = this.vm[name]
,我们有给 app.message 赋值吗,或者说有给它添加这个属性吗?当然没有
所以这里就需要一个代理,来通过 app.message 绑定到 app.data.message
class Vue {
constructor(options) {
// ...
// 代理所有的 data 属性
Object.keys(this.$data).forEach(key=>{
this._proxy(key)
})
// ...
}
_proxy(key){
Object.defineProperty(this, key, { // 定义 this.username
enumerable: true,
configurable: true,
get() {
return this.$data[key]
},
set(v) {
this.$data[key] = v
}
})
}
}
<script>
const app = new Vue({
el: '#app',
data: {
username: 'aaa'
}
})
</script>
1.2.4. 恭喜
经过以上的操作,相信你已经掌握了基本的原理了,接下来去学习更多吧!😁