前面几节我们说了vue初始化显示的一些内容,重点在指令解析上面。
当初始化的视图绘制好了之后,问题又来了——当数据发生了更新,视图怎么办呢?
这就引出了今天的知识点——数据绑定。
什么是数据绑定?
数据绑定,就是一旦更新了data中的某个属性数据,所有界面上直接或间接使用了此属性的节点就会更新。
Title
直接使用:{{firstname}}
间接使用:{{fullname}}
这个如何实现的呢?答案是数据劫持。
什么是数据劫持?
先来回顾一下之前的数据代理:
https://www.jianshu.com/p/3442527d543a
数据代理是通过对Vue对象增加get和set方法,可以监听data里面的信息,从而达到代理目的,那data的数据改变后,如何让视图也更新呢?
Title
{{name}}
我们可以通过defineProperty()来监视vm.data中所有属性(任意层次)数据的变化,一旦变化就去更新界面,这就是数据劫持,简单吧~~
也就是说,数据代理是在vm对象中增加get和set,而数据劫持要在vm.data对象中增加get和set,二者对目的不一样哈,可以对比着来看。
实现数据劫持
我们先把前面的数据代理和模板解析两部分整理出来合成一处:
function Vue(options){
this.$options = options
var data = this._data = this.$options.data
//=====数据代理=====//
Object.keys(data).forEach(key=>{
this._proxy(key)
})
//=====数据劫持=====//
//=====模板解析=====//
this.$compile = new Compile(options.el,this)
}
Vue.prototype = {
_proxy(key){
Object.defineProperty(this,key,{
configurable:false,
enumerable:true,
get(){
return this._data[key]
},
set(newVal){
this._data[key] = newVal
}
})
}
}
function Compile(el,vm){
this.$vm = vm
this.$el = document.querySelector(el)
console.log(this.$el)
if(this.$el){
this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理
this.init() //开始处理
this.$el.appendChild(this.$fragment) //塞回原位
}
}
Compile.prototype = {
//将#app里面的节点都转到文档碎片中
node2Fragment(el){
var fragment = document.createDocumentFragment()
var child = null
while(child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
},
//处理碎片中的信息
init(){
this.compileElement(this.$fragment)
},
//正则匹配
compileElement(el){
var childNodes = el.childNodes;
[].slice.call(childNodes).forEach(node=>{
var text = node.textContent // 获取文本信息
var reg = /\{\{(.*)\}\}/
//这里增加指令的处理
if(node.nodeType === 1){
//如果是元素节点
this.compile(node)
} else if(node.nodeType === 3 && reg.test(text)) {
this.compileText(node,RegExp.$1)
} else if(node.childNodes && node.childNodes.length) {
//递归
this.compileElement(node)
}
})
},
//处理元素节点
compile(node){
var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
[].slice.call(nodeAttrs).forEach(attr=>{
var attrName = attr.name;//获取属性名
if(attrName.indexOf('v-') === 0){//判断是否是指令
var exp = attr.value; //获取属性值,也就是触发的方法名
var dir = attrName.substring(2) // 截取字符串,得到on:click
if(dir.indexOf('on') === 0){ //判断事件指令
var eventType = dir.split(':')[1]; //获取事件类型
var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
if(eventType && fn) {
node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
}
} else if(dir.indexOf('bind') === 0) { // 一般指令
var dirType = dir.split(':')[1] // 获取指令类型class
if(dirType === 'class') {
var oldClassName = node.className; //原来的class类
var newClassName = '' //动态class类
var classObj = eval('(' + exp + ')'); //解析为object对象
for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
if(classObj[key]) {
newClassName += ' ' + key
}
}
node.className = oldClassName + newClassName // 设置className
}
}
node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
}
})
},
//用vm.data信息,替换大括号的name
compileText(node,exp){
node.textContent = this.getVMVal(exp)
},
//处理层级问题
getVMVal(exp){ // a.b.c
var val = this.$vm._data
var arr = exp.split('.') //["a", "b", "c"]
console.log(exp)
arr.forEach(k=>{
//debugger
val = val[k] // 层级递进
})
return val
}
}
剩下的数据劫持部分,我们先脱离以上的代码单独实现,然后再拼回去,这样可以避免代码过多造成的干扰。
其实我们要实现的无非是这样的东西:
//示例对象
var vm = {
data: {
name: '简单数据',
a:{
b:{
c:'嵌套数据'
}
}
}
}
//数据劫持
observe(vm)
console.log(vm)
function observe(data){
//对data进行defineProperty处理,嵌套的数据要用递归
if(!data || typeof data!=='object'){//递归的退出条件
return;
}
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])//对data的每一个key进行定义,如果是嵌套对象,放到defineReactive()函数内部处理
})
}
function defineReactive(data,key,val){
observe(val)//处理嵌套对象:执行递归,确保所有层次的key都可以被定义
Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get(){
return val
},
set(newVal){
val = newVal //修改为最新值
}
})
}
打印一下结果:
可以看出,在vm.data的所有层次的属性都增加了get和set函数监听,我们把上述代码先合进去,起个名字叫myvue.js吧:
function Vue(options){
this.$options = options
var data = this._data = this.$options.data
//数据代理
Object.keys(data).forEach(key=>{
this._proxy(key)
})
//数据劫持
observe(data)
//模板解析
this.$compile = new Compile(options.el,this) //模板解析
}
Vue.prototype = {
_proxy(key){
Object.defineProperty(this,key,{
configurable:false,
enumerable:true,
get(){
return this._data[key]
},
set(newVal){
this._data[key] = newVal
}
})
}
}
function Compile(el,vm){
this.$vm = vm
this.$el = document.querySelector(el)
console.log(this.$el)
if(this.$el){
this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理
this.init() //开始处理
this.$el.appendChild(this.$fragment) //塞回原位
}
}
Compile.prototype = {
//将#app里面的节点都转到文档碎片中
node2Fragment(el){
var fragment = document.createDocumentFragment()
var child = null
while(child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
},
//处理碎片中的信息
init(){
this.compileElement(this.$fragment)
},
//正则匹配
compileElement(el){
var childNodes = el.childNodes;
[].slice.call(childNodes).forEach(node=>{
var text = node.textContent // 获取文本信息
var reg = /\{\{(.*)\}\}/
//这里增加指令的处理
if(node.nodeType === 1){
//如果是元素节点
this.compile(node)
} else if(node.nodeType === 3 && reg.test(text)) {
this.compileText(node,RegExp.$1)
} else if(node.childNodes && node.childNodes.length) {
//递归
this.compileElement(node)
}
})
},
//处理元素节点
compile(node){
var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
[].slice.call(nodeAttrs).forEach(attr=>{
var attrName = attr.name;//获取属性名
if(attrName.indexOf('v-') === 0){//判断是否是指令
var exp = attr.value; //获取属性值,也就是触发的方法名
var dir = attrName.substring(2) // 截取字符串,得到on:click
if(dir.indexOf('on') === 0){ //判断事件指令
var eventType = dir.split(':')[1]; //获取事件类型
var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
if(eventType && fn) {
node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
}
} else if(dir.indexOf('bind') === 0) { // 一般指令
var dirType = dir.split(':')[1] // 获取指令类型class
if(dirType === 'class') {
var oldClassName = node.className; //原来的class类
var newClassName = '' //动态class类
var classObj = eval('(' + exp + ')'); //解析为object对象
for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
if(classObj[key]) {
newClassName += ' ' + key
}
}
node.className = oldClassName + newClassName // 设置className
}
}
node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
}
})
},
//用vm.data信息,替换大括号的name
compileText(node,exp){
node.textContent = this.getVMVal(exp)
},
//处理层级问题
getVMVal(exp){ // a.b.c
var val = this.$vm._data
var arr = exp.split('.') //["a", "b", "c"]
console.log(exp)
arr.forEach(k=>{
//debugger
val = val[k] // 层级递进
})
return val
}
}
//新增部分
function observe(data){
//对data进行defineProperty处理,嵌套的数据要用递归
if(!data || typeof data!=='object'){//递归的退出条件
return;
}
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])//对data里面的每一个key进行定义
})
}
function defineReactive(data,key,val){
observe(val)//先执行递归,确保嵌套对象的key都可以被定义
Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get(){
console.log('getData>>>',key)
return val
},
set(newVal){
console.log('setData',key)
val = newVal //修改为最新值
}
})
}
把这个自定义的js引入测试一下:
Title
{{name}}
输出结果:
当点击按钮时,get和set已经执行到了,但页面并未发生变化。
下面的工作,就是完善get和set函数了,这俩哥们需要负责实现:
当数据发生了变化,要更新到视图。
我们写的Compile类是处理视图的,主要是做了一些解析工作,为了研究方便,这里我们就只考虑大括号表达式。
以{{name}}为例,思路为:
1、{{name}}在页面视图初始化完成后,订阅更新视图的函数(this.compileText)
2、在data的set函数里面,触发更新视图函数。
因此,我们需要增加一个观察者对象进来:
function Vue(options){
this.$options = options
var data = this._data = this.$options.data
//数据代理
Object.keys(data).forEach(key=>{
this._proxy(key)
})
//数据劫持
observe(data)
//模板解析
this.$compile = new Compile(options.el,this) //模板解析
}
Vue.prototype = {
_proxy(key){
Object.defineProperty(this,key,{
configurable:false,
enumerable:true,
get(){
return this._data[key]
},
set(newVal){
this._data[key] = newVal
}
})
}
}
function Compile(el,vm){
this.$vm = vm
this.$el = document.querySelector(el)
if(this.$el){
this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理
this.init() //开始处理
this.$el.appendChild(this.$fragment) //塞回原位
}
}
Compile.prototype = {
//将#app里面的节点都转到文档碎片中
node2Fragment(el){
var fragment = document.createDocumentFragment()
var child = null
while(child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
},
//处理碎片中的信息
init(){
this.compileElement(this.$fragment)
},
//正则匹配
compileElement(el){
var childNodes = el.childNodes;
[].slice.call(childNodes).forEach(node=>{
var text = node.textContent // 获取文本信息
var reg = /\{\{(.*)\}\}/
//这里增加指令的处理
if(node.nodeType === 1){
//如果是元素节点
this.compile(node)
} else if(node.nodeType === 3 && reg.test(text)) {
this.compileText(node,RegExp.$1) //更新视图动作
//订阅更新视图函数
dep.register(RegExp.$1,()=>{
//如果data发生了变化,需要再次调用this.compileText这个函数来更新视图
this.compileText(node,RegExp.$1)
})
} else if(node.childNodes && node.childNodes.length) {
//递归
this.compileElement(node)
}
})
},
//处理元素节点
compile(node){
var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
[].slice.call(nodeAttrs).forEach(attr=>{
var attrName = attr.name;//获取属性名
if(attrName.indexOf('v-') === 0){//判断是否是指令
var exp = attr.value; //获取属性值,也就是触发的方法名
var dir = attrName.substring(2) // 截取字符串,得到on:click
if(dir.indexOf('on') === 0){ //判断事件指令
var eventType = dir.split(':')[1]; //获取事件类型
var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
if(eventType && fn) {
node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
}
} else if(dir.indexOf('bind') === 0) { // 一般指令
var dirType = dir.split(':')[1] // 获取指令类型class
if(dirType === 'class') {
var oldClassName = node.className; //原来的class类
var newClassName = '' //动态class类
var classObj = eval('(' + exp + ')'); //解析为object对象
for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
if(classObj[key]) {
newClassName += ' ' + key
}
}
node.className = oldClassName + newClassName // 设置className
}
}
node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
}
})
},
//用vm.data信息,替换大括号的name
compileText(node,exp){
console.log('最新值是:',this.getVMVal(exp))
node.textContent = this.getVMVal(exp)
},
//处理层级问题
getVMVal(exp){ // a.b.c
var val = this.$vm._data
var arr = exp.split('.') //["a", "b", "c"]
arr.forEach(k=>{
//debugger
val = val[k] // 层级递进
})
return val
}
}
//新增部分
function observe(data){
//对data进行defineProperty处理,嵌套的数据要用递归
if(!data || typeof data!=='object'){//递归的退出条件
return;
}
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])//对data里面的每一个key进行定义
})
}
function defineReactive(data,key,val){
observe(val)//先执行递归,确保嵌套对象的key都可以被定义
Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get(){
console.log('get操作:',key,'===>',val)
return val
},
set(newVal){
console.log('set操作:',key,'===>',newVal)
val = newVal //修改为最新值
//触发更新视图函数
dep.emit(key)
}
})
}
//引入观察订阅模式
var dep = new Dep()
function Dep(){
this.subs = {}
}
Dep.prototype.register = function (key, callback) {
this.subs[key] = callback
}
Dep.prototype.emit = function (key) {
this.subs[key]()
}
执行结果:
这样更新视图就完成了。
不过,这只是最简单的一种情况哦,如果是嵌套对象就嗝屁了:
Title
{{a.b}}
输出结果:
为啥会这样呢?很简单,原因写在了代码注释里,自己看:
function Vue(options){
this.$options = options
var data = this._data = this.$options.data
//数据代理
Object.keys(data).forEach(key=>{
this._proxy(key)
})
//数据劫持
observe(data)
//模板解析
this.$compile = new Compile(options.el,this) //模板解析
}
Vue.prototype = {
_proxy(key){
Object.defineProperty(this,key,{
configurable:false,
enumerable:true,
get(){
return this._data[key]
},
set(newVal){
this._data[key] = newVal
}
})
}
}
function Compile(el,vm){
this.$vm = vm
this.$el = document.querySelector(el)
if(this.$el){
this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理
this.init() //开始处理
this.$el.appendChild(this.$fragment) //塞回原位
}
}
Compile.prototype = {
//将#app里面的节点都转到文档碎片中
node2Fragment(el){
var fragment = document.createDocumentFragment()
var child = null
while(child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
},
//处理碎片中的信息
init(){
this.compileElement(this.$fragment)
},
//正则匹配
compileElement(el){
var childNodes = el.childNodes;
[].slice.call(childNodes).forEach(node=>{
var text = node.textContent // 获取文本信息
var reg = /\{\{(.*)\}\}/
//这里增加指令的处理
if(node.nodeType === 1){
//如果是元素节点
this.compile(node)
} else if(node.nodeType === 3 && reg.test(text)) {
this.compileText(node,RegExp.$1) //更新视图动作
//订阅更新视图函数
dep.register(RegExp.$1,()=>{ //注册的key是a.b
//如果data发生了变化,需要再次调用this.compileText这个函数来更新视图
this.compileText(node,RegExp.$1)
})
} else if(node.childNodes && node.childNodes.length) {
//递归
this.compileElement(node)
}
})
},
//处理元素节点
compile(node){
var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
[].slice.call(nodeAttrs).forEach(attr=>{
var attrName = attr.name;//获取属性名
if(attrName.indexOf('v-') === 0){//判断是否是指令
var exp = attr.value; //获取属性值,也就是触发的方法名
var dir = attrName.substring(2) // 截取字符串,得到on:click
if(dir.indexOf('on') === 0){ //判断事件指令
var eventType = dir.split(':')[1]; //获取事件类型
var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
if(eventType && fn) {
node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
}
} else if(dir.indexOf('bind') === 0) { // 一般指令
var dirType = dir.split(':')[1] // 获取指令类型class
if(dirType === 'class') {
var oldClassName = node.className; //原来的class类
var newClassName = '' //动态class类
var classObj = eval('(' + exp + ')'); //解析为object对象
for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
if(classObj[key]) {
newClassName += ' ' + key
}
}
node.className = oldClassName + newClassName // 设置className
}
}
node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
}
})
},
//用vm.data信息,替换大括号的name
compileText(node,exp){
console.log('最新值是:',this.getVMVal(exp))
node.textContent = this.getVMVal(exp)
},
//处理层级问题
getVMVal(exp){ // a.b.c
var val = this.$vm._data
var arr = exp.split('.') //["a", "b", "c"]
arr.forEach(k=>{
//debugger
val = val[k] // 层级递进
})
return val
}
}
//新增部分
function observe(data){
//对data进行defineProperty处理,嵌套的数据要用递归
if(!data || typeof data!=='object'){//递归的退出条件
return;
}
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])//对data里面的每一个key进行定义
})
}
function defineReactive(data,key,val){
observe(val)//先执行递归,确保嵌套对象的key都可以被定义
Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get(){
console.log('get操作:',key,'===>',val)
return val
},
set(newVal){
console.log('set操作:',key,'===>',newVal)
val = newVal //修改为最新值
//触发更新视图函数
dep.emit(key) // 这里的key是b,而不是a.b
}
})
}
//引入观察订阅模式
var dep = new Dep()
function Dep(){
this.subs = {}
}
Dep.prototype.register = function (key, callback) {
this.subs[key] = callback
}
Dep.prototype.emit = function (key) {
console.log(key)
this.subs[key]()
}
主要是嵌套对象注册的key,跟触发时的key不一致了,那怎么办呢?请看下回分解。