大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
一、Vue对比其他框架原理
Vue相对于React,Angular更加综合一点。AngularJS则使用了“脏值检测”。
React则采用避免直接操作DOM的虚拟dom树。而Vue则采用的是 Object.defineProperty特性(这在ES5中是无法slim的,这就是为什么vue2.0不支持ie8以下的浏览器)
Vue可以说是尤雨溪从Angular中提炼出来的,又参照了React的性能思路,而集大成的一种轻量、高效,灵活的框架。
二、Vue的原理
Vue的原理可以简单地从下列图示所得出
- 通过建立虚拟dom树
document.createDocumentFragment()
,方法创建虚拟dom树。 - 一旦被监测的数据改变,会通过Object.defineProperty定义的数据拦截,截取到数据的变化。
- 截取到的数据变化,从而通过订阅——发布者模式,触发Watcher(观察者),从而改变虚拟dom的中的具体数据。
- 最后,通过更新虚拟dom的元素值,从而改变最后渲染dom树的值,完成双向绑定
Vue的模式是m-v-vm模式,即(model-view-modelView),通过modelView作为中间层(即vm的实例),进行双向数据的绑定与变化。
而实现这种双向绑定的关键就在于:
Object.defineProperty和订阅——发布者模式浙两点。
下面我们通过实例来实现Vue的基本双向绑定。
三、Vue双向绑定的实现
3.1 简易双绑
首先,我们把注意力集中在这个属性上:Object.defineProperty。
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。语法:Object.defineProperty(obj, prop, descriptor)
什么叫做,定义或修改一个对象的新属性,并返回这个对象呢?
var obj = {}; Object.defineProperty(obj,'hello',{ get:function(){ //我们在这里拦截到了数据 console.log("get方法被调用"); }, set:function(newValue){ //改变数据的值,拦截下来额 console.log("set方法被调用"); } }); obj.hello//输出为“get方法被调用”,输出了值。 obj.hello = 'new Hello';//输出为set方法被调用,修改了新值
输出结果如下:
可以从这里看到,这是在对更底层的对象属性进行编程。简单地说,也就是我们对其更底层对象属性的修改或获取的阶段进行了拦截(对象属性更改的钩子函数)。
在这数据拦截的基础上,我们可以做到数据的双向绑定:
var obj = {}; Object.defineProperty(obj,'hello',{ get:function(){ //我们在这里拦截到了数据 console.log("get方法被调用"); }, set:function(newValue){ //改变数据的值,拦截下来额 console.log("set方法被调用"); document.getElementById('test').value = newValue; document.getElementById('test1').innerHTML = newValue; } }); //obj.hello; //obj.hello = '123'; document.getElementById('test').addEventListener('input',function(e){ obj.hello = e.target.value;//触发它的set方法 })
html:
<div id="mvvm"> <input v-model="text" id="test"></input> <div id="test1"></div> </div>
在线演示:demo演示
在这我们可以简单的实现了一个双向绑定。但是到这还不够,我们的目的是实现一个Vue。
3.2 Vue初始化(虚拟节点的产生与编译)
3.2.1 Vue的虚拟节点容器
function nodeContainer(node, vm, flag){ var flag = flag || document.createDocumentFragment(); var child; while(child = node.firstChild){ compile(child, vm); flag.appendChild(child); if(child.firstChild){ // flag.appendChild(nodeContainer(child,vm)); nodeContainer(child, vm, flag); } } return flag; }
这里几个注意的点:
while(child = node.firstChild)
把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。document.createDocumentFragment();
是一个虚拟节点的容器树,可以存放我们的虚拟节点。- 上面的函数是个迭代,一直循环到节点的终点为止。
3.2.2 Vue的节点初始化编译
先声明一个Vue对象
function Vue(options){ this.data = options.data; var id = options.el; var dom = nodeContainer(document.getElementById(id),this); document.getElementById(id).appendChild(dom); } //随后使用他 var Demo = new Vue({ el:'mvvm', data:{ text:'HelloWorld', d:'123' } })
接下去的具体得初始化内容
//编译 function compile(node, vm){ var reg = /\{\{(.*)\}\}/g;//匹配双绑的双大括号 if(node.nodeType === 1){ var attr = node.attributes; //解析节点的属性 for(var i = 0;i < attr.length; i++){ if(attr[i].nodeName == 'v-model'){ var name = attr[i].nodeValue; node.value = vm.data[name];//讲实例中的data数据赋值给节点 //node.removeAttribute('v-model'); } } } //如果节点类型为text if(node.nodeType === 3){ if(reg.test(node.nodeValue)){ // console.dir(node); var name = RegExp.$1;//获取匹配到的字符串 name = name.trim(); node.nodeValue = vm.data[name]; } } }
代码解释:
- 当nodeType为1的时候,表示是个元素。同时我们进行判断,如果节点中的指令含有
v-model
这个指令,那么我们就初始化,进行对节点的值的赋值。 - 如果nodeType为3的时候,也就是text节点属性。表示你的节点到了终点,一般都是节点的前后末端。我们常常在这里定义我们的双绑值。此时一旦匹配到了双绑(双大括号),即进行值的初始化。
至此,我们的Vue初始化已经完成。
在线演示:demo1
3.3 Vue的声明响应式
3.3.1 定义Vue的data的属性响应式
function defineReactive (obj, key, value){ Object.defineProperty(obj,key,{ get:function(){ console.log("get了值"+value); return value;//获取到了值 }, set:function(newValue){ if(newValue === value){ return;//如果值没变化,不用触发新值改变 } value = newValue;//改变了值 console.log("set了最新值"+value); } }) }
这里的obj我们这定义为vm实例或者vm实例里面的data属性。
PS:这里强调一下,defineProperty这个方法,不仅可以定义obj的直接属性,比如obj.hello这个属性。也可以间接定义属性比如:obj.middle.hello。这里导致的效果就是两者的hello属性都被定义成响应式了。
用下列的observe方法循环调用响应式方法。
function observe (obj,vm){ Object.keys(obj).forEach(function(key){ defineReactive(vm,key,obj[key]); }) }
然后再Vue方法中初始化:
function Vue(options){ this.data = options.data; var data = this.data; ------------------------- observe(data,this);//这里调用定义响应式方法 ------------------------- var id = options.el; var dom = nodeContainer(document.getElementById(id),this); document.getElementById(id).appendChild(dom); //把虚拟dom渲染上去 }
在编译方法中v-model属性找到的时候去监听:
function compile(node, vm){ var reg = /\{\{(.*)\}\}/g; if(node.nodeType === 1){ var attr = node.attributes; //解析节点的属性 for(var i = 0;i < attr.length; i++){ if(attr[i].nodeName == 'v-model'){ var name = attr[i].nodeValue; -------------------------//这里新添加的监听 node.addEventListener('input',function(e){ console.log(vm[name]); vm[name] = e.target.value;//改变实例里面的值 }); ------------------------- node.value = vm[name];//讲实例中的data数据赋值给节点 //node.removeAttribute('v-model'); } } } }
以上我们实现了,你再输入框里面输入,同时触发getter&setter,去改变vm实例中data的值。也就是说MVVM的图例中经过getter&setter已经成功了。接下去就是订阅——发布者模式。
在线演示:demo2
实现效果:
3.4 订阅——发布者模式
什么是订阅——发布者?简单点说:你微信里面经常会订阅一些公众号,一旦这些公众号发布新消息了。那么他就会通知你,告诉你:我发布了新东西,快来看。
这种情景下,你就是订阅者,公众号就是发布者。
所以我们要模拟这种情景,我们先声明3个订阅者:
var sub1 = { update:function(){ console.log(1); } } var sub2 = { update:function(){ console.log(2); } } var sub3 = { update:function(){ console.log(3); } }
每个订阅者对象内部声明一个update方法来触发订阅属性。
再声明一个发布者,去触发发布消息,通知的方法::
function Dep(){ this.subs = [sub1,sub2,sub3];//把三个订阅者加进去 } Dep.prototype.notify = function(){//在原型上声明“发布消息”方法 this.subs.forEach(function(sub){ sub.update(); }) } var dep = new Dep(); //pub.publish(); dep.notify();
我们也可以声明另外一个中间对象
var dep = new Dep(); var pub = { publish:function(){ dep.notify(); } } pub.publish();//这里的结果是跟上面一样的
实现效果:
到这,我们已经实现了:
- 修改输入框内容 => 触发修改vm实例里的属性值 => 触发set&get方法
- 订阅成功 => 发布者发出通知notify() => 触发订阅者的update()方法
接下来重点要实现的是:如何去更新视图,同时把订阅——发布者模式进去watcher观察者模式?
3.5 观察者模式
先定义发布者:
function Dep(){ this.subs = []; } Dep.prototype ={ add:function(sub){//这里定义增加订阅者的方法 this.subs.push(sub); }, notify:function(){//这里定义触发订阅者update()的通知方法 this.subs.forEach(function(sub){ console.log(sub); sub.update();//下列发布者的更新方法 }) } }
再定义观察者(订阅者):
function Watcher(vm,node,name){ Dep.global = this;//这里很重要!把自己赋值给Dep函数对象的全局变量 this.name = name; this.node = node; this.vm = vm; this.update(); Dep.global = null;//这里update()完记得清空Dep函数对象的全局变量 } Watcher.prototype.update = function(){ this.get(); switch (this.node.nodeType) { //这里去通过判断节点的类型改变视图的值 case 1: this.node.value = this.value; break; case 3: this.node.nodeValue = this.value; break; default: break; }; } Watcher.prototype.get = function(){ this.value = this.vm[this.name];//这里把this的value值赋值,触发data的defineProperty方法中的get方法! }
以上需要注意的点:
- 在Watcher函数对象的原型方法update里面更新视图的值(实现watcher到视图层的改变)。
- Watcher函数对象的原型方法get,是为了触发defineProperty方法中的get方法!
- 在new一个Watcher的对象的时候,记得把Dep函数对象赋值一个全局变量,而且及时清空。至于为什么这么做,我们接下来看。
function defineReactive (obj, key, value){ var dep = new Dep();//这里每一个vm的data属性值声明一个新的订阅者 Object.defineProperty(obj,key,{ get:function(){ console.log(Dep.global); ----------------------- if(Dep.global){//这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了 dep.add(Dep.global); } ----------------------- return value; }, set:function(newValue){ if(newValue === value){ return; } value = newValue; dep.notify();//触发了update()方法 } }) }
这里有一点需要注意:
在上述圈起来的地方:if(Dep.global)
是在第一次new Watcher()
的时候,进入update()
方法,触发这里的get
方法。这里非常的重要的一点!在此时new Watcher()
只走到了this.update();
方法,此刻没有触发Dep.global = null
函数,所以值并没有清空,所以可以进到dep.add(Dep.global);
方法里面去。
而第二次后,由于清空了Dep的全局变量,所以不会触发add()方法。
PS:这个思路容易被忽略,由于是参考之前一个博主的代码影响,我自己想了很多方法改变,但是在这种情景下难以实现别的更好的交互方式。
所以我暂时现在只能使用Dep的全局变量的方式,来实现Dep函数与Watcher函数的交互。(如果是ES6的模块化方法会不一样)
而后我会尽量找寻其他更好的方法来实现Dep函数与Watcher函数的交互。
紧接着在text
节点和绑定了的input
节点(别忘记了这个节点)new Watcher
的方法来触发以上的内容:
// 如果节点为input if(node.nodeType === 1){ ........... ---------- new Watcher(vm,node,name) // 别忘记给input添加观察者模式 ---------- } //如果节点类型为text if(node.nodeType === 3){ if(reg.test(node.nodeValue)){ // console.dir(node); var name = RegExp.$1;//获取匹配到的字符串 name = name.trim(); // node.nodeValue = vm[name]; ------------------------- new Watcher(vm,node,name);//这里到了一个新的节点,new一个新的观察者 ------------------------- } }
至此,vue双向绑定已经简单的实现。
3.6 最终效果
在线演示:Codepen实现Vue的demo(有时候要FQ)
在线源码参考:demo4
下列是全部的源码,仅供参考。
HTML:
<div id="mvvm"> <input v-model="d" id="test">{{text}} <div>{{d}}</div> </div>
JS:
var obj = {}; function nodeContainer(node, vm, flag){ var flag = flag || document.createDocumentFragment(); var child; while(child = node.firstChild){ compile(child, vm); flag.appendChild(child); if(child.firstChild){ nodeContainer(child, vm, flag); } } return flag; } //编译 function compile(node, vm){ var reg = /\{\{(.*)\}\}/g; if(node.nodeType === 1){ var attr = node.attributes; //解析节点的属性 for(var i = 0;i < attr.length; i++){ if(attr[i].nodeName == 'v-model'){ var name = attr[i].nodeValue; node.addEventListener('input',function(e){ vm[name] = e.target.value; }); node.value = vm[name];//讲实例中的data数据赋值给节点 node.removeAttribute('v-model'); } } } //如果节点类型为text if(node.nodeType === 3){ if(reg.test(node.nodeValue)){ // console.dir(node); var name = RegExp.$1;//获取匹配到的字符串 name = name.trim(); // node.nodeValue = vm[name]; new Watcher(vm,node,name); } } } function defineReactive (obj, key, value){ var dep = new Dep(); Object.defineProperty(obj,key,{ get:function(){ console.log(Dep.global); if(Dep.global){ dep.add(Dep.global); } console.log("get了值"+value); return value; }, set:function(newValue){ if(newValue === value){ return; } value = newValue; console.log("set了最新值"+value); dep.notify(); } }) } function observe (obj,vm){ Object.keys(obj).forEach(function(key){ defineReactive(vm,key,obj[key]); }) } function Vue(options){ this.data = options.data; var data = this.data; observe(data,this); var id = options.el; var dom = nodeContainer(document.getElementById(id),this); document.getElementById(id).appendChild(dom); } function Dep(){ this.subs = []; } Dep.prototype ={ add:function(sub){ this.subs.push(sub); }, notify:function(){ this.subs.forEach(function(sub){ console.log(sub); sub.update(); }) } } function Watcher(vm,node,name){ Dep.global = this; this.name = name; this.node = node; this.vm = vm; this.update(); Dep.global = null; } Watcher.prototype = { update:function(){ this.get(); switch (this.node.nodeType) { case 1: this.node.value = this.value; break; case 3: this.node.nodeValue = this.value; break; default: break; } }, get:function(){ this.value = this.vm[this.name]; } } var Demo = new Vue({ el:'mvvm', data:{ text:'HelloWorld', d:'123' } })
四、回顾
我们再来通过一张图回顾一下整个过程:
从上可以看出,大概的过程是这样的:
- 定义Vue对象,声明vue的data里面的属性值,准备初始化触发observe方法。
- 在Observe定义过响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个vue的data的属性值,就添加到一个Watcher对象到订阅者里面去。
- 每当形成一个Watcher对象的时候,去定义它的响应式。即
Object.defineProperty()
定义。这就导致了一个Observe里面的getter&setter方法与订阅者形成一种依赖关系。 - 由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者我的数据改变了,你需要更新。
- 订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。
- Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。
- 至此,视图的值改变了。形成了双向绑定MVVM的效果。
五、后记
至此,我们通过解析vue的绑定原理,实现了一个非常简单的Vue。
我们可以再借鉴此思路的情况下,进行我们需要的定制框架的二次开发。如果开发人数尚可的话,可以实现类似微信小程序自己有的一套框架。
我非常重视技术的原理,只有真正掌握技术的原理,才能在原有的技术上更好地去提高和开发。
ps:此文是较早之前写的,不够规范,后面会修改一个ES6的版本。下方是参考链接,灵感来源于其他博主,我进行了修正优化和代码解释。
参考链接:
原文地址(原创博客):http://www.tangyida.top/detail/150
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/168164.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...