响应式 基于Object.defineProperty
实现数据劫持
<html>
<header>
<title>数据劫持</title>
</header>
<body>
<div id="app">a</div>
<script src="./mvvm.js"></script>
<script>
let mvvm = new Mvvm({
el: '#app',
data: {
a: 1,
b: {
c: 2
}
}
})
</script>
</body>
</html>
class Mvvm {
constructor(options) {
this.$options = options
let data = this._data = this.$options.data
observe(data)
// 通过对this进行监听绑定对应的数据,完成数据代理,可以通过mvvm.b.c读取
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get () {
return this._data[key]
},
set (newVal) {
this._data[key] = newVal
}
})
}
}
// 编译替换
new Compile(this.$options.el, this)
}
function observe (data) {
if (!data || typeof data !== 'object') return
return new Observe(data)
}
function Observe (data) {
for(let key in data) {
let val = data[key]
observe(val)
Object.defineProperty(data, key, {
configurable: true,
get () {
return val
},
set (newVal) {
val = newVal
observe(newVal)
}
})
}
}
可以看到对应的data上的数据都已经监听到了
另外改写对应的属性数据,也可以看到对应的会递归完成绑定,将原有的b对应的对象,set为新对象,会递归的将新对象完成数据监听。
proxy实现监听
function reactive(obj) {
if (typeof obj !== 'object' && obj != null){
return obj
} // Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return isObject(res) ? reactive(res) : res }, return observed
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`) return res
}
}
return observed
数据编译
MVVM.js 构造器增加
new Compile(this.$options.el, this)
class Compile {
constructor (el, vm) {
// 将el挂载到dom下
this.$vm = vm
vm.$el = document.querySelector(el)
// 利用文档碎片存储
let fragment = document.createDocumentFragment()
let child = null
// 利用appendChild存在的元素会被移除的特性进行循环
while (child = vm.$el.firstChild) {
fragment.appendChild(child) // 此时将el中的内容放入内存中
}
this.replace(fragment)
// 利用文档碎片放回到el中
vm.$el.appendChild(fragment)
}
replace (fragment) {
Array.from(fragment.childNodes).forEach(node => {
let txt = node.textContent
let reg = /\{\{(.*?)\}\}/g // 正则匹配{{}}
if (node.nodeType === 3 && reg.test(txt)) {
console.log(RegExp.$1)
let arr = RegExp.$1.split('.')
let val = this.$vm
arr.forEach(key => {
val = val[key]
})
node.textContent = txt.replace(reg, val).trim()
}
// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
this.replace(node);
}
})
}
}
数据更新视图
通过发布订阅来进行数据的监听并更新视图
1、改造replace替换时,增加创建监听
class Mvvm {
constructor(options) {
this.$options = options
let data = this._data = this.$options.data
observe(data)
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get () {
return this._data[key]
},
set (newVal) {
this._data[key] = newVal
}
})
}
new Compile(this.$options.el, this)
}
}
function observe (data) {
if (!data || typeof data !== 'object') return
return new Observe(data)
}
function Observe (data) {
let dep = new Dep()
for(let key in data) {
let val = data[key]
observe(val)
Object.defineProperty(data, key, {
configurable: true,
get () {
// 将watcher添加到订阅事件中 [watcher]
+ Dep.target && dep.addSub(Dep.target) // 新增
return val
},
set (newVal) {
if (val === newVal) {
return
}
val = newVal
observe(newVal)
+ dep.notify(); // 让所有watcher的update方法执行即可
}
})
}
}
class Compile {
constructor (el, vm) {
// 将el挂载到dom下
this.$vm = vm
vm.$el = document.querySelector(el)
// 利用文档碎片存储
let fragment = document.createDocumentFragment()
let child = null
// 利用appendChild存在的元素会被移除的特性进行循环
while (child = vm.$el.firstChild) {
fragment.appendChild(child) // 此时将el中的内容放入内存中
}
this.replace(fragment)
// 利用文档碎片放回到el中
vm.$el.appendChild(fragment)
}
replace (fragment) {
Array.from(fragment.childNodes).forEach(node => {
let txt = node.textContent
let reg = /\{\{(.*?)\}\}/g // 正则匹配{{}}
if (node.nodeType === 3 && reg.test(txt)) {
console.log(RegExp.$1)
// let arr = RegExp.$1.split('.')
// let val = this.$vm
// arr.forEach(key => {
// val = val[key]
// })
// 监听变化
// 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
+ new Watcher(this.$vm, RegExp.$1, newVal => {
+ node.textContent = txt.replace(reg, newVal).trim()
+ })
+ }
// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
this.replace(node);
}
})
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
class Watcher {
constructor(vm, exp, fn) {
this.fn = fn
this.vm = vm
this.exp = exp
Dep.target = this
let arr = exp.split('.')
let val = vm
arr.forEach(key => {
val = val[key] // 通过获取this.a.b的方式,进行监听的绑定
})
Dep.target = null
}
update() {
// notify的时候值已经更改了
// 再通过vm, exp来获取新的值
let arr = this.exp.split('.')
let val = this.vm
arr.forEach(key => {
val = val[key] // 通过get获取到新的值
})
this.fn(val) // 将每次拿到的新值去替换{{}}的内容即可
}
}
双向数据绑定
通过监听dom上对应元素的输入或者change变化,来调用数据的set,来通知到update完成数据的响应视图
// html结构
<input v-model="c" type="text">
// 数据部分
data: {
a: {
b: 1
},
c: 2
}
function replace(frag) {
// 省略...
+ if (node.nodeType === 1) { // 元素节点
let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
Array.from(nodeAttr).forEach(attr => {
let name = attr.name; // v-model type
let exp = attr.value; // c text
if (name.includes('v-')){
node.value = vm[exp]; // this.c 为 2
}
// 监听变化
new Watcher(vm, exp, function(newVal) {
node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
});
node.addEventListener('input', e => {
let newVal = e.target.value;
// 相当于给this.c赋了一个新值
// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
vm[exp] = newVal;
});
});
+ }
if (node.childNodes && node.childNodes.length) {
replace(node);
}
}
参考文章:
juejin.cn/post/684490…
参考视频地址:
www.bilibili.com/video/BV1u4…