- 使用Vue开发项目有一段时间了,一直停留在使用的阶段,未成真正的深入了解,最近看来一些关于Vue原理的博客和视频,在此记录一些心得方便以后回忆。
vue.js的两个核心
- 数据驱动:ViewModel,保证数据和视图的一致性。
- 组件系统:应用类UI可以看作全部是由组件树构成的。
这里先介绍一下Vue的数据双向绑定原理:
vue的数据双向绑定是通过数据劫持&发布者-订阅者模式。所谓的数据双向绑定表现出来的就是通过v-model指令实现视图层变化促使逻辑层变化,当逻辑层变化视图层也发生相应的变化。
- view层–>model层:个人认为通过给相应的元素添加chang(input)事件,当触发chang(input)事件的时候就可以通过发布订阅者模式来改变model层。
- model层—>view层:个人认为通过Object.defineProperty()来劫持对象属性的set和get方法,当对象属性变化或获取时就可以通过该方法来进行监控,触发相应的监听回调。这样我们就可以监听MVVM中的逻辑层中的数据,然后通过发布订阅者模式来触发视图层的改变。
数据数据双向绑定原理图

Object.defineProperty数据劫持:
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app">
<p id="name"></p>
</div>
<script>
var obj = {};
Object.defineProperty(obj, "name", {
get: function() {
return document.querySelector("#name").innerHTML;
},
set: function(val) {
document.querySelector("#name").innerHTML = val;
}
});
obj.name = "Jerry";
</script>
</body>
</html>
订阅者和发布者模式
- 订阅者和发布者模式,通常用于消息队列中.一般有两种形式来实现消息队列,一是使用生产者和消费者来实现,二是使用订阅者-发布者模式来实现,其中订阅者和发布者实现消息队列的方式,就会用订阅者模式.
所谓的订阅者,就像我们在日常生活中,订阅报纸一样。我们订阅报纸的时候,通常都得需要在报社或者一些中介机构进行注册。当有新版的报纸发刊的时候,邮递员就需要向订阅该报纸的人,依次发放报纸。(这上面三句话引用别人的,感觉很形象)


Vue原理开始实现:

这上面是MVVM原理图
通过上述分析得到一下步骤:
- 需要一个数据劫持Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- 需要一个模板解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
- 需要一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- 再开始前我们看看需要实现的最终目的是什么?这时候需要一个index.html,如下面的代码,在这个html文件中,我们会发现包含大量的Vue指令、插值表达式、事件绑定等。还包含KVue这个对象,该对象就像我们经常写的Vue对象一样。接下来的目的是将html和Vue联系起来实现一个简单的Vue原理。
index.html代码如下:
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app">
<!-- 插值绑定 -->
<p>{{name}}</p>
<!-- 指令解析 -->
<p k-text="name"></p>
<p>{{age}}</p>
<p>{{doubleAge}}</p>
<!-- 双向绑定 -->
<input type="text" k-model="name" />
<!-- 事件处理 -->
<button @click="changeName">呵呵</button>
<!-- html内容解析 -->
<div k-html="html"></div>
</div>
<script src="./compile.js"></script>
<script src="./Dep.js"></script>
<script src="./Watcher.js"></script>
<script src="./KVue.js"></script>
<script>
const kaikeba = new KVue({
el: "#app",
data: {
name: "I am test.",
age: 12,
html: "<button>这是一个按钮</button>"
},
created() {
setTimeout(() => {
this.name = "我是测试";
}, 1500);
},
methods: {
changeName() {
this.name = "哈喽,开课吧";
this.age = 1;
}
}
});
</script>
</body>
</html>
第一步实现Observer数据劫持:
- 这时候需要将data中的数据显示到html对应的属性中,首先我们需要创建一个KVue类,并且遍历KVue实例中data里的属性,接着我们需要实现Compile解析器,解析模板指令,并替换模板数据,初始化视图。**
- 数据监听器的核心方法就是Object.defineProperty(),通过遍历循环对所有属性值进行监听,并对其进行Object.defineProperty()处理。
Observe代码:
class KVue{
constructor(options) {
this.$options = options;
this.$data = options.data;
this.observe(this.$data);
//实例化解析器
new Compile(options.el,this)
}
observe(dataObj){
if(!dataObj || typeof(dataObj) !=='object'){
return false;
}
Object.keys(dataObj).forEach(key=>{
this.defineReactive(dataObj,key,dataObj[key])
//代理data中的属性到vue实例上
this.proxyData(key);
})
}
// 将data每一个属性添加set 和get 方法
defineReactive(dataObj,key,value){
this.observe(value)
Object.defineProperty(dataObj,key,{
get(){
return value
},
set(newValue){
if(newValue === value){
return;
}
value = newValue
}
})
}
}
- 在上述代码中只是简单的对KVue实例中data里的属性进行数据劫持和实例化解析器Compile,并没有在劫持的方法中进行相应的回调监听,因为接下来会牵扯到Dep订阅者和wather观察者。
第二步实现模板解析器Compile(上述代码中已经 new Compile(options.el,this))
- 可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
Compile代码:
//new Compile(el,vm)
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
// // 因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中(这部分就是虚拟Dom)
this.fragment = this.node2Fragment(this.$el);
// 开始编译
this.compile(this.fragment);
// 将编译完成的html添加到app上
this.$el.appendChild(this.fragment);
}
}
node2Fragment(el) {
let frag = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
//如果文档树中已经存在了 el.firstChild,它将从文档树中删除,然后插入它的新位置。也就是说Dom树中的元素在一个个减少,frag中元素在慢慢的添加。
frag.appendChild(child)
}
return frag;
}
compile(el) {
const childNodes = el.childNodes;
//childNodes不是真正的数组,是一个类数组,需要用Array.from()方法来将他转化为真正的数组,这样他才能使用forEach()方法。
Array.from(childNodes).forEach(node => {
//判断是不是元素节点(nodeType === 1)
if (this.isElement(node)) {
//这是元素节点
console.log(`这是元素节点${node}`)
//获取每一个node的属性,因为需要识别还有k-、@等Vue特殊的指令
const attributes = node.attributes;
//同样将attributes转为真正的数组
Array.from(attributes).forEach(attr => {
const attrName = attr.name; //属性名(k-html,k-text,k-model,@...)
const exp = attr.value; // 属性值(data中的各个属性name,age,html)
//k-指令
if (this.isDirective(attrName)) {
// 提取k-model,k-text,k-html 后面的名称(model,text,html)
var dir = attrName.substring(2)
// 调用指令解析方法,这里会以dir来创建一下方法如 html(node=节点,vm=KVue实例,exp=data的属性name,age..)
this[dir] && this[dir](node, this.$vm, exp);
}
//@事件
if (this.isEvent(attrName)) {
console.log(`这是事件${attrName}`)
//提取事件类别(click,input,change...)
var dir = attrName.substring(1)
//调用事件解析方法
this.EventHander(node, this.$vm, exp, dir)
}
})
}
//判断是不是{{...}}
if (this.isInterpolation(node)) {
//这是插值表达式
console.log(`这是插值表达式${node.textContent}`)
//调用插值表达式的解析方法
this.textCompile(node);
}
//递归遍历含有子节点的Node
if (node.childNodes && node.childNodes.length != 0) {
this.compile(node)
}
})
}
//model解析方法
model(node, vm, exp) {
this.updated(node, vm, exp, 'model')
//为需要双向绑定的元素绑定input change事件
node.addEventListener("input", function (e) {
vm[exp] = e.target.value
})
}
//1.html解析方法
html(node, vm, exp) {
this.updated(node, vm, exp, 'html')
}
//2.文本解析方法
text(node, vm, exp) {
this.updated(node, vm, exp, 'text')
}
//3.事件解析方法
EventHander(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp];
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm));
}
}
// 编译插值表达式
textCompile(node) {
let exp = RegExp.$1;
this.updated(node, this.$vm, exp, 'text')
}
//初始化页面,注册wather的集合
updated(node, vm, exp, dir) {
let updateFn = this[dir + 'Update'];
updateFn && updateFn(node, vm[exp])
new Watcher(vm, exp, function (value) {
updateFn(node, value)
})
}
textUpdate(node, value) {
node.textContent = value
}
htmlUpdate(node, value) {
node.innerHTML = value
}
modelUpdate(node, value) {
node.value = value
}
// 判断是否为指令
isDirective(attrName) {
return attrName.indexOf('k-') === 0;
}
// 判断是否为事件
isEvent(attrName) {
return attrName.indexOf('@') === 0;
}
// 判断是否为元素节点
isElement(node) {
return node.nodeType == 1;
}
// 判断是否为插值表达式
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
第三步实现Dep
- 发布者,每一个vue的data属性都对应一个Dep,Dep是一个数组,可能包含多个或零个,取决于html中是否使用了该属性,如果同一个属性多次出现在html中,那么该属性对应的Dep含有多个watcher。
- 初始化,定义Dep实例含有一个deps属性,该属性是一个数组。
- 添加一个addDep方法,用来添加watcher实例到对应的Dep中
- 添加一个notify方法,用来通知每一个watcher进行页面更新。
// 订阅者 用来管理watcher
class Dep{
constructor() {
this.deps=[]
}
addDep(dep){
this.deps.push(dep)
}
notify(){
this.deps.forEach(item=>{
item.update()
})
}
}
第四步完善Observe
- 将每一个vue中data的属性都分配一个发布者Dep
- 每当获取该属性值得时候,就可以将该属性对应的订阅者watcher添加到Dep数组中了。
- 每当属性值发生变化时,该属性对应的Dep就会调用自身的notify() 方法来通知所包含的watcher改变视图。
//new KVue(data:{....})
class KVue{
constructor(options) {
this.$options = options;
this.$data = options.data;
this.observe(this.$data);
// new Watcher();
// this.name;
this.$data.name = "我是"
new Compile(options.el,this)
//绑定created方法到KVue实例上,使用了call(this) 这也就是为什么created中this指向KVue的原因了
if(options.created){
options.created.call(this)
}
}
observe(dataObj){
if(!dataObj || typeof(dataObj) !=='object'){
return false;
}
Object.keys(dataObj).forEach(key=>{
this.defineReactive(dataObj,key,dataObj[key])
//代理data中的属性到vue实例上
this.proxyData(key);
})
}
// 代理data中的属性到vue实例上 ,方便获取和设置KVue实例data中的值
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key]
},
set(newValue){
console.log(newValue)
this.$data[key] = newValue
}
})
}
// 将data每一个属性添加set 和get 方法
defineReactive(dataObj,key,value){
this.observe(value)
const dep = new Dep();
Object.defineProperty(dataObj,key,{
get(){
//当获取该属性值得时候,就可以将该属性对应的订阅者添加到Dep数组中了。
Dep.target && dep.addDep(Dep.target)
return value
},
set(newValue){
if(newValue === value){
return;
}
value = newValue
//当值发生变化时,dep通知它所包含的watcher进行相应的更新。
dep.notify();
}
})
}
}
第五步实现Watcher
-
在Compile代码中我们能够发现,在遍历整个dom节点中只要发现data中含有的属性或者methods中的方法都会new Watcher生产一个watcher;
-
也就是说页面中同一个data属性可能有多个watcher。
-
watcher用来将node和KVue中的数据联系起来,在Observe代码中,我们已经为每一个data属性添加了一个自定义的get方法,在该方法中我们需要将对应的watcher添加到Dep中,那么如何才能确保每次只有一个watcher实例能,这时候我们需要通过Dep.target来指向watcher实例。
-
初始化在Compile代码中 new Watcher(Kvue实例,data中对应的属性,初始化页面的方法)
-
Dep.target指向自己,确保单例
-
访问一下vm[exp],触发get方法,将watcher添加得到Dep中
-
清除Dep.target;
-
创建一个更新方法,通过调用实例化传过来的初始化方法来改变vue页面。
class Watcher {
constructor(vm, exp, cb) {
this.$vm = vm;
this.exp = exp;
this.$cb = cb;
Dep.target = this;
vm[exp]
Dep.target = null;
}
update() {
console.log("我要更新处理",this.$vm[this.exp])
this.$cb.call(this.$vm,this.$vm[this.exp])
}
}
本文探讨了Vue.js的核心——数据驱动和组件系统,重点讲解了Vue的数据双向绑定原理,通过数据劫持(Object.defineProperty)和发布者-订阅者模式实现。在实现过程中涉及Observer、Compile、Watcher的创建,以及Dep类的设计,揭示了Vue如何确保数据变化与视图同步。
3185

被折叠的 条评论
为什么被折叠?



