详解vue原理之观察模式Dep->Watcher
根据前几节课,相信大家都明白的vue的基本原理 能够实现vue响应及渲染 这如果还不清楚的 请看上几篇文章
这节课 我们讲解vue中数据的响应实现 即vue中的观察模式 如果还不明白观察模式的 也请看我的文章详解js中观察模式和订阅发布模式的区别
Dep(Dependency)
功能
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
结构
下面是代码的基本实现
// 要实现数据的响应机制 即数据变化 视图变化
// 在vue的响应机制中 我们要使用观察模式来监听数据的变化
// 因此 在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知 调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知
class Dep {
// 存储所有观察者
constructor() {
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
在 compiler类 中收集依赖,发送通知
// defineReactive 中 // 创建 dep 对象收集依赖
const dep = new Dep()
// getter 中 // get 的过程中收集依赖
Dep.target && dep.addSub(Dep.target)
// setter 中 // 当数据变化之后,发送通知
dep.notify()
Watcher
功能
- 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
- 自身实例化的时候往 dep 对象中添加自己
结构
```cpp
//具体代码实现如下
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数 负责更新视图
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target中
Dep.target = this;
// 触发get方法 在get方法中调用addSub
this.oldValue = vm[key]
Dep.target = null;
}
// 当数据发生变化的时候 更新视图
update() {
let newValue = this.vm[this.key];
if (newValue === this.oldValue) {
return
}
this.cb(newValue)
}
}
在 compiler类 中为每一个指令/插值表达式创建 watcher 对象,监视数据的变化
// 因为在 textUpdater等中要使用
this updaterFn && updaterFn.call(this, node, this.vm[key], key)
// v-text 指令的更新方法
textUpdater (node, value, key) { node.textContent = value }
// 每一个指令中创建一个 watcher,观察数据的变化
new Watcher(this.vm, key, value => { node.textContent = value }) }
附上完整的代码
// 具体实现步骤
// 1: 通过属性 保存选项的数据
// 2: 把data中的成员 转换为getter和setter 注入到vue实例中 方便使用
// 3:调用observer对象 监听数据变化
// 4:调用compiler 解析指令和插值表达式
class Vue {
constructor(options) {
// 通过属性 保存选项的数据
this.$options = options || { };//如果我们在调用vue构造函数的时候 没有传入参数 我们初始化一个空对象
this.$data = options.data || { };
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;//如果我们是传入的选择器 则将选择器转换为dom对象
// 把data中的成员 转换为getter和setter 注入到vue实例中 方便使用
this._proxyData(this.$data)
// 调用observer对象 监听数据变化
new Observer(this.$data)
// 调用compiler 解析指令和插值表达式
new Compiler(this)
}
_proxyData(data) { //vue传过来的参数 转换为getter和setter
// 遍历data的所有属性
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
// 可遍历 可枚举
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return
} else {
data[key] = newValue;
}
}
})
})
// 把data中的属性 注入到vue实例中
}
}
class Observer {
constructor(data) {
this.walk(data)
}
// 1. 判断数据是否是对象,如果不是对象返回
// 2. 如果是对象,遍历对象的所有属性,设置为 getter/setter
walk(data) {
if (!data || typeof data != 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 定义响应式成员 即对data总的数据实现setter和getter
defineReactive(data, key, val) {
//负责收集依赖 并发布通知
let dep = new Dep()
const that = this
// 如果 val 是对象,继续设置它下面的成员为响应式数据
this.walk(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return val;
},
set(newValue) {
if (val === newValue) {
return
}
// 如果 newValue 是对象,设置 newValue 的成员为响应式
that.walk(newValue)//这里不用this 因为在set方法中 在function的内部 会开启新的作用域 此时的this执行data对象
val = newValue;
// 发布通知
dep.notify()
}
})
}
}
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compile(this.el);
}
// 编译模板 处理文本节点和元素节点
compile(el) {
const nodes = el.childNodes;
Array.from(nodes).forEach(node => {
if (this.isElementNode(node)) {
this.compileElement(node);
} else if (this.isTextNode(node)) {
this.compileText(node)
}
// 如果当前节点中还有子节点,递归编译
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点 处理指令
compileElement(node) {
// 暂时只涉及v-text和v-model
// 逻辑思路 首先我们获取node节点所有的属性 查找到我们需要的指令 并用值替换
// 下面我们打印一下node的所有属性 具体可切换google浏览器查看
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => { //Array.from 将伪数组转换为数组
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 在指令中 有v-text v-model等各种各样的指令 我们不能用if语句判断 如果用if 在后期 如果增加了别的指令 则不便于维护 需要手动增加if判断
// 因此 我们讲指令的v-去掉 只保留后面部分 即:v-text -> text v-model ->model
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
// 判断是否是指令
}
// 通过update方法拼接处对应指令对应的方法 方便后序或者 如:v-text指令 则为textUpdater方法 v-model指令 则为modelUpdater方法 Updater字符串固定 因此 在后续 如果追加的不同的指令 执行增加指令名+Updater凭借的方法
// 通过update方法拼接处对应指令对应的方法 方便后序或者 如:v-text指令 则为textUpdater方法 v-model指令 则为modelUpdater方法 Updater字符串固定 因此 在后续 如果追加的不同的指令 执行增加指令名+Updater凭借的方法
update(node, key, attrName) {
let updateFun = this[attrName + 'Updater']
// updateFun && updateFun(node, this.vm[key])
//将这里使用call改变this 引入这里的updateFun是直接调用 this对象不是compiler 为了在modelUpdater和textUpdater方法中的this指向是compiler而不是window 所以使用call将updateFun的this指向改为compiler
// 增加key参数 是为了在textUpdater和modelUpdater方法中能有key使用
updateFun && updateFun.call(this,node, this.vm[key], key)
}
textUpdater(node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
})
}
modelUpdater(node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
})
}
// 编译文本节点 处理插值表达式
compileText(node) {
// console.dir(node)
// { { msg }} 解析: 插值表达式 是双括号中奖有一个变量 变量的前后可能有空格 可能无空格 可能有多个空格 我们需要将变量提出来 因此用正则表达式
// 详解{ { msg }}案例正则使用规则
// 1:正则的使用规范 首先用//表示正则的开始和介绍
// 2:插值表达式中有{ {}},即前后两个大括号 因此这里的正则是 /{ {}}/
// 3: 犹豫{是特殊符号 因此需要转义符\ 故正则是 /\{\{\}\}/
// 4: 匹配花括号 即{ {}}中间的名字 而变量的名字前面都可能有空格 故:我们需要匹配任意的字符 使用点表示 即 '.' 点表示匹配任意的单个字符 ,不包括换行 后面跟上'+', 加号表示以前修饰的内容出现一次或者多次 因此 .+就可以匹配变量的名字 故 正则为: /\{\{.+\}\}/
// 5: 我们在.+后面跟上问好'?' 表示今早的来结束匹配 故 正则为:/\{\{.+?\}\}/
// 6: 通过/\{\{.+?\}\}/ 我们可以匹配到变量的名字 现在我们需要将匹配的变量名字提取出来 也就是把".+?"的这个位置的内容提取出来 在正则表达式中 这个非常简单 我们只需要需要提取内容的位置加上小括号(),小括号在正则中有分组的含义 我们可以获取到分组中的结果 最终的正则为/\{\{(.+?)\}\}/
let reg = /\{ \{ (.+?)\}\}/
let value = node.textContent;//获取文本的内容
if (reg.test(value)) {
let key = RegExp.$1.trim();//这里使用正则对象的构造函数 来获取第一个分组的内容 这里$1表示第一个分组的内容 如果要获取第二个 则用$2 获取之后 可能有空格 因此使trim去掉空格
node.textContent = value.replace(reg, this.vm[key]);//replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
})
}
}
// 判断元素是否是指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判断元素是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 判断元素是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
}
// 要实现数据的响应机制 即数据变化 视图变化
// 在vue的响应机制中 我们要使用观察模式来监听数据的变化
// 因此 在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知 调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知
class Dep {
// 存储所有观察者
constructor() {
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
//数据变化 watcher去更新视图
// 当我们去创建一个watcher对象时 需要把自己添加到自己的主题对象中去
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数 负责更新视图
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target中
Dep.target = this;
// 触发get方法 在get方法中调用addSub
this.oldValue = vm[key]
Dep.target = null;
}
// 当数据发生变化的时候 更新视图
update() {
let newValue = this.vm[this.key];
if (newValue === this.oldValue) {
return
}
this.cb(newValue)
}
}
let vm = new Vue({
el: '#app',
data: {
msg: 'hello',
count: 123,
person: {
name: 'zs'
}
}
})
// vm.msg = { 'sex': 'ada' }
上一篇:详解vue原理中的编译compiler
下一篇:
还没有评论,来说两句吧...