有了前面的基礎(chǔ),v-model其實(shí)很容易實(shí)現(xiàn)私蕾。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<p>{{name}}</p>
<input type="text" v-model="name" />
</div>
</body>
</html>
<script src="./myvue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'jack',
}
})
</script>
實(shí)現(xiàn)思路:
1僵缺、解析element的屬性節(jié)點(diǎn)(v-model="name")
if(dir.indexOf('model') === 0){
...
}
2、將input的value設(shè)置為vm.data里面的name值
if(dir.indexOf('model') === 0){
node.value = this.getVMVal(exp) //exp就是name
}
3踩叭、增加input事件監(jiān)聽磕潮,當(dāng)獲取了e.target.value新值時(shí),修改vm.data.name容贝。
if(dir.indexOf('model') === 0){
//雙向數(shù)據(jù)綁定
//1.將input的value設(shè)置為當(dāng)前data的name值
node.value = this.getVMVal(exp)
//2.綁定input事件
node.addEventListener('input',(e)=>{
var newval = e.target.value;
//修改data的name值
this.$vm._data[exp] = newval
},false)
}
4自脯、增加watcher監(jiān)聽。
這個(gè)是容易被忽視的地方斤富。
因?yàn)殡p向數(shù)據(jù)綁定是model和view雙向的膏潮,通過第三步addEventListener可實(shí)現(xiàn)view->model;而model->view的過程满力,我們還需要增加數(shù)據(jù)監(jiān)聽watcher焕参,只是初始化時(shí)設(shè)置input輸入框的value是不夠的。
new Watcher(this.$vm,exp,(newVal,oldVal)=>{
console.log('執(zhí)行回調(diào)函數(shù)',newVal,oldVal)
node.value = newVal
})
5油额、完整代碼叠纷。
function Vue(options){
this.$options = options
var data = this._data = this.$options.data
//數(shù)據(jù)代理
Object.keys(data).forEach(key=>{
this._proxy(key)
})
//數(shù)據(jù)劫持
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) //將節(jié)點(diǎn)轉(zhuǎn)到fragment處理
this.init() //開始處理
this.$el.appendChild(this.$fragment) //塞回原位
}
}
Compile.prototype = {
//將#app里面的節(jié)點(diǎn)都轉(zhuǎn)到文檔碎片中
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){
//如果是元素節(jié)點(diǎn)
this.compile(node)
} else if(node.nodeType === 3 && reg.test(text)) {
this.compileText(node,RegExp.$1) //更新視圖動(dòng)作
//訂閱更新視圖函數(shù)
/*
dep.register(RegExp.$1,()=>{ //注冊的是key是a.b
//如果data發(fā)生了變化,需要再次調(diào)用this.compileText這個(gè)函數(shù)來更新視圖
this.compileText(node,RegExp.$1)
})
*/
//引入watcher對象
new Watcher(this.$vm,RegExp.$1,(newVal,oldVal)=>{ //this.cb.call(this.vm,newVal,oldVal)//更新視圖操作
if(newVal !== oldVal) {
node.textContent = newVal //更新為最新值
}
})
}
if(node.childNodes && node.childNodes.length) {
//遞歸
this.compileElement(node)
}
})
},
//處理元素節(jié)點(diǎn)
compile(node){
var nodeAttrs = node.attributes; // 獲取元素節(jié)點(diǎn)的所有屬性潦嘶,偽數(shù)組
[].slice.call(nodeAttrs).forEach(attr=>{
var attrName = attr.name;//獲取屬性名
if(attrName.indexOf('v-') === 0){//判斷是否是指令
var exp = attr.value; //獲取屬性值涩嚣,也就是觸發(fā)的方法名
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];// 獲取函數(shù)
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 = '' //動(dòng)態(tài)class類
var classObj = eval('(' + exp + ')'); //解析為object對象
for(var key in classObj) { //遍歷對象,如果value為true追加class名,false不追加
if(classObj[key]) {
newClassName += ' ' + key
}
}
node.className = oldClassName + newClassName // 設(shè)置className
}
} else if(dir.indexOf('model') === 0){ //1.捕獲v-model的值
//雙向數(shù)據(jù)綁定
//2.將input的value設(shè)置為當(dāng)前data的name值
node.value = this.getVMVal(exp)
//3.綁定input事件
node.addEventListener('input',(e)=>{
var newval = e.target.value;
//修改data的name值
this.$vm._data[exp] = newval
},false)
//4.增加監(jiān)聽
new Watcher(this.$vm,exp,(newVal,oldVal)=>{
console.log('執(zhí)行回調(diào)函數(shù)',newVal,oldVal)
node.value = newVal
})
}
node.removeAttribute(attrName) // 從chrome控制臺(tái)的Elements看文檔結(jié)構(gòu)航厚,發(fā)現(xiàn)沒有v-這樣的屬性(指令)顷歌,所以需要處理完去掉
}
})
},
//用vm.data信息,替換大括號(hào)的name
compileText(node,exp){
node.textContent = this.getVMVal(exp)
},
//處理層級(jí)問題
getVMVal(exp){ // a.b.c
var val = this.$vm._data
if(exp.indexOf('.') !== -1) {
var arr = exp.split('.') //["a", "b", "c"]
arr.forEach(k=>{
//debugger
val = val[k] // 層級(jí)遞進(jìn)
})
return val
} else {
return val[exp]
}
}
}
//新增部分
function observe(data){
//對data進(jìn)行defineProperty處理幔睬,嵌套的數(shù)據(jù)要用遞歸
if(!data || typeof data!=='object'){//遞歸的退出條件
return;
}
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])//對data里面的每一個(gè)key進(jìn)行定義
})
}
function defineReactive(data,key,val){
var dep = new Dep()
observe(val)//先執(zhí)行遞歸眯漩,確保嵌套對象的key都可以被定義
Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get(){
console.log('get操作:',key,'===>',val)
/*
dep.register(key,()=>{
console.log('更新操作')
})
*/
if(Dep.target) { //如果Dep.target有了Watcher,就存入到dep的容器里面
dep.register(Dep.target)
}
return val
},
set(newVal){
console.log('set操作:',key,'===>',newVal)
val = newVal //修改為最新值
//觸發(fā)更新視圖函數(shù)
dep.emit(key) // 這里的key是b溪窒,而不是a.b
}
})
}
//引入Watcher
function Watcher(vm,exp,cb){
this.vm = vm; //vue對象
this.exp = exp; //正則匹配的大括號(hào)表達(dá)式
this.cb = cb; // 回調(diào)函數(shù)坤塞,也就是待執(zhí)行的更新視圖操作
this.value = this.getValue() //在初始化的時(shí)候調(diào)用,觸發(fā)defineProperty里面的get
}
Watcher.prototype.getValue = function(){
Dep.target = this //Dep.target是一個(gè)臨時(shí)變量
var newVal = this.getVMVal()
Dep.target = null
return newVal
}
Watcher.prototype.getVMVal = function(){
var val = this.vm._data
if(this.exp.indexOf('.') !== -1) {
var arr = this.exp.split('.') //["a", "b", "c"]
arr.forEach(k=>{
//debugger
val = val[k] // 層級(jí)遞進(jìn)
})
return val
} else {
return val[this.exp]
}
}
Watcher.prototype.update = function(){
//獲取新值
let newVal = this.getValue()//這里會(huì)觸發(fā)defineProperty的get
let oldVal = this.value
if(newVal !== oldVal){
this.value = newVal
this.cb.call(this.vm,newVal,oldVal)//更新視圖操作
}
}
//引入觀察訂閱模式
function Dep(){
this.subs = [] //容器澈蚌,
}
Dep.prototype.register = function (obj) {
if(Dep.target){
this.subs.push(obj) //丟入容器的是Watcher對象摹芙,在Watcher實(shí)例化的時(shí)候丟入
}
}
Dep.prototype.emit = function () {
this.subs.forEach(obj=>{ //將容器中的Watcher對象遍歷,順序執(zhí)行
obj.update()
})
}
最后做一下測試:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<p>{{name}}</p>
<input type="text" v-model="name" />
<button v-on:click="changeName">修改</button>
</div>
</body>
</html>
<script src="./myvue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'jack',
},
methods:{
changeName(){
this.name = 'tom'
}
}
})
</script>
大家可以屏蔽一下第四步試試宛瞄,當(dāng)點(diǎn)擊按鈕修改了name浮禾,input輸入框的value是無法更新的。