手写 — 简单的vue数据响应(二)
最后更新于:2022-04-02 08:12:03
>[success] # 手写一个简单vue数据响应
~~~
1.实现一个简单的vue 响应的模型,刚才已经分析过了整体思路,现在实现一个简单的
vue 版本,分析一下Vue 在使用时候一些规则。
1.1.首先接受的是一个对象,其中data可以是对象也可以是function
1.2.data 属性可以直接通过vue实例调用
2.对下面几个类做个说明:
2.1.Vue 类一个配置入口,其中'_proxyData'方法将data中的数据挂载到vue实例上
2.2.Observe 类负责对data 中每个属性都进行getter 和setter,并且对数据变化进行监听调用
2.3.解析指令和差值表达式
~~~
* vue 官网的图解
~~~
1.发布者Dep 在数据getter时候,将需要的观察者 收集到发布者中,在setter进行发送通知
1.watcher 作为一个观察者,等待setter 时候触发整体调用机制
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/81/e7/81e765156986df52c7374621fefdddf6_725x465.png)
>[danger] ##### 最后整体效果
* 使用效果
~~~
~~~
~~~
function getData(data, vm) {
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
}
}
class Vue {
constructor(options) {
// 1.将options 进行保存
this.$opitions = options || {}
const data = options.data
this.$data = typeof data === 'function' ? getData(data, vm) : data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2.将data 中数据转换成getter setter 存在vue 中,
// 这次只是将 data 中的数据 以getter setter 形式放到Vue 对象中
this._proxyData(this.$data)
// 3.调用observe对象,observe 对象负责对data 中每个属性都进行getter 和setter
// 并且对数据变化进行监听调用
new Observe(this.$data)
// 4. 调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData(data) {
// 遍历data中的所有key为了将数据形成getter 和setter 的形式
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
// 如果新老值没有变化就不用进行set 重新赋值
if (newValue !== data[key]) {
data[key] = newValue
}
}
})
})
}
}
class Observe {
constructor(data) {
this.walk(data)
}
// 循环遍历data对象的所有属性,作为递归的一个判断条件
walk(data) {
if (typeof data !== 'object' || !data) return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, val) {
let that = this
// 负责收集依赖,并发送通知
let dep = new Dep()
console.log(dep, 111)
this.walk(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 将需要的观察者 收集到发布者中
Dep.target && dep.addSub(Dep.target)
return val
},
set(newValue) {
if (newValue === val) return
val = newValue
// 如果赋值的 是对象需要从新进行getter 和setter 绑定
that.walk(newValue)
// 发送通知
dep.notify()
}
})
}
}
// 处理判el 对象绑定是
function query(el) {
if (typeof el === 'string') {
var selected = document.querySelector(el);
if (!selected) {
warn(
'Cannot find element: ' + el
);
return document.createElement('div')
}
return selected
} else {
return el
}
}
class Compiler {
constructor(vm) {
this.el = query(vm.$el)
this.vm = vm
// 调用处理模板
this.compile(this.el)
}
// 编译模板
// 处理文本节点和元素节点
compile(el) {
// 获取节点中的内容
const childNodes = el.childNodes
// 需要对不同节点做不同的处理
// 1.1.文本节点一般是通过{{msg}},双大括号这种进行值替换
// 1.2.dom 节点一般会自定义一些指令 v-if 或者 v-text 一类
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
this.compileElement(node)
}
// 如果当前节点还有子节点 这时候就需要递归继续compile操作
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 判断是不是 文本节点,这是dom 自己的属性3 表示文本
isTextNode(node) {
return node.nodeType === 3
}
// 判断是不是属性节点,这是dom 自己的属性1 表示dom 元素
isElementNode(node) {
return node.nodeType === 1
}
// 判断是不是指令 也就是v- 开头
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 编译文本节点
compileText(node) {
// 1.文本节点一般是通过{{msg}},双大括号这种进行值替换
// 现在的思路就是利用正则取出 双大括号中的key 并且在vm找到key 对应的value渲染上
const reg = /\{\{(.+)\}\}/
// 获取文本节点中的内容例如
';
{{name}}
{{name}}
我是{{ name }}
// 获取的内容就为 -- 我是{{ name }} const value = node.textContent if (reg.test(value)) { // 获取双大括号中的key const key = RegExp.$1.trim() // 重新赋值 文本节点中的内容 // 之前已经将data 中的数据通过_proxyData方法放到vue 实例上了 node.textContent = value.replace(reg, this.vm[key]) // 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 编译属性节点 compileElement(node) { // 遍历dom 节点所有元素中的属性,找到指令 Array.from(node.attributes).forEach(attr => { // 获取元素中的属性 let attrName = attr.name if (this.isDirective(attrName)) { // attrName 的形式 v-text v-model // 截取属性的名称,获取 text model attrName = attrName.substr(2) // 获取属性的名称,属性的名称就是我们数据对象的属性 v-text="name",获取的是name const key = attr.value // 处理不同的指令 this.update(node, key, attrName) } }) } // 负责更新 DOM // 创建 Watcher update(node, key, dir) { // node 节点,key 数据的属性名称,dir 指令的后半部分 const updateFn = this[dir + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) } // v-text 指令的更新方法 textUpdater(node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model 指令的更新方法 modelUpdater(node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } } // -------利用发布订阅 模式来进行 数据响应通知---------- // 发布者 -- 发布者要收集所有观察者才能 给每个观察者发送要接受的内容 class Dep { constructor() { this.subs = [] } addSub(sub) { if (sub && sub.update) { this.subs.push(sub) } } notify() { this.subs.forEach(sub => sub.update()) } } // 观察者 等待发布者发送指令 class Watcher { constructor(vm, key, cb) { this.vm = vm // data 中的key 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 (this.oldValue === newValue) { return } this.cb(newValue) } } ~~~ >[danger] ##### 参考文章 [mvvm](https://github.com/DMQ/mvvm)