前言
经过上一篇的介绍,已经实现了监听数据的变化,接下来就是要实现数据变化后,界面也跟着变化,这就是数据驱动界面改变。
想要实现数据变化之后更新UI界面,我们可以使用发布订阅模式来实现,先定义一个观察者类, 再定义一个发布订阅类, 然后再通过发布订阅的类来管理观察者类。
接下来我们就来实现这个代码。
定义一个观察者类,观察属性的变化,当属性变化时,触发回调函数。
代码语言:javascript复制class Watcher {
constructor(vm, attr, cb) {
this.vm = vm;
this.attr = attr;
this.cb = cb;
// 在创建观察者对象的时候就去获取当前的旧值
this.oldValue = this.getOldValue();
}
getOldValue() {
return CompilerUtil.getValue(this.vm, this.attr);
}
/**
* 更新的方法, 用于判断新值和旧值是否相同
* 如果不相同, 那么就调用回调函数
*/
update() {
let newValue = CompilerUtil.getValue(this.vm, this.attr);
if (newValue !== this.oldValue) {
this.cb(newValue, this.oldValue);
}
}
}
上方的类中主要有三个属性,分别是vm, attr, cb,vm是Vue的实例,attr是属性名,cb是回调函数。
定义了一个getOldValue方法,用于获取旧值,这个方法在创建观察者对象的时候就会调用,用于获取旧值。
定义了一个update方法,用于更新数据,当数据发生变化时,就会调用这个方法,用于判断新值和旧值是否相同,如果不相同,就调用回调函数。
接下来我们就来定义一个发布订阅类,用于管理观察者对象。
代码语言:javascript复制class Dep {
constructor() {
// 这个数组就是专门用于管理某个属性所有的观察者对象的
this.subs = [];
}
/**
* 订阅观察的方法
* @param watcher 观察者对象
*/
addSub(watcher) {
this.subs.push(watcher);
}
/**
* 发布订阅的方法
*/
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
上方的类中主要有两个方法,分别是addSub和notify:
- addSub方法用于订阅观察的方法,将观察者对象添加到数组中。
- notify方法用于发布订阅的方法,遍历数组中的观察者对象,调用观察者对象的update方法。
构造器中定义了一个数组,用于管理某个属性所有的观察者对象。
好了,现在我们已经定义了观察者类和发布订阅类。
接下来要完成的就是将发布订阅模式运用到 Nue 中,实现数据驱动界面改变。
在监听数据变化的章节当中,根据指定的区域和数据去编译渲染界面
这个步骤处,我写了一个注释,这个注释是这样的:第一步:给外界传入的所有数据都添加get/set方法
,第二步就是在第一步的基础上,给所有属性都添加观察者对象,当数据发生变化时,发布订阅触发观察者对象的回调函数重新渲染界面。
先处理下 v-model 的情况,找到 CompilerUtil 中的 model 方法,将其修改添加观察者对象代码:
代码语言:javascript复制model: function (node, value, vm) {
// 第二部:在第一次渲染的时候, 就给所有的属性添加观察者
new Watcher(vm, value, (newValue, oldValue) => {
node.value = newValue;
});
node.value = this.getValue(vm, value);
},
这样就完成了第二步,接下来第三步就是将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来
在创建观察者对象的时候,在构造函数当中,会调用 getOldValue 方法,会调用 CompilerUtil.getValue
方法,这个方法就是用于获取属性值的,在编译模板之前已经给所有属性添加了 get/set 方法,所以在获取属性值的时候,就会触发 get 方法,我们就可以在 get 方法中将当前属性的观察者对象添加到当前属性的发布订阅对象中管理起来。
在 Observer 类中的 defineRecative 方法中添加如下代码:
代码语言:javascript复制defineReactive(obj, attr, value) {
this.observer(value);
// 第三步:将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来
// 创建属于当前属性的发布订阅对象
let dep = new Dep();
Object.defineProperty(obj, attr, {
get() {
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newValue) => {
if (value !== newValue) {
this.observer(newValue);
value = newValue;
dep.notify();
console.log('监听到数据的变化, 需要去更新UI');
}
}
})
}
在上述代码中,创建了一个属于当前属性的发布订阅对象,然后在 get 方法中,判断 Dep.target 是否存在,如果存在,就将当前属性的观察者对象添加到当前属性的发布订阅对象中管理起来。Dep.target 就是当前属性的观察者对象,这里该需要在改造一下观察者的类,将观察者对象添加到 Dep.target 中,放在全局中管理起来。等到所有的属性都添加完观察者对象之后,就将 Dep.target 置为 null。
改造观察者类中的 getOldValue 方法, 这样在 get 方法中就可以将当前属性的观察者对象添加到当前属性的发布订阅对象中管理起来了:
代码语言:javascript复制getOldValue() {
Dep.target = this;
let oldValue = CompilerUtil.getValue(this.vm, this.attr);
Dep.target = null;
return oldValue;
}
这样就完成了数据驱动界面改变的功能,接下来我们就来测试一下,打开浏览器控制台,更改下数据,看看是否会触发界面的重新渲染,如下图所示:
好了到此为止,我们已经完成了 v-model 数据驱动界面改变的功能。
下面我将以 debugger 的形式来讲解一下整个数据驱动界面改变的过程, 在 defineReactive get 方法中打上断点,如下图所示:
返回浏览器,主要关注调用栈,如下图所示:
自己从下依次往上看,就可以看到整个数据驱动界面改变的过程了,这里我就不一一截图了,大家可以自己去看一下。
如上是 get 方法代码的执行流程,那么 set 的我也可以说明一下,set 方法的 debugger 不是打在 defineReactive 中,而是打在 Watcher 类中的 update 方法中,所执行的回调函数当中,如下图所示:
返回浏览器,打开控制台更改数据触发 set 方法,发布订阅触发 update 方法:
这次也是主要关注调用栈,自己从下依次往上看,就可以看到整个数据驱动界面改变的过程了,这里我就不一一截图了,大家可以自己去看一下,如下图所示:
数据驱动界面改变 v-model 的双向绑定已告一段落, 接下来再将剩余的内容在完成一下。
首先完成我们的 v-html,v-text, 其实很简单,就是将我们之前的 v-model 创建观察者的方法,在 v-html 和 v-text 中再写一次即可,创建属于 v-html 和 v-text 的观察者。
v-html:
代码语言:javascript复制html: function (node, value, vm) {
new Watcher(vm, value, (newValue, oldValue) => {
node.innerHTML = newValue;
});
node.innerHTML = this.getValue(vm, value);
},
v-text:
代码语言:javascript复制text: function (node, value, vm) {
new Watcher(vm, value, (newValue, oldValue) => {
node.innerText = newValue;
});
node.innerText = this.getValue(vm, value);
},
测试一下 v-html,打开浏览器控制台,输入 vue.$data.html = '<p>我是测试v-html<p/>'
,可以看到界面上的内容已经改变了。
测试一下 v-text,打开浏览器控制台,输入 vue.$data.text = '<p>我是测试v-text<p/>'
,可以看到界面上的内容已经改变了。
好了到此为止,指令的数据驱动界面改变已经完成了,接下来我们来完成模板语法的数据驱动界面改变。
这个就与之前的指令的数据驱动界面改变不同了,好了先不说问题,我们先直接来看代码一步一步分析。
我看了下之前处理 content 的代码发现,获取不到对应的属性名称叫什么,因为是直接调用 this.getContent(vm, value); 获取的,所以会出现一个问题就是给 content 创建观察者对象的时候不能直接告诉他我要监听的是哪个属性,所以我就想到了一个办法。
首先将之前的代码注释掉,再然后我编写一个正则表达式,关于这个正则表达式在之前的文章中有讲到,大概意思就是匹配 {{}}
中的内容,这里就不再赘述了。
let reg = /{{(. ?)}}/gi;
继续往下看,我利用 value 调用了 replace 方法,传递了两个参数,第一个参数是刚刚编写的正则表达式,第二个参数是一个函数,这个函数的作用就是将匹配到的内容替换成对应的值,我先将其返回值打印出来,看看是什么,我们的代码就可以写成这样。
代码语言:javascript复制content: function (node, value, vm) {
// console.log(value); // {{ name }} -> name -> $data[name]
// node.textContent = this.getContent(vm, value);
let reg = /{{(. ?)}}/gi;
value.replace(reg, (...args) => {
console.log(args[1].trim());
});
}
是的,我们的确获取到了对应的属性名称,接下来我们就可以利用这个属性名称来创建观察者对象了,我们的代码就可以写成这样。
代码语言:javascript复制content: function (node, value, vm) {
// console.log(value); // {{ name }} -> name -> $data[name]
// node.textContent = this.getContent(vm, value);
let reg = /{{(. ?)}}/gi;
node.textContent = value.replace(reg, (...args) => {
const attr = args[1].trim();
new Watcher(vm, attr, (newValue, oldValue) => {
node.textContent = this.getContent(vm, value);
});
return this.getValue(vm, args[1]);
});
}
好了,我们来测试一下,打开浏览器控制台,输入 vue.$data.name = '我是测试 {{ name }}'
,可以看到界面上的内容已经改变了。
一切看起来都很完美,最终版代码其实是我没有将坑点说出来,现在我们来看看这个坑点是什么,再看之前,我来讲述一下为什么是又调用了 this.getContent 方法而不是直接将 newValue 赋值给 node.textContent。
假如我们的数据结构是这样的 {{ name }} - {{ age }}
如果是通过直接将 newValue 赋值给 node.textContent 的话,这个时候呢,我假设 name 的值是 BNTang, age 的值是 33,那么界面上第一次加载的内容会是 BNTang - 33,但是如果我将 name 的值改成了 xhh,那么界面上的内容就会变成 xhh,这个时候 age 的值就丢掉了,如下图是我的测试结果。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!