vue2
使用Object.defineProperty进行数据劫持,再结合观察者模式实现双向绑定。
主要是observer、watcher、dependency。
入口是observer,dependency负责收集wather和通知watcher进行视图更新,主要方法就是add和notify。watcher是负责订阅属性,执行更新视图的回调函数。
入口是observer主要是使用defineProperty给对象属性添加get和set方法。并在
get中执行dep的add,在set中执行dep的update。
- 为每个属性添加getter和setter,一开始temp肯定不存在,因为watch的构造函数还没有执行,不会添加到访问者数组subscribers。
- 添加了以后会执行compile创建一个缓冲区,把document全部剪切到一个空白页面fragment,然后执行fragment_compile修改缓冲区页面。
- 在fragment中,先识别模板字符串,把值替换上去,再订阅,这样值改变就能实现响应式。
- new订阅的时候得告诉发布者把自己加到订阅者数组啊,不然你后面发通知我不在群里接收不到啊,所以订阅者的constructor中temp暂存一下我这个watcher(this),然后故意触发一下get,get这时候发现temp不为空了,有信号了,那就添加到订阅者数组。
- 然后让数据改变的时候发布者在群里发通知就行了,这样订阅者知道数据改变了也就相应的去执行更新的回调就行了。
- 同理,数据修改时触发set也要让发布者通知订阅者执行更新回调。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='app'>
<span>姓名:{{name}}</span>
<input type="text" v-model='name'/>
<span>爱好:{{more.hobby}}</span>
<input type="text" v-model='more.hobby' />
</div>
</body>
<script>
class Vue{
constructor(obj_instance){
this.$data=obj_instance.data
//1.数据劫持
Observer(this.$data)
Compile(obj_instance.el,this)
}
}
function Observer(data_instance){
//递归出口
if(!data_instance || typeof data_instance !=='object')
return
const dependency=new Dependency()
Object.keys(data_instance).forEach((key)=>{
//使用Object.defineProperty后属性就被修改,如果直接在get里调用值为undefined,所以先保存value
let value=data_instance[key]
//递归的对属性嵌套的对象添加监听
Observer(value)
Object.defineProperty(data_instance,key,{
enumerable:true,
configurable:true,
get(){
console.log(`访问了属性:${key} ---> 值:${value}`)
//8.发布者收集订阅者
Dependency.temp &&dependency.addSub(Dependency.temp)
return value
},
set(newValue){
console.log(`修改了属性:${key} ---> 值:${newValue}`)
value=newValue
//如果对象属性被修改为新的对象,也要给新的对象属性们进行监听
Observer(newValue)
//9.属性修改赶紧告诉发布者,让发布者通知订阅者执行update操作
dependency.notify()
}
})
})
}
/*
*3.创建缓冲区将所有数据更新后再渲染页面
*/
function Compile(element,vm){
vm.$el=document.querySelector(element)
// console.log(vm.$el.childNodes)
//创建文档碎片
const fragment=document.createDocumentFragment()
let child
while((child=vm.$el.firstChild)){
// 将所有子节点摘下来放入文档碎片中,页面会变成空白因为createDocumentFragment是剪切过去的
fragment.appendChild(child)
}
// console.log(fragment)
// 4.替换文档碎片的值
fragment_compile(fragment)
/**
* 替换文档碎片内容
*/
function fragment_compile(node){
// console.log(node)
const pattern=/\{\{\s*(\S+)\s*\}\}/
// 如果结点是文本,修改其中插值表达式的内容
if(node.nodeType === 3){
// console.log(node)
//保存模板字符串中的属性名称
const xxx=node.nodeValue
const result_regex=pattern.exec(node.nodeValue)
// console.log(node.nodeValue)
// console.log(result_regex)
if(result_regex){
//more.like搞成['more','like']
const arr=result_regex[1].split('.')
// console.log(arr)
//叠加为vm.$data[more][like]
const value=arr.reduce((total,cur)=>total[cur],vm.$data)
//实现插值表达式显示data内容,value还是初始没变的旧值
node.nodeValue=xxx.replace(pattern,value)
//7.创建订阅者,如果改变了值以后如何更新
new Watcher(vm,result_regex[1],newValue=>{
node.nodeValue=xxx.replace(pattern,newValue)
})
}
return
}
// 10.处理input框的双向绑定
if(node.nodeType===1 && node.nodeName ==='INPUT'){
// console.log(node)
const attr=Array.from(node.attributes);
console.log(attr)
attr.forEach(i=>{
if(i.nodeName === 'v-model'){
const value=i.nodeValue.split('.').reduce((total,cur)=>total[cur],vm.$data)
// Input赋值
node.value=value
//创建订阅者
new Watcher(vm,i.nodeValue,newValue=>{
node.value=newValue
})
// 11.监听input框的改变,同时改变数据
node.addEventListener('input',e=>{
const arr1=i.nodeValue.split('.') //['more','like']
const arr2=arr1.slice(0,arr1.length-1) //['more']
const final=arr2.reduce((total,cur)=>total[cur],vm.$data)
final[arr1[arr1.length-1]]=e.target.value //vm.$data[]
})
}
})
}
//递归修改结点的子节点
node.childNodes.forEach(child=>{
fragment_compile(child)
})
}
vm.$el.appendChild(fragment)
}
/**
* 5.实现响应式更新值 创建发布者:收集和响应订阅者,让订阅者更新
*/
class Dependency{
constructor(){
this.subscribers=[]
}
addSub(sub){
this.subscribers.push(sub)
}
notify(){
this.subscribers.forEach(sub=>sub.update())
}
}
/**
* 6.创建订阅者类
* key属性名
* callback更新执行的回调函数
*/
class Watcher {
constructor(vm,key,callback){
this.vm=vm
this.key=key
this.callback=callback
// 临时属性
Dependency.temp=this
//访问属性能触发getter,例如more[like],为啥要触发是想要在get里执行dependy里添加订阅者,进行第8步
key.split('.').reduce((total,cur)=>total[cur],vm.$data)
// 因为getter会多次触发而一个属性只用订阅一次所以触发getter后让temp等于空,这样不会再add了
Dependency.temp=null
}
update(){
// console.log(this),this是触发的那个watcher
console.log(this.key)
const value=this.key.split('.').reduce((total,cur)=>total[cur],this.vm.$data)
this.callback(value)
}
}
const vm=new Vue(
{
el:'#app',
data:{
name:'tom',
more:{
hobby:'唱歌'
}
}
}
)
</script>
</html>
vue3 proxy
不用defineProperty而是用proxy代理整个对象,对象属性的增加删除,数组等都可以。