vue使用MVVM框架,MVVM框架核心是数据双绑。即通过数据劫持和发布订阅模式,使用Object.defineProperty()对数据的属性和子属性进行递归遍历并对其设置getter和setter方法,数据发生改变时,发布消息给订阅者,出发对应的监听回调渲染视图。达到数据更新则视图更新,视图更新则数据更新的效果。
主要涉及三个核心:Observe、Compile、Watcher。
1:new Vue()时进行初始化,并在Observe中对data数据进行数据劫持(响应式处理)。
2:在Compile中进行模版编译,找到其中动态绑定的数据,从data
中获取并初始化视图
3:定义一个dep()函数和watcher,由watcher对数据的变化和更新进行统一处理
4:data中的属性,称为key,一个视图中key可能会出现多次;一个watcher对应一个key;相同key对应的watcher存在一个dep()中。即一个watcher对应一个key,但是一个dep()里面存放多个相同key的watcher
5:data数据发生变化时,首先找到对应的dep(),然后通知watcher执行更新函数
流程图如下:
通过Object.defineProperty()对数据进行数据劫持
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
对每个元素节点及其子元素节点进行递归遍历,对元素上的节点和文本进行编译,把模板中的变量替换成数据,然后初始化渲染视图,同时把每个指令对应的节点绑定上更新函数,添加订阅者,如果数据变化,收到通知,更新视图
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
视图中会用到data
中某key
,称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集。多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知。
defineReactive
时为每⼀个key
创建⼀个Dep
实例 (Observe) key
,例如name1
,创建⼀个watcher1
(Compile) name1
的getter
方法,便将watcher1
添加到name1
对应的Dep中(Observe中调用addDep())name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新Watcher类负责更新视图:创建实例时,把当前实例指定到Dep.targer静态属性上;然后读取key,出发getter,再将Dep.target置空;当执行Dom更新函数时,由dep()调用进行Watcher进行更新
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
4.4Observe中
创建Dep
实例,并依赖收集
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
Observe: 递归遍历data上的属性和子属性,并通过Object.defineProperty()进行数据劫持;同时创建dep实例,并在Object.defineProperty()的get()上将watcher设置进dep中
Compile: 编译解析模版指令,并将模版中的变量替换成数据,然后初始化渲染视图,并给每个节点指令绑定上更新函数,添加订阅者。当数据变化时,收到通知更新视图
依赖收集: 初始化渲染视图时就会触发属性的getter方法,并调用Dep中的addDep()将值设置进Dep中;而数据中每个属性key对应一个watcher,多个相同key的watcher存放在一个Dep中;数据变化时,会首先找到对应的Dep
,通知Dep对应的所有Watcher
执行更新函数
参考地址:
面试官:双向数据绑定是什么 | web前端面试 - 面试官系列