寫在前面
前段時間面試,MVVM原理成為了一道必考題。由於理解不夠深,最近詳細瞭解以結構圖流程分析原理。
一原型圖解
使用MVVM雙向繫結
- 定義雙向繫結,傳入元素,和資料。
var vm = new MVVM({
el: '#mvvm-app',
data: {
someStr: 'hello ',
className: 'btn',
htmlStr: '<span style="color: #f00;">red</span>',
child: {
someStr: 'World !'
}
}
});
複製程式碼
MVVM類
- 新建劫持資料
- 編譯繫結資料
class MVVM {
constructor(options) {
this.$options = options || {};
var data = this._data = this.$options.data;
Object.keys(data).forEach(key => {
this._proxyData(key);
})
//資料劫持
observe(data, this);
//編譯
this.$compile = new Compile(options.el || document.body, this);
}
_proxyData(key, setter, getter) {
Object.defineProperty(this, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return this._data[key];
},
set: function proxySetter(newVal) {
this._data[key] = newVal;
}
})
}
}
複製程式碼
Compile類
- 將真實DOM移動到虛擬DOM中
- 解析元素中的指令
- 指令新建訂閱傳入更新函式
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
//生成文件碎片
this.$fragment = this.node2Fragment(this.$el);
//編譯
this.init()
//文件碎片加回容器中
this.$el.appendChild(this.$fragment);
}
}
node2Fragment(el) {
var fragment = document.createDocumentFragment(),
child;
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(this.isElementNode(node)){
//指令解析
this.compile(node);
}else if(this.isTextNode(node) && reg.test(text)){
this.compileText(node, RegExp.$1)
}
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(this.isDirective(attrName)){
var exp = attr.value;
var dir = attrName.substring(2);
//事件指令
if(this.isEventDirective(dir)){
compileUtil.eventHandler(node, this.$vm, exp, dir);
}else{
compileUtil[dir] && compileUtil[dir](node, this.$vm, exp);
}
node.removeAttribute(attrName);
}
})
}
isDirective(attr){
return attr.indexOf('v-') === 0;
}
isEventDirective(attr){
return attr.indexOf('on') === 0;
}
isElementNode(node) {
return node.nodeType == 1;
}
isTextNode(node) {
return node.nodeType == 3;
}
compileText(node, exp) {
compileUtil.text(node, this.$vm, exp);
}
}
//指令處理集合
var compileUtil = {
...
}
var updater = {
...
}
複製程式碼
Observer類
- 劫持資料
- 資料變化通知Watcher
//資料劫持
class Observer {
constructor(data) {
this.data = data;
this.walk(data);
}
walk(data) {
Object.keys(data).forEach(key => {
this.convert(key, data[key]);
})
}
convert(key, val) {
this.defineReactive(this.data, key, val);
}
//繫結資料,新增發布訂閱,核心**
defineReactive(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, //可列舉
configurable: false, //不能再define
get: function(){
if(Dep.target){
console.log(Dep.target, 'Dep.target');
dep.depend();
}
return val;
},
set: function(newVal){
if(newVal === val){
return;
}
val = newVal;
// 新的值object的話,進行監聽
childObj = observe(newVal);
console.log(newVal);
//通知訂閱者
dep.notify();
}
})
}
}
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
複製程式碼
Dep類
- 釋出訂閱類
var uid = 0;
class Dep {
constructor() {
this.id == uid++;
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
Dep.target.addDep(this)
}
removeSub(sub) {
this.subs.remove(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
Dep.target = null;
複製程式碼
Watcher類
- 監控資料變化,釋出訊息,執行訂閱函式。
class Watcher{
constructor(vm, expOrFn, cb){
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if(typeof expOrFn === 'function') {
this.getter = expOrFn;
}else{
this.getter = this.parseGetter(expOrFn);
}
this.value = this.get();
}
update(){
this.run();
}
run(){
var value = this.get();
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
get(){
Dep.target = this;
var value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
parseGetter(exp){
if(/[^\w.$]/.test(exp)) return;
var exps = exp.split(',');
return function(obj) {
for (let i = 0; i < exps.length; i++) {
if(!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
addDep(dep){
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
}
複製程式碼