理解Vue的設(shè)計(jì)思想
MVVM框架的三要素:數(shù)據(jù)響應(yīng)式俐东、模板引擎及其渲染
數(shù)據(jù)響應(yīng)式:監(jiān)聽數(shù)據(jù)變化并在視圖中更新
Object.defineProperty()
Proxy
模版引擎:提供描述視圖的模版語法
插值:{{}}
指令:v-bind锌雀,v-on县爬,v-model寂纪,v-for,v-if
渲染:如何將模板轉(zhuǎn)換為html
模板 => vdom => dom
數(shù)據(jù)響應(yīng)式原理
數(shù)據(jù)變更能夠響應(yīng)在視圖中兄朋,就是數(shù)據(jù)響應(yīng)式徐块。vue2中利用 Object.defineProperty() 實(shí)現(xiàn)變更檢測。
簡單實(shí)現(xiàn)
const obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${key}:${newVal}`);
val = newVal
} }
}) }
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'foooooooooooo'
結(jié)合視圖
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div id="app"></div>
<script>
const obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
} }
}) }
defineReactive(obj, 'foo', '')
obj.foo = new Date().toLocaleTimeString()
function update() {
app.innerText = obj.foo
}
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
</body>
</html>
遍歷需要響應(yīng)化的對象
// 對象響應(yīng)化:遍歷每個(gè)key仔涩,定義getter忍坷、setter function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
const obj = {foo:'foo',bar:'bar',baz:{a:1}}
observe(obj)
obj.foo
obj.foo = 'foooooooooooo' obj.bar
obj.bar = 'barrrrrrrrrrr' obj.baz.a = 10 // 嵌套對象no ok
解決嵌套對象問題
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
//...
解決賦的值是對象的情況
obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是對象的情況 notifyUpdate()
如果添加/刪除了新屬性無法檢測
obj.dong = 'dong' obj.dong // 并沒有g(shù)et信息
function set(obj, key, val) {
defineReactive(obj, key, val)
}
Vue中的數(shù)據(jù)響應(yīng)化
目標(biāo)代碼
vue.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{counter}}</p>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data: {
counter: 1 },
})
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
</html>
原理分析
- newVue()首先執(zhí)行初始化,對data執(zhí)行響應(yīng)化處理熔脂,這個(gè)過程發(fā)生在Observer中
- 同時(shí)對模板執(zhí)行編譯佩研,找到其中動態(tài)綁定的數(shù)據(jù),從data中獲取并初始化視圖霞揉,這個(gè)過程發(fā)生在 Compile中
- 同時(shí)定義一個(gè)更新函數(shù)和Watcher旬薯,將來對應(yīng)數(shù)據(jù)變化時(shí)Watcher會調(diào)用更新函數(shù)
- 由于data的某個(gè)key在一個(gè)視圖中可能出現(xiàn)多次,所以每個(gè)key都需要一個(gè)管家Dep來管理多個(gè)
Watcher將來data中數(shù)據(jù)一旦發(fā)生變化适秩,會首先找到對應(yīng)的Dep绊序,通知所有Watcher執(zhí)行更新函數(shù)
image.png
涉及類型介紹
KVue:框架構(gòu)造函數(shù)
Observer:執(zhí)行數(shù)據(jù)響應(yīng)化(分辨數(shù)據(jù)是對象還是數(shù)組)
Compile:編譯模板,初始化視圖秽荞,收集依賴(更新函數(shù)骤公、watcher創(chuàng)建)
Watcher:執(zhí)行更新函數(shù)(更新dom)
Dep:管理多個(gè)Watcher,批量更新
KVue
框架構(gòu)造函數(shù):執(zhí)行初始化 執(zhí)行初始化扬跋,對data執(zhí)行響應(yīng)化處理阶捆,vue.js
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
new Observer(obj)
}
function defineReactive(obj, key, val) {}
class KVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
observe(this.$data)
}
}
class Observer {
constructor(value) {
this.value = value
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
} }
為$$data做代理,代理$method
class KVue {
constructor(options) {
// 。钦听。洒试。
proxy(this, '$data')
}
}
function proxy(vm) {
Object.keys(vm.$methods).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$methods[key]
},
})
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(newVal) {
vm.$data[key] = newVal;
} });
}) }
編譯 - Compile
編譯模板中vue模板特殊語法,初始化視圖朴上、更新視圖
初始化視圖
根據(jù)節(jié)點(diǎn)類型編譯垒棋,compile.js
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
this.compile(this.$el);
} }
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElement(node)) { console.log("編譯元素" + node.nodeName);
} else if (this.isInterpolation(node)) { console.log("編譯插值文本" + node.textContent);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
}); }
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
} }
編譯插值,compile.js
compile(el) {
// ...
} else if (this.isInerpolation(node)) {
// console.log("編譯插值文本" + node.textContent); this.compileText(node);
} });
}
compileText(node) {
console.log(RegExp.$1);
node.textContent = this.$vm[RegExp.$1];
}
編譯元素
compile(el) {
//...
if (this.isElement(node)) {
// console.log("編譯元素" + node.nodeName); this.compileElement(node)
} }
compileElement(node) {
let nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
let attrName = attr.name;
let exp = attr.value;
if (this.isDirective(attrName)) {
let dir = attrName.substring(2);
this[dir] && this[dir](node, exp);
}
}); }
isDirective(attr) {
return attr.indexOf("k-") == 0;
}
text(node, exp) {
node.textContent = this.$vm[exp];
}
k-html
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
依賴收集
視圖中會用到data中某key痪宰,這稱為依賴叼架。同一個(gè)key可能出現(xiàn)多次畔裕,每次都需要收集出來用一個(gè)
Watcher來維護(hù)它們,此過程稱為依賴收集碉碉。 多個(gè)Watcher需要一個(gè)Dep來管理柴钻,需要更新時(shí)由Dep統(tǒng)一通知。 看下面案例垢粮,理出思路:
new Vue({
template:
`<div>
<p>{{name1}}</p>
<p>{{name2}}</p>
<p>{{name1}}</p>
<div>`,
data: {
name1: 'name1',
name2: 'name2'
}
});
實(shí)現(xiàn)思路
- defineReactive時(shí)為每一個(gè)key創(chuàng)建一個(gè)Dep實(shí)例
- 初始化視圖時(shí)讀取某個(gè)key贴届,例如name1,創(chuàng)建一個(gè)watcher1
- 由于觸發(fā)name1的getter方法蜡吧,便將watcher1添加到name1對應(yīng)的Dep中
- 當(dāng)name1更新毫蚓,setter觸發(fā)時(shí),便可通過對應(yīng)Dep通知其管理所有Watcher更新
創(chuàng)建Watcher昔善,kvue.js
const watchers = [];//臨時(shí)用于保存watcher測試用
// 監(jiān)聽器:負(fù)責(zé)更新視圖 class Watcher {
constructor(vm, key, updateFn) { // kvue實(shí)例
this.vm = vm;
// 依賴key
this.key = key;
// 更新函數(shù)
this.updateFn = updateFn;
// 臨時(shí)放入watchers數(shù)組
watchers.push(this)
}
// 更新 update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
編寫更新函數(shù)元潘、創(chuàng)建watcher
// 調(diào)用update函數(shù)執(zhí)插值文本賦值 compileText(node) {
// console.log(RegExp.$1);
// node.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
this.update(node, exp, 'text')
}
html(node, exp) {
this.update(node, exp, 'html')
}
update(node, exp, dir) {
const fn = this[dir+'Updater']
fn && fn(node, this.$vm[exp])
new Watcher(this.$vm, exp, function(val){
fn && fn(node, val)
})
}
textUpdater(node, val) {
node.textContent = val;
}
htmlUpdater(node, val) {
node.innerHTML = val
}
聲明Dep
class Dep {
constructor () {
this.deps = []
}
addDep (dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update());
} }
創(chuàng)建watcher時(shí)觸發(fā)getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
} }
依賴收集,創(chuàng)建Dep實(shí)例
defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);
return val },
set(newVal) {
if (newVal === val) return
dep.notify()
} })
}