我们先来看如下vue实例有什么属性
var vm = new Vue({
el:'#app',
data:{
name:'hello',
sex:1
}
})
console.log(vm);
可以看到我们设置的data属性被提升到Vue实例上了
Vue
$attrs: (...)
$listeners: (...)
sex: (...)
name: (...)
$data: (...)
$props: (...)
$isServer: (...)
$ssrContext: (...)
...
他是怎样做到的? 继续往下面看我们发现他使用了代理,将data的属性代理到vue上,所以才能进行this.属性名
进行访问或设置
...
$el: div#app
get $attrs: ƒ reactiveGetter()
set $attrs: ƒ reactiveSetter(newVal)
get $listeners: ƒ reactiveGetter()
set $listeners: ƒ reactiveSetter(newVal)
get sex: ƒ proxyGetter()
set sex: ƒ proxySetter(val)
get name: ƒ proxyGetter()
set name: ƒ proxySetter(val)
我们也可以简单实现 首先创建一个vue实例,构造方法接收options
class Vue{
constructor(options){
this.$options = options
this.$el = document.querySelector(this.$options.el)
this.proxyData()
}
//代理data
proxyData(){
for(let key in this.$options.data){
Object.defineProperty(this,key,{
enumerable:true,
configurable:false,
get(){
return this.$options.data[key]
},
set(val){
this.$options.data[key] = val
}
})
}
}
}
打印实例结果返回
var vm = new Vue({
el:'#app',
data:{
name:'zh',
sex:'1'
}
})
console.log(vm);
//
Vue {$options: {…}, $el: div#app}
name: (...)
sex: (...)
$options: {el: "#app", data: {…}}
$el: div#app
get name: ƒ get()
set name: ƒ set(val)
get sex: ƒ get()
set sex: ƒ set(val)
__proto__: Object
此时我们可以使用 vm.name
获取到data的name vm.name=123
设置data的name
我们大概知道他的机制之后来进行一个数据绑定的实现
如下结构 当我们修改vue的data属性值的时候 对应的html绑定的相关属性也要进行改变
<div id="app">
<input type="text" v-model="name" />
<h1>{{name}}</h1>
<h2 v-html="name"></h2>
<button type="button" @click="change">修改name</button>
</div>
这里要用到事件机制,当某属性值被修改时触发某事件对html绑定的对应属性值进行更新 所以基于上面的代码,我们还要在data属性值改变时进行操作 定义方法observe
...
observe(){
for(let key in this.$options.data){
var value = this.$options.data[key]
var that = this
Object.defineProperty(this.$options.data,key,{
enumerable:true,
configurable:false,
get(){
console.log('获取属性');
return value
},
set(val){
console.log('设置属性');
value = val
}
})
}
}
定义observe方法监听data属性值的变化
我们发现可以在属性值被修改时进行一些操作,我们完成“当属性值被修改时改变html”不就可以了吗? 如何实现? 我们的html可能有很多元素/元素的属性都绑定了该data的属性 如
<h1>{{name}}</h1>
<h2 v-html="name"></h2>
现在的问题是如何在值变化的时候修改所有绑定了相应属性的html元素 这里我们用一个对象watchEvent
来存储data属性发生改变时要触发的事件 如
watchEvent = {
event:[event,event],
name:[event1,event2],
sex:[event1,event2]
}
...
this.$el = document.querySelector(this.$options.el)
this.$watchEvent = {}
...
eventn是一个事件对象,这个事件对象包括绑定了该data属性值的信息;如哪个节点绑定的,节点绑定的属性是什么等, 我们在先外面定义这个对象
//生成事件对象
class Watch{
//vm: app(vue实例)对象
//key data对应的属性
//node html节点
//attr html节点属性
constructor(vm,key,node,attr){
this.vm = vm
this.key = key
this.node = node
this.attr = attr
}
//更新属性
update(){
this.node[this.attr] = this.vm[this.key]
}
}
当data属性发生变化时外面将调用这个update方法对象节点进行更新 在observe
中
set(val){
console.log('设置属性');
value = val
//循环调用事件对象,使绑定值更新
if(that.$watchEvent[key]){
that.$watchEvent[key].forEach((item,i)=>{
item.update()
})
}
}
接下来的问题是我们如何知道某个节点绑定了data的属性? 我们要对html进行编译,拿到el
的子节点进行操作 vue对象定义 complie
方法
...
//编译html
compile(){
//遍历el下的所有子节点
this.$el.childNodes.forEach((item,index)=>{
// console.log(item);
//判断当前的节点类型 (nodeType==1==节点)和文本类型(nodeType==3==文本)
if(item.nodeType==1){
//v-html
if(item.hasAttribute('v-html')){
//获取v-html属性绑定的data属性
let vmKey = item.getAttribute('v-html').trim()
//设置属性值
item.innerHTML = this[vmKey]
//生成事件对象
let watcher = new Watch(this,vmKey,item,'innerHTML')
//将事件对象添加到 watchEvent、
if(this.$watchEvent[vmKey]){
this.$watchEvent[vmKey].push(watcher)
}else{
this.$watchEvent[vmKey] = []
this.$watchEvent[vmKey].push(watcher)
}
}
}
})
}
...
编译html在初始的时候直接访问到data的值,并根据绑定的属性值生成事件对象class Watch
,存储到 watchEvent 这样当属性值修改时html也会发生变化
接下来我们实现数据双向绑定(v-model)
//v-model
if(item.hasAttribute('v-model')){
//数据双向绑定
let vmKey = item.getAttribute('v-model').trim()
if(this.hasOwnProperty(vmKey)){
item.value = this[vmKey]
//生成事件对象
let watcher = new Watch(this,vmKey,item,'value')
//将事件对象添加到 watchEvent、
if(this.$watchEvent[vmKey]){
this.$watchEvent[vmKey].push(watcher)
}else{
this.$watchEvent[vmKey] = []
this.$watchEvent[vmKey].push(watcher)
}
item.addEventListener('input',()=>{
this[vmKey] = item.value
})
}
}
在基于之前的操作我们实现数据双向绑定就很简单了,直接监听input事件即可
接下来完成基于事件的数据绑定 在原来的vue对象添加methods
var vm = new Vue({
el:'#app',
data:{
name:'zh'
},
methods:{
change(){
this.name='点了一下'
}
}
})
if(item.nodeType==1){
...
//@click
if(item.hasAttribute('@click')){
//获取method
let method = item.getAttribute('@click').trim()
item.addEventListener('click',(e)=>{
this.eFn = this.$options.methods[method].bind(this)
this.eFn(e)
})
}
}
...
按钮点击时触发change方法,对name值进行修改
接下来完成文本节点的更新
由于我们要实现的文本节点还包了一个h1
<h1>{{name}}</h1>
默认el的文本节点只能找到el的子节点,并不能找到子节点的子节点,为了解决这个问题我们要对complie
进行改进,使其递归查找
class Vue{
...
this.proxyData()
this.observe()
//传入操作节点
this.compile(this.$el)
...
compile(cNode){
cNode.childNodes.forEach((item,index)=>{
...
if(item.childNodes.length>0){
this.compile(item)
}
})
}
文本节点部分
if(item.nodeType==1){
...
}else if(item.nodeType==3){
//text
let reg = /\{\{(.*?)\}\}/g
let text = item.textContent
item.textContent = text.replace(reg,(match,vmKey)=>{
vmKey = vmKey.trim()
if(this.hasOwnProperty(vmKey)){
//生成事件对象
let watcher = new Watch(this,vmKey,item,'textContent')
//将事件对象添加到 watchEvent、
if(this.$watchEvent[vmKey]){
this.$watchEvent[vmKey].push(watcher)
}else{
this.$watchEvent[vmKey] = []
this.$watchEvent[vmKey].push(watcher)
}
}
})
}
到此一个简单的数据绑定完成
本示例源码已上传至 GitHub