defineProperty 2.0 (3.0 proxy)
vue 是如何做的?
html:
<div id="app">
{{message}}
</div>
js:
let vm = new Vue({
el: '#app',
data: {
message: '测试数据'
}
})
console.log(vm._data)
setTimeout(() => {
vm._data.message = 'update'
}, 2000);

2s后:

几个问题:
数据是怎么渲染到视图的?数据变了视图怎么变化?插值表达式要怎么实现?
Object.defineProperty
- 拿返回值:创建
- 修改
// obj
let obj = {
name:"张安"
}
console.log(obj);

// 用defineProperty劫持过的obj
// 获取obj.name 触发get
let obj = {
name:"张安"
}
Object.defineProperty(obj,"name",{
configurable: true, // 是否可删除 默认false
enumerable: true, // 可枚举的属性 默认false
get(){
console.log("get...");
return "张安"
},
set(newValue){
console.log("set...",newValue)
}
})
console.log(obj);
console.log(obj.name);

// 设置属性,触发set
let obj = {
name:"张安"
}
Object.defineProperty(obj,"name",{
configurable:true, // 是否可删除 默认false
enumerable:true, // 可枚举的属性 默认false
get(){
console.log("get...");
return "张安"
},
set(newValue){
console.log("set...",newValue)
}
})
obj.name = "王五"

用自定义事件实现上述问题
“数据是怎么渲染到视图的?数据变了视图怎么变化?插值表达式要怎么实现?”
<!--
1. 数据渲染到视图
- 在根结点(#app)作用域内查找所有节点,寻找符合条件的{{message}}
- 正则匹配出满足条件的{{ }}
- 拿到message 在data中找到message的值,替换#app 中的{{message}}
2. 数据更新到视图
- Object.defineProperty 劫持 data
- EventTarget、dispatchEvent、addEventListener
- 新值替换旧值
-->
<div id="app">
<div>
</div>
<div>
<!--递归遍历所有的message-->
{{message}}
</div>
<!-- 表达式 -->
{{message}}
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
message: 123,
mess: 456
}
})
// 加了定时器,数据变化,才能劫持到
setTimeout(()=>{
vm.$data.message = "修改的数据"
},1000)
</script>
class Vue extends EventTarget{
constructor (options){
super()
this.$options = options
this.$el = this.$options.el
this.$data = this.$options.data
this.observer(this.$data)
this.complie()
}
observer(data){
for(let key in data) {
let val = data[key];
let that = this
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// console.log('get')
// 在外层定义一个val = data[key]而不是在这里直接 return data[key] 是因为:
// 获取数据会触发get 从而造成死循环
return val
},
set(newVal) {
// console.log('set')
let event = new CustomEvent(key, {
detail: newVal
})
// 经测试,this指向会发生改变指向obj
that.dispatchEvent(event)
val = newVal
}
})
}
}
complie() {
let ele = document.querySelector(this.$options.el)
this.complieNodes(ele)
}
complieNodes(ele) {
let childs = ele.childNodes
let childList = [...childs].filter(v => {
return v.nodeType == 3 || v.nodeType == 1
});
childList.forEach((v) => {
if(v.nodeType == 3) {
// 文本节点
let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
// console.log(reg.test(v.textContent))
// 这样写会有问题,RegExp对象有一个lastIndex的属性。如果使用了全局修饰符,那么执行test方法后,lastIndex就会记录匹配的字符串在原始字 符串中最后一位的索引加一。
// 如果没有发现匹配,lastIndex置为0。当下次再执行时,对给定的字符串匹配不是从开头位置,而是要依据lastIndex提供的位置
// 会造成结果反复的问题
if(reg.test(v.textContent)){
let $1 = RegExp.$1
v.textContent = v.textContent.replace(v.textContent, this.$data[$1]);
this.addEventListener($1, e=>{
let oldValue = this.$data[$1]
let newValue = e.detail
let reg = new RegExp(oldValue)
v.textContent = v.textContent.replace(reg, newValue)
})
}
} else if(v.nodeType == 1) {
// 递归
if(v.childNodes.length > 0) {
this.complieNodes(v)
}
}
})
}
}
修改前:


// 修改成订阅发布后
<div id="app">
<div>
{{mess}}
</div>
<!-- <div v-text="message" class="box"> -->
<!-- </div> -->
<!-- 表达式 -->
{{message}}
<hr>
<!-- <input type="text" v-model="test"> -->
{{test}}
<hr>
{{message}}
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
message: '历史的值',
mess: 'mess',
test: '123'
}
})
// console.log(vm)
setTimeout(()=>{
vm.$data.message = "修改的数据"
},1000)
</script>
class Vue{
constructor (options){
// super()
this.$options = options
this.$el = this.$options.el
this.$data = this.$options.data
this.observer(this.$data)
this.complie()
}
observer(data){
for(let key in data) {
let val = data[key];
// let that = this
let dep = new Dep()
console.log(dep)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
if(Dep.target) {
dep.addSub(Dep.target)
// console.log(dep)
}
// console.log(dep)
return val
},
set(newValue) {
dep.notify(newValue)
// console.log('set')
// let event = new CustomEvent(key, {
// detail: newVal
// })
// that.dispatchEvent(event)
val = newValue
// console.log(event)
}
})
}
}
complie() {
let ele = document.querySelector(this.$options.el)
this.complieNodes(ele)
}
complieNodes(ele) {
let childs = ele.childNodes
let childList = [...childs].filter(v => {
return v.nodeType == 3 || v.nodeType == 1
});
childList.forEach(v => {
if(v.nodeType == 3) {
let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/;
// 这样写会有问题,RegExp对象有一个lastIndex的属性。如果使用了全局修饰符,那么执行test方法后,lastIndex就会记录匹配的字符串在原始字 符串中最后一位的索引加一。
// 如果没有发现匹配,lastIndex置为0。当下次再执行时,对给定的字符串匹配不是从开头位置,而是要依据lastIndex提供的位置
// 会造成结果反复的问题
// console.log(reg.test(v.textContent))
if(reg.test(v.textContent)){
let $1 = RegExp.$1
v.textContent = v.textContent.replace(v.textContent, this.$data[$1]);
// this.addEventListener($1, e=>{
// let oldValue = this.$data[$1]
// let newValue = e.detail
// let reg = new RegExp(oldValue)
// v.textContent = v.textContent.replace(reg, newValue)
// })
// 实例化watcher, 重新编译模版
new Watcher(this.$data, $1, (newValue) => {
let oldValue = this.$data[$1]
let reg = new RegExp(oldValue)
v.textContent = v.textContent.replace(reg, newValue)
})
}
} else if(v.nodeType == 1) {
let attrs = v.attributes;
[...attrs].forEach(attr => {
let attrName = attr.name
let attrValue = attr.value
if(attrName == 'v-text') {
v.innerText = this.$data[attrValue]
} else if(attrName == 'v-model') {
v.value = this.$data[attrValue];
v.addEventListener('input', e => {
this.$data[attrValue] = e.target.value
})
}
});
// 递归
if(v.childNodes.length > 0) {
this.complieNodes(v)
}
}
})
}
}
// 依赖收集器
class Dep {
constructor() {
this.subs = []
}
// 收集订阅者
addSub(sub) {
this.subs.push(sub)
}
notify(newValue) {
this.subs.forEach(sub => {
sub.update(newValue)
})
}
}
// 订阅者
class Watcher {
constructor(data, key, cb) {
Dep.target = this
data[key]
this.cb = cb
Dep.target = null
}
update(newValue) {
// console.log('update...', newValue)
this.cb(newValue)
}
}