共计 3626 个字符,预计需要花费 10 分钟才能阅读完成。
前言
Vue 最独特的个性之一,是其非侵入性的响应式零碎。比方咱们批改了数据,那么依赖这些数据的视图都会进行更新,大大提高了咱们的 ” 搬砖 ” 效率,回忆一下初学 JS 的时候海量的 Dom 操作~.~……,Vue 通过数据驱动视图,极大的将咱们从繁琐的 DOM 操作中解放出来。
如下图,咱们扭转了 msg 的值,视图也响应式的进行了更新
Vue 响应式原理
咱们先看 vue 官网的图,其实不太清晰,我初看的时候也是一脸懵逼的~.~:
再看上面这张图,响应式原理涵盖在外面了(图片来源于网络):
梳理一下流程:
-
- Vue 初始化 => 劫持 data 设置 get、set(拦挡数据读写)
-
- Compile 解析模板 => 生成 watcher => 读取 data,触发 get 办法 => Dep 收集依赖(watcher)
-
- 数据变动 => 触发 set 办法 => 告诉 Dep 中的所有 watcher => 视图更新
对于 Observer、Dep 和 Watcher 这三大金刚,我初学的时候也是傻傻的分不清楚很懵,我的了解是:
Dep(dependence) 即依赖收集器,收集 Watcher 即观察者。
Watcher 即观察者,察看数据,数据变动时更新对应的视图(dom)。
Observer 即劫持者,通过 Object.defineProperty() 给数据设置 get 和 set 办法:
- get: 当某个中央用到数据时,如下 h1、h2 标签都用到了 msg 数据,即察看 msg 数据 的两个 watcher 将被放入 msg 数据的依赖收集器 Dep 中。
data() {
return {msg: 'hello vue',}
},
<h1>{{msg}}</h1>
<h2>{{msg}}</h2>
- set:当 msg 数据扭转的时候,遍历 Dep 依赖收集器,告诉所有 Watcher 更新视图,即更新 h1、h2 标签内的文本内容
实现 Vue 的响应式零碎
通过下面剖析,可知每一个数据有一个依赖收集器 Dep,Dep 外面寄存用到该数据的 Watcher,如下图所示(图片来源于网络):
1. Dep
咱们先实现 Dep,Dep 咱们能够用数组来模仿,它应该有两个办法:
- add,收集 Watcher
- notify,数据变动的时候告诉 Watcher 更新视图
# 依赖收集器
class Dep {constructor() {this.subs = [];
}
addSub(watcher) {
# 增加观察者
this.subs.push(watcher);
}
notify() {
# 告诉每一个观察者更新视图
this.subs.forEach(watcher => watcher.update());
}
}
2. Watcher
Watcher 实现如下,其中 cb 是更新视图的办法,关键点在于 oldVal,它有两个用途:
- Dep 触发 update 办法时,比对新旧值,若有变动才更新,防止不必要的视图更新
- 初始化的时候,会获取旧值,会触发数据的 get 办法,在此时能够把依赖注入到 Dep 中(即依赖收集)
# 观察者,用于更新视图
class Watcher {constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
# 视图更新函数
this.cb = cb;
# 旧值
this.oldVal = this.getOldVal();}
getOldVal() {
# 传递 watch 本人
Dep.target = this;
# 获取值的时候会触发 get 办法,把本人 push 进 deps[] 里
const oldVal = compileUtils.getVal(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
# 获取新值
const newVal = compileUtils.getVal(this.expr, this.vm);
if (newVal !== this.oldVal) {this.cb(newVal);
}
}
}
Dep.target = this
的用途是相当于设置了一个全局变量 让 Dep 能收集到 watcher 本人,前面 Dep.target = null
用途是销毁全局变量
3. Observer
Observer 实现如下,通过 Object.defineProperty 拦挡数据的读写操作:
- get 收集依赖,留神判断 Dep.target 是否有值,因为模板解析的时候也会读取数据触发 get 办法
- set 告诉依赖收集器,更新视图
// 数据劫持
class Observer {constructor(data) {this.observer(data, key, data[key]);
}
observer(obj, key, value) {const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
# 避免视图初始化的时候也被收集到 Dep 中
Dep.target && dep.addSub(Dep.target);
return value;
},
set: newVal => {this.observer(newVal);
if (newVal !== value) {
value = newVal;
# 告诉依赖收集器,有变动
dep.notify();}
},
});
}
}
4. Compile
到这里咱们曾经实现了 Observer、Dep 和 Watcher,实现了数据的响应式追踪,可是还有一个点没买通,那就是 依赖收集,那么依赖什么时候收集呢?换言之咱们怎么晓得哪些数据依赖了哪些视图呢?
在 Vue 解析模板的时候,实际上咱们曾经晓得了哪些 Dom 依赖了哪些数据,所以是在 compile 的时候实现了模板解析并实现了依赖收集。
Compile 实现如下,省略大部分 dom 操作相干代码,能够用 DocumentFragment 文档碎片晋升性能,逻辑比较简单,咱们在 dom 解析数据的时候生成了对应的 watcher,并实现了依赖收集:
# 编译类,输入实在 Dom
class Compile {constructor(el, vm) {this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
# 获取文档对象
const fragment = this.nodeFragment(this.el);
# 编译
this.compile(fragment);
# 挂载回 app
this.el.appendChild(fragment);
}
# 是否元素节点
isElementNode(node) {return node.nodeType === 1;}
# 获取文档碎片
nodeFragment(el) {# do something}
compile(fragment) {
const childNodes = fragment.childNodes;
[...childNodes].forEach(node => {if (this.isElementNode(node)) {
# 元素节点
# do something
} else {
# 文本节点
# do something
}
})
}
}
# 依据不同指令 执行不同的编译操作
const compileUtils = {
# v-text
text(node, expr, vm) {const value = vm.$data[expr];
# 创立观察者 实现依赖收集
new Watcher(vm, expr, newVal => {node.textContent = value;});
node.textContent = value;
},
};
至此一个响应式的零碎就曾经完了
双向数据绑定
什么是双向数据绑定
下面咱们实现了响应式的零碎,但只是单向的,即数据驱动视图,什么是双向数据绑定呢?如下图:
咱们常见的 v-model,就是双向数据绑定,其实它是一个语法糖:
<input v-model="msg" />
等价于 =>
<input :value="msg" @input="msg = $event.target.value" />
实现
双向数据绑定即:
- 数据扭转 => 视图更新
- 视图扭转 => 数据扭转 => 视图更新
比方最简略的 input,咱们只须要监听 input 事件,文本发生变化时更新数据,触发数据的 set 办法,告诉所有的 watcher 更新视图
咱们在模板编译的时候,给 dom 元素绑定相应的事件,如 input 标签绑定 input 事件并指定更新数据的回调函数:
const compileUtils = {
# v-model
model(node, expr, vm) {const value = vm.$data[expr];
# 创立观察者 实现依赖收集
new Watcher(vm, expr, newVal => {node.value = value;});
node.addEventListener('input', (e) => {
# 更新数据,触发数据的 set 办法
vm.$data[expr] = newVal;
});
node.value = value;
},
};
至此功败垂成
源码
源码
END