尤大大在開發者大會上說新版的Vue會採用Proxy做爲數據驅動,來代替本來的 defineProperty。今天聊一聊使用Proxy的好處和Vue是怎樣實現數據驅動的。 最後會帶着你們手動實現一個Proxy版的MVVMhtml
目前官網使用的版本是Vue 2.5.Xnode
這個版本的底層數據驅動是經過Object.defineProperty 方法來實現了getter/setter。那這個方法是作什麼的呢? 咱們都知道js對象 都會具有get/set方法,這個方法就是用來定義對象裏的get/set方法(雖然也能夠定義其餘屬性 好比是否遍歷 是否容許修改等)git
Object.defineProperty(obj, prop, descriptor)
其中obj表明定義屬性的對象;prop表明定義的屬性名稱;descriptor表明定義的內容。舉個🌰你們就明白啦
const obj = {
name: 'jack',
sex: 'male',
age: 20
}
for(let key in obj) {
let val = obj[key]
Object.defineProperty(obj, key, {
get() {
return val // 觸發get函數就返回對象裏的值
// others...
},
set(newVal) {
val = newVal // 觸發set函數就修改對象
// others...
}
})
}
console.log(obj.name) // getter
obj.name = 'rose' // setter
上述代碼就利用get/set完成一個簡單的訪問着,這個🌰也是MVVM中的核心內容
複製代碼
爲何要使用Proxy代替defineProperty?github
let obj = new Proxy(target, handler)
// 其中target是要用Proxy包裝的對象;handler是包裝時執行的操做。仍是舉個🌰
let obj = {
name: 'jack',
sex: 'male',
age: 20
}
let newObj = new Proxy(obj, {
get: (target, property, receiver) => {
// get方法的參數,target:目標對象;property:獲取的屬性名;receiver:當前的Proxy
好比我訪問了 obj.name 這裏的target = obj;property = name
return obj[property]
// others...
},
set:(target, property, value, receiver) => {
// 參數同上,不一樣的是 value是新值
obj[property] = value
/// others...
}
})
複製代碼
首先須要建立Vue類,並解構一些所需對象
class Vue {
constructor(options) {
const { data } = options
this.$data = data
initObserve.call(this, this.$data)
}
function initObserve(data) {
// 初始化data對象,將data裏的對象轉化爲可觀察的對象
this.$data = observe(data);
}
function observe(data) {
// 判斷是否爲對象類型,不然則返回原對象
if(typeof data !== 'object') return data;
return new Observe(data);
}
class Observe {
//實現觀察者,遞歸監聽data裏的數據
constructor(data) {
// 使用 for in 作深層次遞歸,保證data裏嵌套格式也能正確爲轉化
for(let key in data) {
data[key] = observe(data[key]);
}
return this.proxy(data);
}
proxy(data) {
// 這裏就是觀察者的核心部分,對接收的data附加getter/setter
return new Proxy(data, {
get(target, property, receiver) {
return Reflect.get(target, property);
},
set(target, property, value, receiver) {
const result = Reflect.set(target, property, observe(value));
return result;
}
})
}
}
}
const mvvm = new Vue({
data: {}
})
複製代碼
咱們的第一步已經完成了,總結一句話:遍歷$data並使用Proxy函數對data進行加工處理// 爲了保持代碼整潔,每次只展現所須要的代碼,其餘代碼並非不須要了
class Vue {
constructor(options) {
const { data } = options
this.$data = data
let vm = initVm.call(this); ++
// others code
return this.$vm ++
}
function initVm() { ++
/*
這一步主要是代理數據
至關於將this.$data上的數據代理到this上 訪問this.name === 訪問 this.$data.name
*/
this.$vm = new Proxy(this, {
get: (target, property, receiver) => {
return this.$data[property]
},
set:(target, property, value, receiver) => {
return Reflect.set(this.$data, property, value);
}
});
return this.$vm;
}
}
const mvvm = new Vue({
data: {}
})
複製代碼
class Vue {
constructor(options) {
// 首先獲取指定渲染的dom根節點
let { data, el } = options; ++
this.$el = document.querySelector(el); ++
new Compile(el, vm) ++
// others code
}
class Compile { ++
/*
編譯數據
將data的數據渲染到頁面上
*/
constructor(el, vm) {
this.vm = vm;
let fragment = document.createDocumentFragment();
fragment.append(document.querySelector(el));
this.replace(fragment);
document.body.appendChild(fragment);
}
replace(arr) {
Array.from(arr.childNodes).forEach(node => {
const reg = /\{\{(.*?)\}\}/g;
let txt = node.textContent;
// nodeType === 3表示該節點爲文本節點
if(node.nodeType === 3 && reg.test(node.textContent)) {
let vm = this.vm;
updateTxt();
function updateTxt() {
// 去除首尾空格,把符合條件的目標替換爲data裏的對象
// 🌰:{{ name }} => {{name}} => $data.name => jack
// 使用reduce函數是爲了防止:$data.user.sex 這種嵌套狀況
const val = txt.replace(reg, (matched, arrs) => {
return arrs.split('.').map(el => el.trim()).reduce((obj, key) => {
return obj[key] === undefined? node.textContent : obj[key]; // 例如:去vm.makeUp.one對象拿到值
}, vm);
});
if(val != node.textContent) {
node.textContent = val;
}
}
}
// 遞歸遍歷dom節點
if(node.childNodes && node.childNodes.length > 0) {
this.replace(node);
}
});
}
}
}
複製代碼
// 還記得第一步實現的初始化data嗎?
// 首先咱們來想一個問題:視圖怎麼才能知道我什麼時候要更新呢?
// 每當用戶修改了$data時就應該更新視圖,否則的話視圖和$data就會不一致了。其實咱們已經知道如何解決了,咱們只要在$data所觸發的 getter/setter裏註冊函數就行了,在相應時刻調用函數就能更新視圖了
function initObserve(data) {
this.$data = observe(data);
}
function observe(data) {
if(typeof data !== 'object') return data;
return new Observe(data);
}
class Observe {
constructor(data) {
this.dep = new Dep(); ++
for(let key in data) {
data[key] = observe(data[key]);
}
return this.proxy(data);
}
proxy(data) {
let dep = this.dep; ++
return new Proxy(data, {
get(target, property, receiver) {
// 在getter裏註冊監聽方法
if(Dep.target) { ++
if(!dep.subs.includes(Dep.exp)) {
dep.addSub(Dep.exp);
dep.addSub(Dep.target);
}
}
return Reflect.get(target, property);
},
set(target, property, value, receiver) {
// 觸發setter時一併觸發修改方法
const result = Reflect.set(target, property, observe(value));
dep.notify(); ++
return result;
}
});
}
}
class Compile {
// 找到編譯函數,在替換數據這個函數里加一行 Watcher的監聽
// 這樣咱們就在全部須要編譯的地方實例了觀察函數,Watcher還接收了編譯的值
replace() {
function updateTxt() {
new Watcher(vm, arrs, updateTxt);
}
}
}
class Dep {
/*
發佈訂閱
監聽setter 當觸發setter會調用註冊過的函數 依次調用函數
*/
constructor() {
// 須要更新數據放在這個數組裏
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
// 當setter時遍歷數組執行全部函數
this.subs.filter(fn => typeof fn !== 'string').forEach(sub => sub.update());
}
}
class Watcher {
// 修改dom最核心的函數,接收了當前Vue實例、更新的字段和更新函數
constructor(vm, exp, fn) {
// 每一次的Watcher都能對應一次Dep
// 因此說在getter函數裏的Dep都
this.fn = fn;
this.vm = vm;
this.exp = exp;
Dep.exp = exp;
Dep.target = this;
const arr = exp.split('.').map(el => el.trim());
let val = vm;
arr.forEach(key => {
val = val[key] || val;
});
Dep.target = null;
}
update() {
const arr = this.exp.split('.').map(el => el.trim());
let val = this.vm;
arr.forEach(key => {
val = val[key];
});
this.fn(val);
}
}
複製代碼
<input value="val" onChange="(e) => val = e.target.value" />
複製代碼
知道v-model的原理咱們就很好實現了,首先找到 Compile
類,在replace中加一行代碼replace(arr) {
// 這行代碼首先判斷node是否是元素節點,由於只有元素上纔會有v-model
if(node.nodeType === 1) {
this.directives(node); +++
}
}
directives(node) { +++
// 遍歷元素上是否存在叫v-model的名字。若是找到,就將v-model擴展成 value+onChange的形式
const vm = this.vm;
Array.prototype.slice.call(node.attributes).forEach(el => {
if(el.name === 'v-model') {
node.value = vm[el.value];
node.addEventListener('input', e => {
vm[el.value] = e.target.value;
});
}
});
}
複製代碼
@
// 就像上面同樣,在v-model的下面增長相應代碼
if(el.name.includes('@')) { +++
const eventName = el.name.split('@')[1];
node.setAttribute(`v-bind:${ eventName }`, el.value);
node.addEventListener(eventName, vm.$methods[el.value].bind(vm));
}
複製代碼
class Vue {
function initComputed() { +++
let computed = this.$options.computed;
this.$computed = {};
if(!computed) return;
Object.keys(computed).forEach(key => {
this.$computed[key] = computed[key].call(this.$vm);
new Watcher(this.$vm, key, val => {
this.$computed[key] = computed[key].call(this.$vm);
});
});
}
}
複製代碼
function initVm() {
this.$vm = new Proxy(this, { +++
get: (target, property, receiver) => {
return this[property] || this.$data[property] || this.$computed[property] || this.$methods[property];
},
set:(target, property, value, receiver) => {
return Reflect.set(this.$data, property, value);
}
});
return this.$vm;
}
複製代碼
謝謝觀看!api