1.MVVM(入口函数)
- 为vm添加数据代理
- 调用其他函数(数据劫持,模版编译)
function MVVM(options){
// 保存传入的配置
this.$options = options
// 保存data对象
var data = this._data = options.data
// 遍历data中所有的key
Object.keys(data).forEach(key => {
// 为vm添加相同的key属性来对data进行数据代理
this._proxyData(data,key)
})
// 数据劫持,监听所有data中所有层次属性值的变动
observe(data)
// 模版解析
new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxyData: function(data,key){
// 保存vm
var me = this
// 为vm添加属性,代理同名的data属性数据
Object.defineProperty(me,key,{
configurable: false, // 不可重定义
enumerable: true, // 可枚举 该属性名能被Object.keys()获取
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
2.observer(数据劫持)
- 通过对data中所有层次的属性添加get/set方法来添加属性值的变化
- 为每个属性new一个dep,dep里面的数组用来保存用到该属性的信息(Watcher)
- 每次更新数据时,如果新的值不是对象,则什么也不执行。但是如果新的值是对象,那么即使新的值与旧值一模一样,他们完全是两个数据(只是里面的属性和值一样罢了),这样先前创建的dep也就失效了,而新的属性也需要再次进行数据劫持,为其创建新的dep,该节点原有的watcher也应该添加到dep数组中
function observe(value){
// 只有value为对象类型才进行数据劫持
if(value instanceof Object){
new Observer(value)
}
}
function Observer(data){
// 保存data
this.data = data
// 为data所有的key添加数据劫持
Object.keys(data).forEach(key => {
this.defineReactive(data,key,data[key])
})
}
Observer.prototype = {
defineReactive: function(data,key,val){
// val:在添加get/set方法前保存属性值,而这个属性值也将供get/set方法return和修改
// 间接递归调用为该属性值进行数据劫持
observe(val)
// 为每个属性new 一个 dep
var dep = new Dep()
// 为属性添加get/set方法
Object.defineProperty(data,key,{
configurable: false,
enumerable: true,
get(){
// 只有在new Watcher的时候Dep.target != null
if(Dep.target){
if(!Dep.target.hasOwnProperty(dep.id)){
// 将当前watcher添加到dep.subs中
Dep.target.addToDep(dep)
// 为watcher添加属性,防止重复添加到同一个dep中
Dep.target[dep.id] = dep
}
}
return val
},
set(newVal){
if(newVal !== val){
val = newVal
// 为新的值添加数据劫持
observe(val)
// 通知所有订阅者(当前dep里面的所有watcher)
dep.notify()
}
}
})
}
}
var uid =0
function Dep(){
// 每个new出来的Dep都有自己独有的id
this.id = uid++
// subs这个数组用来装watcher
this.subs = []
}
Dep.prototype = {
notify(){
this.subs.forEach(watcher => {
watcher.update()
})
}
}
Dep.target = null
3.compile 编译模版,创建Watcher
- 对模版的指令进行解析,事件指令则为其添加事件监听,其他指令则调用相应的函数进行解析
- 解析数据绑定的表达式时,为其创建一个Watcher。根据表达式用到的属性,触发其get方法,就可以得到这个属性相应的dep,从而将当前Watcher添加到dep里面的数组中
- 编译v-model比较麻烦,因为input的类型有好几种,对于text需要绑定value属性,同时监听input事件
对于radio,将绑定的数据与单选框的value属性值进行比较,相等则选中,否则相反。而监听radio选中是change事件,如果被选中则修改绑定的数据为单选框的value属性值
function Compile(el,vm){
// 保存vm,以便访问vm._data或者vm.$opstions.methods
this.$vm = vm
this.$el = document.querySelector(el)
// 只有这个dom元素存在才进行编译解析
if(this.$el){
// 将这个dom元素的所有子节点移入到fragment中
this.$fragment = this.nodeToFragment(this.$el)
// 调用初始化函数,编译fragment
this.init()
// 将编译好的fragment插入到el中
this.$el.appendChild(this.$fragment)
}
}
Compile.prototype = {
nodeToFragment: function(el){
// 创建fragment
var fragment = document.createDocumentFragment()
var child
while(child = el.firstChild){
// 将原生节点移动到fragment中
fragment.appendChild(child)
}
// 返回fragment
return fragment
},
init: function(){
// 编译this.$fragment的子节点
this.compileElement(this.$fragment);
},
compileElement: function(el){ // 此函数用来编译el的所有子节点
// 获取el的所有子节点
var childNodes = el.childNodes
// 遍历所有子节点
Array.from(childNodes).forEach(node => {
// 匹配 {{}} 的正则表达式 禁止贪婪
var reg = /\{\{(.*?)\}\}/
// 如果该节点是 元素节点
if(node.nodeType === 1){
// 编译此元素属性中的指令
this.compileOrder(node)
}else if(node.nodeType === 3 && reg.test(node.textContent)){
// 如果是该节点是文本节点且匹配到 大括号 表达式
// 获取大括号内的表达式
var exp = RegExp.$1.trim()
// 调用数据绑定的方法 编译此文本节点 传入vm是为了读取vm._data
compileUtil.text(node,exp,this.$vm)
}
// 如果该元素存在子节点 则调用递归 编译此节点
if(node.childNodes && node.childNodes.length) {
this.compileElement(node)
}
})
},
compileOrder: function(node){
// 获取该节点所有属性节点
var nodeAttrs = node.attributes
// 遍历所有属性
Array.from(nodeAttrs).forEach(attr => {
// 获取属性名
var attrName = attr.name
// 判断属性是否是我们自定的指令
if(this.isDirective(attrName)){
// 获取指令对应的表达式
var exp = attr.value
// 获取指令 v-text => text (截去前两个字符)
var dir = attrName.substring(2)
// 判断指令类型 是否是事件指令
if(this.isEventDirective(dir)){
// 调用指令处理对象的相应方法 dir == on:click
compileUtil.eventHandler(node,dir,exp,this.$vm)
}else {
// 普通指令 v-text
compileUtil[dir] && compileUtil[dir](node,exp,this.$vm)
}
// 指令编译完成之后移除指令
node.removeAttribute(attrName)
}
})
},
isDirective: function(attrName){
// 只有 v- 开头的属性名才是我们定义的指令
return attrName.indexOf('v-') == 0
// attrName.startsWith("v-")
},
isEventDirective: function(dir){
// 事件指令以 on 开头
return dir.indexOf('on') == 0
}
}
// 指令处理集合
// 凡事涉及数据绑定的指令统一调用bind方法
var compileUtil = {
text: function(node,exp,vm){
this.bind(node,exp,vm,'text')
},
html: function(node,exp,vm){
this.bind(node,exp,vm,'html')
},
model: function(node,exp,vm){
this.bind(node,exp,vm,'model')
var bindAttr = 'value'
var eventName = 'input'
// 只针对输入框进行处理
if(node.nodeName.toLowerCase() == 'input'){
// 如果是单选框和复选框,则绑定的属性为checked,事件为change
if(node.type == 'radio' || node.type == 'checkbox'){
bindAttr = 'checked'
// oninput 事件在元素值发生变化是立即触发, onchange 在元素失去焦点时触发
eventName = 'change'
}
//保存一个val值,避免input事件触发重复读取
var val = this._getValue(exp,vm)
node.addEventListener(eventName,function(e){
if(node.type === 'text'){
// 获取输入框的值
var newVal = e.target[bindAttr]
// 对比输入框与绑定数据的值
if(newVal !== val){
// 绑定的值发生改变,修改vm._data对应的值
compileUtil._setValue(exp,newVal,vm)
// 更新val
val = newVal
}
}else if(node.type === 'radio'){
// 获取当前单选框的选中状态
var checked = e.target[bindAttr]
// 如果当前单选框被选中,则修改vm._data对应的值
if(checked){
compileUtil._setValue(exp,e.target.value,vm)
}
}
},false)
}
},
bind(node,exp,vm,dir){
// 根据指令获取更新节点的方法
var updaterFn = updater[dir + 'Updater']
// 获取exp表达式的值并调用更新节点的方法
updaterFn && updaterFn(node,this._getValue(exp,vm))
new Watcher(vm,exp,function(value){
updaterFn && updaterFn(node,value)
})
},
eventHandler: function(node,dir,exp,vm){
// 为节点绑定事件 (哪个节点,哪个事件,触发哪个回调)
// 获取事件名称 on:click => click
var eventName = dir.split(':')[1]
// 根据exp获取其在在vm中对应的函数
var fn = vm.$options.methods && vm.$options.methods[exp]
// 只有事件名称和回调同时存在才添加事件监听
if(eventName && fn){
// 回调函数强制绑定this为vm
node.addEventListener(eventName,fn.bind(vm),false)
}
},
_getValue(exp,vm){
var val = vm._data
// 例如 a.b 先获取到a的值,再根据a的值获取到a.b的值
var expArr = exp.split('.')
expArr.forEach(key => {
val = val[key]
})
return val
},
_setValue(exp,newVal,vm){
var val = vm._data
var expArr = exp.split('.')
expArr.forEach((key,index) => {
// 如果不是最后一个key,则获取值
if(index < expArr.length - 1){
val = val[key]
}else {
// 如果是最后一个key,则为该key赋予新的值
val[key] = newVal
}
})
}
}
// 更新元素节点的方法
var updater = {
textUpdater: function(node,value){
node.textContent = typeof value == 'undefined' ? '' : value
},
htmlUpdater: function(node,value){
node.innerHTML = typeof value == 'undefined' ? '' : value
},
modelUpdater: function(node,value){
var bindAttr = 'value'
// 根据节点类型绑定不同的属性
if(node.nodeName.toLowerCase() == 'input'){
if(node.type === 'text'){
// text输入框则更新value属性
node[bindAttr] = typeof value == 'undefined' ? '' : value
}else if(node.type == 'radio'){
// 单选框的value属性值与绑定的value一致时则为选中状态
bindAttr = 'checked'
if(node.value === value){
node[bindAttr] = true
}else {
node[bindAttr] = false
}
}
}
}
}
4.watcher 每个watcher里面配置了与属性值绑定相关的节点,更新函数等信息
- 一个有数据绑定的节点对应一个watcher,为watcher配置更新该节点用到的函数
- 更新节点的新数据需要根据表达式来获取vm._data中对应的数据,获取数据会触发属性的get方法,从而找到其对应的dep,将当前watcher添加到其数组中
- watcher对应的dep有可能会发生改变(当前绑定的属性指向新的对象,换句话说,就是属性值是新的对象),所以每次数据修改时都要尝试将watcher添加到对应的dep中
// 一个数据绑定的表达式对应一个Watcher
// Watcher记录了当前表达式对应的更新函数,还有表达式本身,为了后面获取表达式对应的值,还需要传入vm
function Watcher(vm,exp,cb){
this.vm = vm
this.exp = exp
this.cb = cb
// depIds这个对象用来记录当前watcher已经添加过的dep,防止重复添加
this.depIds = {}
// 初次编译此节点时为dep.subs添加watcher
this.value = this.get()
}
Watcher.prototype = {
get(){
// 给dep指定当前Watcher
Dep.target = this
// 获取表达式对应的值,并触发get方法
var value = this.getVMval()
Dep.target = null
return value
},
addToDep(dep){
// 将当前Wacther添加到dep数组中
dep.subs.push(this)
},
update(){
// 数据发生改变时,获取当前表达式对应的值
// 同时将当前Watcher添加到dep.subs中(dep可能是后面添加的,所以每次更新数据都需要尝试再添加一次)
var value = this.get()
// 调用回调函数更新界面
this.cb.call(this.vm, value)
},
getVMval(){
var val = this.vm._data
// 例如 a.b 先获取到a的值,再根据a的值获取到a.b的值
var expArr = this.exp.split('.')
expArr.forEach(key => {
val = val[key]
})
return val
}
}
网友评论