一.核心结构
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
的一对多关系
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
的关系,更新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