一.コア構造
Vue のデータバインディングメカニズム:
setter+ 汚染チェック + 发布订阅管理
0.x からこのようになっています。dep.js、watcher.js、observer.js:
Subject: dep.js
Observer: watcher.js(内置脏检查)
Setter: observer.js(set 时触发 Subject.notify)
setterを定義してデータ変化を監視しますが、非常に重要な問題があります:深いデータ構造に対して、一つ一つsetterを定義するのでしょうか?
確かにそうです:
是数组的话,挨个 observe 定义 setter,深度递归监听所有 Object 的 key
被摸过的数据身上都有__ob__
メモリ爆発の問題が存在するように感じられます。超大きなデータオブジェクトを渡した場合、getter/setterだけでかなりのスペースを占有しますが、実際のシナリオではページ全体が一大きなデータの塊であることは滅多にありません。重いデータのシナリオでは、一般にデータ層を分離し、専用の状態管理メカニズムでデータを分割するため、メモリ爆発は起こりにくくなります
最も重要な部分はこれらですが、実行可能にするには Compiler & Directive でコンパイル変換が必要です。構造は以下の通り:
解出关系
创建 View 监听变化
tpl —— Compiler —— Subject & Observer & Manager
|
Directive
tpl & dataを入力し、viewを出力し、data-viewの関係を構築します
二.フレームワーク
語るコードは以下の通り:
// 从模板解析出 data-view 的关系
var Compiler = function(tpl) {
// 模板编译,转 dom 操作
};
var Directive = function(directive) {
// 配合 compiler,处理复杂一些的 DOM 操作(repeat, on, bind),建立 data-view 的关联
};
// 监听数据变化,实现 data-view 的绑定关系
var Subject = function() {
// 主题
this.obs = [];
};
var Observer = function(updateFn) {
// 观察者
};
var Manager = function(data) {
// 定义 setter,管理 Subject 和 Observer
};
入力は以下の通り:
<!-- 模版 -->
<div id="demo" v-cloak>
<h1 v-bind:style="{ items.length ? 'border-bottom: 1px solid #ddd' : 'border: none' }">
{{title}}
</h1>
<p v-if="!items.length">empty</p>
<ul v-for="item in items">
<li v-on:click="item.a[1].a[1].a.a++" style="background-color: #2b80b6">
{{item.a[1].a[1].a.a}}
</li>
</ul>
</div>
// 数据
var data = {
title: 'list',
items: [{a: [0, {a: [1, {a: {a: 1}}]}]}]
};
var v = new V({
el: '#demo',
data: data,
created: function() {
console.log(data);
}
});
まだ長い道が必要そうで、先が見えませんが、少し詳細化します
Compiler
// 从模板解析出 data-view 的关系
var Compiler = function(tpl) {
// 模板编译,转 dom 操作
};
コンパイラはテンプレートを解析し、構造情報を出力すべきです。そこでNodeMetaを定義します:
Compiler.NodeMeta = function(tag) {
// Compiler 要输出的 DOM 结构 meta 格式
// {
// tag: 'ol',
// children: [NodeMeta, NodeMeta...],
// props: [{key: 'id', value: 'ol'}...],
// directives: [{name: 'v-if', value: '!items.length'}],
// // 文本节点情况比较复杂,这里不考虑非数据绑定形式的文本和没有被单独包起来的文本
// textContent: 'item.text',
// // for 指令会扩展子级作用域
// extraScope: {'item': Object}
// }
};
コアタスクはテンプレートの解析です:
Compiler.prototype.parse = function() {
// 提取标签名,创建 meta 树
this.nodeMeta = this.matchTag();
};
Compiler.prototype.matchTag = function() {
var rootMeta;
//...创建结构树
return rootMeta;
};
構造metaツリーを取得した後、View を作成します:
Compiler.prototype.render = function(vm) {
this.vm = vm;
var render = function(nodeMeta) {
//...解析指令
//...创建节点
//...设置 attr,绑定事件 handler,实现 view-data 的响应
};
var node = render(this.nodeMeta);
// 用创建好的节点替掉模版元素
vm.el.parentNode.replaceChild(node, vm.el);
};
Directive
ディレクティブはコンパイラを補助し、いくつかの複雑なものを処理する責任があります:
var Directive = function(directive) {
// 配合 compiler,处理复杂一些的 DOM 操作(repeat, on, bind),建立 data-view 的关联
};
コンパイラを補助するので、Viewの作成も担当します:
Directive.prototype.render = function(vm, node, nodeMeta, compilerRender) {
switch (dir) {
case 'for':
//...创建多组节点
break;
case 'if':
//...条件创建
break;
case 'on':
//...绑定事件 handler,建立 view-data 的关联
break;
case 'bind':
//...简单的表达式求值
break;
case 'cloak':
//...不用管
break;
default:
console.error('unknown directive: ' + key);
}
};
各種ディレクティブはデフォルトでdata-viewの関係を構築する必要があります。イベントは特殊で、handlerがdataを変更する可能性があるため、viewが変化し、dataも連動して変化する必要があります。そのため、イベントディレクティブはview-dataの関係の完了も担当します
コンパイラと比較して、ディレクティブの複雑な部分は式の評価とhandlerの作成が必要なことです:
Directive.getParams = function(vm, extraScope) {
};
Directive.createFn = function(vm, fnStr, extraScope) {
var handler = function() {
var args = [].slice.call(arguments);
var param = Directive.getParams(vm, extraScope);
var fnBody;
//...填充函数定义的各部分
var fn = eval(fnBody);
return fn.apply(vm, args.concat(param[1]));
};
return handler;
};
Subject & Observer
发布订阅模式(观察者模式)中的主题(报纸):
// 监听数据变化,实现 data-view 的绑定关系
var Subject = function() {
// 主题
this.obs = [];
};
いくつかの基本インターフェースを実装するだけです:
Subject.prototype.add = function(ob) {
this.obs.push(ob);
};
Subject.prototype.remove = function(ob) {
var index = this.obs.indexOf(ob);
if (index !== -1) this.obs.splice(index, 1);
};
Subject.prototype.notify = function(lastValue, newValue) {
this.obs.length && this.obs.forEach(function(ob) {
ob.update();
});
};
パラメータobは观察者(新聞を購読する人)Observerインスタンスで、以下のように定義されます:
var Observer = function(updateFn) {
// 观察者
if (typeof updateFn === 'function') {
this.update = updateFn;
}
};
Observer.prototype.update = function() {};
新聞が情報を发布する時、すべての購読者に通知します。ここで观察者模式はdata-viewの 1 対多の関係を維持するために使用されます
Manager
Managerは汎用的な观察者模式と実際のシナリオを接続するもので、ここでは主にsetterの定義を担当します:
var Manager = function(data) {
this.data = data;
this.dep = new Subject();
// 定义 setter,管理 Subject 和 Observer
this.observe(data);
};
Manager.prototype.observe = function(obj) {
};
Manager.prototype.observeArray = function(arr) {
};
再帰的にsetterを定義し、データ変化hookを埋め込みます
三.具体的な実装
おおよそ 3 つの部分に分かれます:
-
データ変化の監視
-
テンプレートの解析、
viewとdataの関係を找出す -
View を作成し、
data-view、view-dataの関係を構築する -
エントリー
データ変化の監視は非常に簡単で、すぐに完了します。テンプレートの解析は重要ですが決定的な部分ではなく、複雑さは普通です。View を作成し、データバインディングを構築するのが最も決定的な部分で、最も複雑です。もちろん、最後にエントリーポイントを開く必要があります
データ変化の監視
Subject & Observerの部分は上記にあり、もう動かす必要はありません。汎用的なものは簡単に完了します
では主にManagerのsetter定義部分です:
var Manager = function(data) {
this.data = data;
this.dep = new Subject();
// 定义 setter,管理 Subject 和 Observer
this.observe(data);
};
Subjectインスタンスを保持し、後続でObserverを追加します。言うことはありません。深度setterの定義は実装も比較的簡単です:
Manager.prototype.observe = function(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
// 先监听孩子的
if (typeof obj[key] === 'object') {
if (Array.isArray(obj[key])) {
self.observeArray(obj[key]);
}
else {
self.observe(obj[key]);
}
}
}
// 定义 setter
void function() {
// 隔离一个 value
var value = obj[key];
Object.defineProperty(obj, key, {
set: function(newValue) {
if (typeof newValue === 'object' || newValue !== value) {
console.log('data 变了', value, newValue);
value = newValue;
// setter 通知变化
obj.__ob__.dep.notify(value, newValue);
}
return newValue;
}
});
}();
}
};
配列の場合、遍历してobserveします:
Manager.prototype.observeArray = function(arr) {
arr.forEach(function(data) {
if (typeof data === 'object') {
if (Array.isArray(data)) {
self.observeArray(data);
}
else {
self.observe(data);
}
}
});
};
P.S.ここでの実装はVueとあまり同じではありません。簡単にするためです
テンプレートの解析
テンプレートを読み、dataとviewの関係を找出します
n個の正規表現で必要な各部分を抽出します:
var Compiler = function(tpl) {
// 模板编译,转 dom 操作
this.tpl = tpl.trim();
this.REGEX = {
tag: /<([^>/\s]+)[^<>]*>/gm,
attrTag: /<([^>/\s]+)\s+([^>]+)>/gm,
text: /<([^>\s]+).*>\s*{{([^}]*)}}\s*<\/\1>/gm,
attr: /(?:([^="\s]+)(?:="([^"]+)")?)/gm
};
};
P.S.正直に言うと、ここはまだかなり大変です(師父は 1 年必要だと言いました...哈哈)
ここでは簡単なテンプレート解析のみを実装し、怠けて裸のテキストノードをサポートしません。ソースコードは比較的長く、ここでは重要な部分のみを示します:
Compiler.prototype.matchTag = function() {
var openTagStack = [], peak;
while ((tmp = this.REGEX.tag.exec(str)) !== null) {
newMeta = new NodeMeta(tag);
if (lastEndIndex > 0) {
// 构造 nodeMeta 树
peak = openTagStack[openTagStack.length - 1];
closeTagRegex = new RegExp('</' + peak + '>', 'm');
skipMatch = str.substring(lastEndIndex, tmp.index);
newMeta.parent = meta;
// 默认进入下一层
if (meta.children.length > 0){
meta = meta.children[meta.children.length - 1];
}
// 匹配一个就出一层
if (closeTagRegex.test(skipMatch)) {
openTagStack.pop();
meta = meta.parent;
}
meta.children.push(newMeta);
}
openTagStack.push(tag);
// 填充 props & directives
attrs = this.matchAttr(thisMatch);
// 填充 textContent
}
return rootMeta;
};
ここで構造metaツリーが得られました。次にこの設定データを持って View を作成します:
View を作成し、データバインディングを構築
ソースコードが長すぎるため、簡単なプロセスは以下の通り:
Compiler.prototype.render = function(vm) {
var render = function(nodeMeta) {
// tag
var node = document.createElement(nodeMeta.tag);
// props
node.setAttribute(prop.key, prop.value);
// textContent
var fn = function() {
var exp = Directive.createFn(vm, nodeMeta.textContent, nodeMeta.extraScope);
var text = exp();
node.innerText = text || "";
}
//!!! 实现 data-view 的绑定
vm.data.__ob__.dep.add(new Observer(fn));
// directives
var directive = nodeMeta.directives[i];
var d = new Directive(directive);
// 指令 render 返回 false 表示不需要渲染 node 及 children
// 返回 true 表示已经把 children 渲染好了
var renderOrNot = d.render(vm, node, nodeMeta, render);
// children
var childNode = render(meta);
childNode && node.appendChild(childNode);
return node;
};
// 创建 View,替掉模版元素
var node = render(this.nodeMeta);
vm.el.parentNode.replaceChild(node, vm.el);
};
初回レンダリング時にdataにobserverを追加し、後続のデータ変化を取得できます。これでdata-viewのバインディングが実装されました
重要な補助的役割を果たす Directive は以下の通りで、長すぎるため、ここではonディレクティブを例にします:
Directive.prototype.render = function(vm, node, nodeMeta, compilerRender) {
var dir = this.REGEX.directive.exec(key)[1];
var event, handler, exp, prop, propValue;
switch (dir) {
case 'on':
event = this.REGEX.key.exec(key);
if (event) {
event = event[1];
handler = Directive.createFn(vm, value, nodeMeta.extraScope);
node.addEventListener(event, function() {
handler();
});
}
break;
}
};
handlerを作成し、addEventListenerします。setterを定義してデータ変化監視を実装する情况下、view-dataのバインディングは天然的で、追加の処理は必要ありません。handler実行時にdataを変更すると、setterがトリガーされ、进而notifyが View 作成時に構築したdata-viewの関係を更新するからです
P.S.handlerの作成部分はかなり劣っており、new Function()定義を拼接し、さらにevalで取り出します。性能爆発ですが、このステップはコンパイル段階で行えるので、問題ありません
エントリー
ここでほぼ完了しました。エントリーポイントを開放し、プロセスを繋ぎます:
// 入口
var V = function(config) {
// 基本配置数据(view, data)
this.el = el;
this.data = config.data;
this.methods = config.methods;
// 生命周期 hook
var LIFE_CYCLES = ['created'];
this._init();
};
V.prototype._init = function() {
// 监听数据变化
this._observe();
console.log(this.data);
// 解析关系,转 DOM 操作
this.compiler = this._render();
};
V.prototype._render = function() {
var c = new Compiler(this.el.outerHTML);
c.parse();
c.render(this);
return c;
};
V.prototype._observe = function() {
this.__ob__ = new Manager(this.data);
};
四.オンライン Demo
Demo アドレス:http://ayqy.net/temp/data-binding/vue/index.html
P.S.ソースコードはすべてソースコード内にあり、注釈が非常に明確です
最後に
生活には常に灰色の部分があり、光が見えず、方向も見つかりません。しかし、幸いにも道は足元にあり、初心も将来も関係ありません。路上にいればよいのです。keep up
コメントはまだありません