喜欢请关注 会不定时更新 ***
简介
- 最近花功夫研究了一下
Vue
双向数据绑定的原理,代码看了很多遍,才逐渐掌握它的设计思路。- 对于用
Vue
的人来说,也是估计被面试问的最多的一个问题之一,了解它给自己竞争来增加优势,毕竟当被问时你回答的头头是道,是一件非常nice
的事情。- 以下实现简易双向绑定以及事件监听,并且尽可能的对代码加了详尽的注释。
首先看下效果图
效果图.gif
效果图.gif
文件目录
├─myVue
| ├─compile.js
| ├─index.html
| ├─index.js
| ├─observer.js
| └watcher.js
流程分析
我的源码
流程分析(请下载我的源码对照来看)
- 首先,我们创建一个
index.html
,如下
<div id='app'>
<input v-model="title"></input>
<p>{{title}}</p>
</div>
- 接下来,我们创建一个
Vue
实例
const vm = new mVue({
el:'#app',
data: {
title: '有一种放手叫余生不打扰'
}
})
- 那么,我们创建了
vue
会发生什么尼?在index.js
可以看到,它会执行以下两个操作
// 初始化数据劫持
observe(this.data)
// 初始化 进行编译
new Compile(this.el ,this)
- 先调用观察者
observe
对data
中的数据进行劫持,利用Object.defineProperty
方法,这样我们就能监听到data
数据中的变动。例如上例就会对data
里面的title
进行数据监听。同时,会对每个属性里创建一个dep
,来存放所有订阅这个属性的数组。对于dep
,你可能不太清楚,先接着往下看。
- 再进行
Compile
编译,编译是干嘛的尼?
首先会获取创建Vue
实例时,el
属性绑定的dom
元素的内容,如上例就是获取到div#app
的内容,在compile.js
里面,我们会对div#app
下面的所有元素进行遍历,来比对是否有绑定的数据或事件。
对于上例,当我们比对到<input v-model="title"></input>
时,我们发现了这个input
标签使用v-model
绑定了title
属性,于是乎,我们会创建一个watcher
,问题又来了,watcher
又是啥?watcher
就是一个封装的对象,创建时会存进去该元素绑定该属性的更新方法。
例如:刚刚检索到的input
标签,创建的watcher
大致是下面这样的(看下面代码)。我们创建watcher
时,会自动调一遍get()
方法,将自己添加到titile
属性的dep
中,再将自己的update
方法放进去。因为我们在observe
时已经对所有属性进行了劫持,当每个属性变化时,会遍历一遍自己的dep
数组,并逐个触发每个update
函数。
// 我是一个Watcher模型
Watcher = {
get(){
// 将watcher自己添加到title的dep数组里
},
update(){
// 将当前这个input标签的值 替换为 新的值
}
- 当我们遍历完成时,会发现两个元素绑定了属性
title
。
<input v-model="title"></input>
<p>{{title}}</p>
所以title
属性的dep
数组中存放两条数据,每个是个watcher
// 类似于
[ {watcher-for-p}, {watcher-for-input} ]
- 当我们改变
titile
的值改变时,会触发setter
劫持,接着遍历dep
数组,触发每个的更新。
例如:title = 123
,遍历dep
,先会通知订阅了这个属性的p标签更新,再通知input
跟新。从而数据到视图的绑定。
- 当我们在发现
v-model
的时候,还另外干了一件事,就是给这个input
添加事件监听,监听输入,改变绑定的数据
// 在compile.js中有这一段代码
node.addEventListener('input',function(e){
var newVal = e.target.value
if(val === newVal){
return
}
self.vm[exp] = newVal; // 更改绑定属性的值
val = newVal;
})
- 当我们在
input
输入时,会改变data
中绑定的对应属性的值,触发属性的劫持,执行订阅者更新,完成了input->数据->视图
的更新。
从而完成了数据的双向绑定。
源码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#app{
margin-top:100px;
text-align: center;
}
#app button{
color: #0084ff;
border-color: #0084ff;
background: #fff;
}
</style>
</head>
<body>
<div id='app'>
<input v-model="title"></input>
<p>{{title}}</p>
<BUtton v-on:click="clickMe">快来点我</BUtton>
</div>
</body>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./compile.js"></script>
<script src="./index.js"></script>
<script type="text/javascript">
const vm = new mVue({
el:'#app',
data: {
title: '有一种放手叫余生不打扰'
},
methods:{
clickMe(){
this.title = '你点了我'
}
}
})
setTimeout(()=>{
vm.id = 888
},1000)
</script>
</html>
index.js
// 创建mVue类
function mVue(options){
var self = this
this.data = options.data
this.el = options.el
this.methods = options.methods
// 遍历data对象
Object.keys(this.data).forEach(function(key){
// 对data里每个属性进行代理
self.proxyKey(key)
})
// 对data对象的数据进行劫持 监听
observe(this.data)
// 初始化 进行编译
new Compile(this.el ,this)
}
// 属性代理
mVue.prototype = {
proxyKey(key){
var self = this;
// 对vm实例进行数据劫持
// 对data中存在的键 使类似于vm.id进行访问时 实际操作的是vm.data.id
Object.defineProperty(this,key,{
enumerable:true, // 可枚举
configurable:true, // 可修改
get(){
return self.data[key] // 包裹data层
},
set(newVal){
self.data[key] = newVal // 包裹data层
}
})
}
}
Observer.js
// 对 对象进行劫持的初始化方法
function observe(value,vm){
// 只有当该值为对象时递归遍历劫持
if(!value || typeof value !== 'object'){
return
}
return new Observe(value)
}
// 观察者构造器
function Observe(data){
this.data = data;
// 初始化 对data的所有数据进行劫持
this.walk(data)
}
Observe.prototype = {
walk:function(data){
var self = this
// 遍历data的所有属性
Object.keys(data).forEach(function(key){
// 对属性进行劫持
self.defineReactive(data,key,data[key])
})
},
defineReactive:function(data,key,val){
// dep==>用来存储这条属性的订阅信息的构造器
// dep.subs 存储订阅者的数组
// 例如 页面中有<p>{{id}}<p/> <h1>{{id}}<h1/> 两个标签都绑定了data中的id属性
// 进行compile时,会被获取到,添加到该id属性的dep.subs中
// 上例该dep.subs中就有两条数据,每条数据即一个watcher
// 每个watcher中包含了该订阅者节点数据更新的方法
// 当数据变化后只需遍历dep.subs数组,执行相关的更新方法即可
var dep = new Dep()
//这里对子属性进行递归操作,因为该属性可能是对象嵌套对象
var childObj = val
observe(childObj)
// 这里开始利用Object.defineProperty进行属性劫持
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
// get方法 即数据的获取方法
get(){
// Dep.target默认是null
// 只有当compile页面时,每发现一条绑定data中属性时,会生成一个watcher,
// 让Dep.target = watcher,再触发该属性的get方法,完成添加
// 再置空
if(Dep.target){
// 添加一个订阅者Dep.target=watcher 到dep.subs
dep.addSub(Dep.target)
}
// 普通访问时直接返回数据
return val
},
set(newVal){
// 当新旧值一样时,不触发更新
if( newVal === val){
return
}
// 否则赋值 并 触发dep构造器的notice通知方法
val = newVal
dep.notice()
}
})
}
}
// Dep构造器 存储每条属性的订阅信息
// 每个data中属性都会生成一个dep构造器
let Dep = function(){
// 存储该属性被订阅的数组
this.subs = []
}
Dep.prototype = {
// 添加订阅信息
addSub : function(sub){
this.subs.push(sub)
},
// 触发更新
notice:function(){
// 遍历所有的订阅者 执行对应的跟新函数
this.subs.forEach(function(sub){
sub.update()
})
}
}
Dep.target = null
compile.js
function Compile(el, vm){
this.vm = vm;
this.el = document.querySelector(el)
// 虚拟dom
this.fragment = null
// 初始化第一次编译
this.init()
}
Compile.prototype = {
init(){
// 判断绑定的元素是否存在
if(this.el){
// 创建一个虚拟dom
this.fragment = this.nodeToFragment(this.el)
this.compileElement(this.fragment)
this.el.appendChild(this.fragment)
}else{
console.log('Dom元素不存在')
}
},
nodeToFragment(el){
// 创建虚拟dom
var fragment = document.createDocumentFragment()
var child = el.firstChild
while(child){
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
// 编译元素
compileElement(el){
var self = this;
var childNodes = el.childNodes;
// [].slice.call() 将伪数组转化为数组 遍历
[].slice.call(childNodes).forEach(function(node){
var reg = /\{\{(.*)\}\}/
var text = node.textContent
// 如果是文本节点 且 符合表达式
if(self.isElementNode(node)){
self.compile(node)
}else if(self.isTextNode(node) && reg.test(text)){
// 对文本进行编译
self.compileText(node,reg.exec(text)[1])
}
// 如果存在子节点 递归编译
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
})
},
// 文本进行编译
compileText(node,exp){
var self = this;
var initText = this.vm[exp]
// 初始化要更新一次文本 用于第一次页面显示内容
this.updateText(node,initText)
new Watcher(this.vm,exp,function(value){
self.updateText(node,value)
})
},
compile(node){
var nodeAttrs = node.attributes;
var self = this;
// 节点伪数组 转化为数组
[].slice.call(nodeAttrs).forEach(function(attr){
var attrName = attr.name;
// 判断是否是v-开头的指令
if(self.isDirective(attrName)){
var exp = attr.value // 绑定的数据
var dir = attrName.substring(2) // 指令名称
// 判断是否是:on事件指令
if(self.isEventDirective(dir)){
self.compileEvent(node, self.vm, exp, dir)
}else{
//否则 编译v-model指令
self.compileModel(node,self.vm,exp,dir);
}
// 对元素中的指令进行移除
node.removeAttribute(attrName);
}
})
},
// 编译事件
compileEvent(node, vm, exp, dir){
var eventType = dir.split(':')[1]
var cb = vm.methods && vm.methods[exp]
if(eventType && cb){
node.addEventListener(eventType,cb.bind(vm),false)
}
},
// 编译v-nodel
compileModel(node, vm, exp, dir){
console.log(dir)
var self = this;
var val = this.vm[exp]
if(dir == 'model'){
self.updateModel(node,val)
}
new Watcher(self.vm,exp,function(value){
self.updateModel(node,value)
})
node.addEventListener('input',function(e){
var newVal = e.target.value
if(val === newVal){
return
}
self.vm[exp] = newVal;
val = newVal;
})
},
// 判断属性是否是指令
isDirective(attr){
return attr.indexOf('v-') == 0
},
// 判断属性是否是事件指令
isEventDirective(dir){
console.log('dir',dir,dir.indexOf('on:') == 0)
return dir.indexOf('on:') == 0
},
// 判断是否是文本节点
isTextNode(node){
return node.nodeType == 3
},
// 判断是否是元素节点
isElementNode(node){
return node.nodeType == 1
},
// 跟新文本
updateText(node,value){
node.textContent = typeof value == 'undefined' ? '' : value
},
// 更新model绑定
updateModel(node,value){
node.value = typeof value == 'undefined' ? '' : value
}
}
watcher.js
// 订阅者构造器
function Watcher(vm,exp,cb){
this.vm = vm // mVue实例
this.exp = exp // 被订阅的属性
this.cb = cb //更新该属性的回调方法
this.value = this.get(); // 初始化时将自己添加到订阅器
}
Watcher.prototype = {
get(){
// 令Dep.target等于当前的watcher实例
Dep.target = this;
// 触发对应属性的getter方法
// 会将当前watcher添加到dep.subs中
var value = this.vm.data[this.exp]
// 添加完成后置空
Dep.target = null
return value
},
// 该订阅者的更新方法
update(){
this.run()
},
run(){
// 也就是触发构造函数的cb回调,触发视图跟新
let value = this.vm.data[this.exp]
let oldVal = this.value
if(value !== oldVal){
this.value = value
this.cb.call(this.vm,value,oldVal)
}
}
}