参考文章
https://baijiahao.baidu.com/s?id=1678787660555400574&wfr=spider&for=pc
https://blog.csdn.net/weixin_40712618/article/details/126232040
vue双向绑定的核心
- Object.defineProperty()。通过这个方法定义一个值。
- 调用,使用了get方法
- 赋值,使用了set方法
自己实现一个数据双向绑定
- 界面影响数据,通过事件监听,改变数据,触发set函数
- 数据影响界面,改变数据,触发set函数,操纵dom节点
<body>
<!-- 自己手动实现一个js双向绑定 -->
<div id="app">
<input type="text" id="aa">
<span id="bb"></span>
</div>
</body>
<script>
let obj={};
let value='hello';
Object.defineProperty(obj,'hello',{
get: function(){
console.log('get value:'+ value)
return value
},
set: function(newvalue){
value=newvalue
console.log('set value:'+value)
document.getElementById('aa').value=value;
document.getElementById('bb').innerHTML=value
}
})
document.addEventListener('keyup',function(e){
obj.hello=e.target.value
})
</script>
vue实现数据双向绑定,第一步:data数据绑定到dom节点中(内容绑定)
- 自己手动实现代码
<body>
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
</body>
<script>
<!-- 定义模版编译函数,传入节点,和vue实例对象,将vm的data替换进去 -->
function compile(node,vm){
let reg=/\{\{(.*)\}\}/;
if(node.nodeType===1){
let attr=node.attributes;
for(let i=0;i<attr.length;i++){
if(attr[i].nodeName=='v-model'){
let name=attr[i].nodeValue;
node.value=vm.data[name];
node.removeAttribute('v-model')
}
}
}
if(node.nodeType===3){
if(reg.test(node.nodeValue)){
let name=RegExp.$1;
name=name.trim();
node.nodeValue=vm.data[name]
}
}
}
<!-- 定义碎片化文档函数,传入节点和它的所有子节点,和vue实例对象,遍历每个节点,模版编译每个节点,再装入碎片化文档这个容器-->
function nodeToFragment(node,vm){
let fragment=document.createDocumentFragment();
let child;
while(child=node.firstChild){
compile(child,vm);
fragment.appendChild(child)
}
return fragment
}
<!-- 定义Vue构造函数,传入对象包含数据和元素id,碎片化文档函数结束后,再将这个碎片化文档容器加入到文档中-->
function Vue(options){
this.data=options.data;
let id=options.el;
let dom=nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom);
}
let vm=new Vue({
el:'app',
data:{
text:'hello3333333'
}
})
</script>
documentFragment 碎片化文档
- 可以把它当作是一个dom节点容器
- 每当一个dom节点插入到文档中就会引发一次浏览器的回流,十分消耗资源
- 使用碎片化文档,就是把所有节点先放入到一个容器中,最后再把容器插入到文档中,浏览器只回流了一次
- 使用appendChild方法将原dom树的节点添加到documentFragment中,会删除原来的节点
vue实现数据双向绑定,第二步:view层影响modle层
- 通过defineProperty把vm的data设置为访问器属性
- 通过事件监听,获取最新的值,给data赋值,触发set函数
- 自己手动实现代码
<body>
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
</body>
<script>
function compile(node,vm){
let reg=/\{\{(.*)\}\}/;
if(node.nodeType===1){
let attr=node.attributes;
for(let i=0;i<attr.length;i++){
if(attr[i].nodeName=='v-model'){
let name=attr[i].nodeValue;
<!-- 给input节点绑定监听事件,改变访问器属性的数据,触发set函数-->
node.addEventListener('input',function(e){
vm[name]=e.target.value
})
node.value=vm[name];
node.removeAttribute('v-model')
}
}
}
if(node.nodeType===3){
if(reg.test(node.nodeValue)){
let name=RegExp.$1;
name=name.trim();
node.nodeValue=vm[name]
}
}
}
function nodeToFragment(node,vm){
let fragment=document.createDocumentFragment();
let child;
while(child=node.firstChild){
compile(child,vm);
fragment.appendChild(child)
}
return fragment
}
<!-- 给vm对象,增加访问器属性-->
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
get:function(){
return val
},
set:function(newValue){
if(newValue===val){
return
}
val=newValue
console.log('newvalue: '+val)
}
})
}
<!-- 将vm的data对应的属性,增加到vm作为访问器属性-->
function observe(obj,vm){
for(let key of Object.keys(obj)){
defineReactive(vm,key,obj[key])
}
}
function Vue(options){
this.data=options.data;
let data=this.data;
observe(data,this)
let id=options.el;
let dom=nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom);
}
let vm=new Vue({
el:'app',
data:{
text:'22'
}
})
</script>
vue实现数据双向绑定,第三步:modle层影响view层
- 发布订阅者模式
<!-- 三个订阅者-->
let sub1={update:function(){console.log(1)}}
let sub2={update:function(){console.log(2)}}
let sub3={update:function(){console.log(3)}}
<!-- 调度中心有几个订阅者-->
function Dep(){
this.subs=[sub1,sub2,sub3]
}
<!-- 调度中心构造函数的原型对象增加一个notify方法,遍历调度中心订阅者去执行update方法-->
Dep.prototype.notify=function(){
this.subs.forEach(function(sub){
sub.update();
})
}
<!-- 构造一个调度中心实例对象-->
let dep=new Dep()
<!-- 定义一个发布者-->
let pub={
publish:function(){
dep.notify();
}
}
<!-- 发布者发布消息-->
pub.publish()
- set函数的第二个作用就是发布消息
- 每当new一个vue,主要做两件事情,监听数据和编译html
- 监听数据的过程,会给data的每一个属性生成一个dep(调度中心)
- 在编译html,会给数据绑定的节点生成一个订阅者watcher,watcher会把自己添加到相应属性的dep容器中
<body>
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
</body>
<script>
<!-- 调度中心构造函数-->
function Dep(){
this.subs=[]
}
<!-- 定义调度中心构造函数的原型对象,添加订阅者,订阅者更新数据-->
Dep.prototype={
addSub(sub){
this.subs.push(sub)
},
notify(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
<!-- 定义订阅者构造函数,Dep.target是个全局变量,先赋值为订阅者实例,订阅者更新数据后,Dep.target再改为null-->
function Watcher(vm,node,name){
Dep.target=this;
this.vm=vm;
this.node=node;
this.name=name;
this.update();
Dep.target=null;
}
<!-- 定义订阅者构造函数的原型对象,更新界面的节点,获取vm访问器属性的数据-->
Watcher.prototype={
update(){
this.get();
this.node.nodeValue=this.value;
},
get(){
this.value=this.vm[this.name]
}
}
function compile(node,vm){
let reg=/\{\{(.*)\}\}/;
if(node.nodeType===1){
let attr=node.attributes;
for(let i=0;i<attr.length;i++){
if(attr[i].nodeName=='v-model'){
let name=attr[i].nodeValue;
node.addEventListener('input',function(e){
vm[name]=e.target.value
})
node.value=vm[name];
node.removeAttribute('v-model')
}
}
}
if(node.nodeType===3){
if(reg.test(node.nodeValue)){
let name=RegExp.$1;
name=name.trim();
<!-- 不是直接修改界面的节点,而是新建订阅者实例-->
// node.nodeValue=vm[name]
new Watcher(vm,node,name)
}
}
}
function nodeToFragment(node,vm){
let fragment=document.createDocumentFragment();
let child;
while(child=node.firstChild){
compile(child,vm);
fragment.appendChild(child)
}
return fragment
}
function defineReactive(obj,key,val){
// 每一个访问器属性,都定义一个调度中心
let dep=new Dep();
Object.defineProperty(obj,key,{
get:function(){
// 如果Dep.target是个watcher实例,就要增加一个订阅者
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set:function(newValue){
if(newValue===val){
return
}
val=newValue
console.log('newvalue: '+val)
// 修改数据值,调度者中心就要通知订阅者更新
dep.notify();
}
})
}
function observe(obj,vm){
for(let key of Object.keys(obj)){
defineReactive(vm,key,obj[key])
}
}
function Vue(options){
this.data=options.data;
let data=this.data;
observe(data,this)
let id=options.el;
let dom=nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom);
}
let vm=new Vue({
el:'app',
data:{
text:'22'
}
})
</script>
总结
- 给data中数据的属性,通过defineProperty方法定义为访问器属性,这样就拥有了set函数和get函数和调度中心
- 通过createDocumentFragment方法定义一个碎片化文档,可以把它理解为一个容器,然后将dom节点放进这个容器中
- 在放每个节点进入容器中的过程,可以将vue模版代码编译为正常的html代码
- 编译过程,需要将访问器属性的数据值替换vue的插值语法和v-model语法的值。在替换的过程,就会调用get函数,给调度中心添加订阅者。
- 替换v-model的值时,需要绑定监听事件,一旦input值改变,就会修改访问器属性的数据值,就会修改dom节点值。
- 访问器属性的数据值改变,就会触发set函数,调度中心就会发出通知订阅者更新,其实就是改变节点值
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?