Skip to content

Instantly share code, notes, and snippets.

@iahu
Last active November 4, 2016 06:44
Show Gist options
  • Save iahu/90d405fd24d331bbc018fa8ddb86ba81 to your computer and use it in GitHub Desktop.
Save iahu/90d405fd24d331bbc018fa8ddb86ba81 to your computer and use it in GitHub Desktop.
Vue 数据绑定原理 简单实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<ul id="todo"></ul>
<body>
<script>
function Observer(vm) {
this.vm = vm;
var data = vm.$data;
if (!isObject(data)) {return;}
this.$data = data;
var keys = Object.keys(data);
keys.forEach(function(key) {
defineReactive(vm, data, key, data[key]);
});
}
function defineReactive(vm, obj, key, val) {
var dep = new Dep(vm);
if (typeof val === 'object') {
// 遍历子元素
travel(vm.$data);
// observer(vm);
}
var def = Object.getOwnPropertyDescriptor(obj, key);
var getter = def && def.get ?
def.get
: function () {
if (Dep.target) {
dep.addSub( Dep.target );
}
return val;
}
var setter = def && def.set ?
def.set
: function (newValue) {
var oldValue = val;
if ( !isObject(val) && oldValue === newValue ) {
return;
}
val = newValue;
dep.dispatch(newValue, oldValue);
}
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: getter,
set: setter
})
}
function travel(data) {
if ( Array.isArray(data) ) {
data.forEach(function(elem) {
new Vue({data: elem});
});
} else {
for (var prop in data) {
if (data.hasOwnProperty(prop)) {
new Vue({data: data[prop]});
}
}
}
}
Observer.prototype.$set = function (expr, val) {
var props = expr.split('.');
var data = this.$data;
var prop;
while (props.length > 1) {
prop = props.shift();
if (! data.hasOwnProperty(prop) ) {
defineReactive(this.vm, data, prop, {});
}
data = data[prop];
}
prop = props[0];
defineReactive(this.vm, data, prop, val);
if ( data.hasOwnProperty(props) ) {
data[prop] = val;
} else {
data[prop] = val;
}
}
function observer(vm) {
return new Observer(vm);
}
function Dep(vm) {
this.vm = vm;
this.subs = [];
}
Dep.prototype.addSub = function(fn) {
if (typeof fn === 'function') {
this.subs.push(fn);
}
};
Dep.prototype.dispatch = function (value, oldValue) {
var vm = this.vm;
this.subs.forEach(function(fn) {
fn.call(vm, value, oldValue);
});
}
function Watcher(vm, exp, cb) {
this.vm = vm;
this.getter = typeof exp === 'function' ? exp : parsePath(exp);
this.cb = cb;
this.value = this.get();
}
Watcher.prototype.get = function () {
Dep.target = this.cb;
var value = this.getter.call(this.vm.$data, this.vm.$data);
traverse(value);
Dep.target = null;
return value;
}
Watcher.prototype.update = function () {
var oldValue = this.value;
var value = this.get();
if ( oldValue !== value ) {
this.value = value;
this.cb.call(this.vm, value, oldValue);
}
this.getter();
}
function Set() {
this.set = {};
}
Set.prototype.add = function (id) {
this.set[id] = true;
}
Set.prototype.has = function (id) {
return !! this.set[id];
}
Set.prototype.clear = function () {
this.set = {};
}
function isObject(o) {
return Object.isExtensible(o);
}
const seenObjects = new Set()
function traverse (val) {
seenObjects.clear()
_traverse(val, seenObjects)
}
function _traverse (val, seen) {
var i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
function Vue(options) {
this.$options = options || {};
this.$data = options.data || {};
this.$observer = observer(this);
}
Vue.prototype.$watcher = function (expr, cb) {
this.cb = cb;
return new Watcher(this, expr, cb);
}
Vue.prototype.$set = function (expr, val) {
this.$observer.$set(expr, '');
this.$watcher(expr, this.cb);
this.$observer.$set(expr, val);
}
function parsePath(expr) {
var data = this.$data;
if ( /[^\w\.$]/.test(expr) ) {
return
}
var props = expr.split('.');
return function (data) {
var prop;
while (props.length) {
prop = props.shift();
if (! data.hasOwnProperty(prop) ) {
return;
}
data = data[prop];
}
return data;
}
}
// test case
// var data = {a: 1, b: {c: 1}, d: {e: {f: {g: 1}}}};
// var ob = new Vue({data: data});
// ob.$watcher('a', function (v, ov) {
// console.log('data.a changed from', v, 'to', ov);
// })
// data.a = 2; // use original data, 1 -> 2
// ob.$watcher('b.c', function (v, ov) {
// console.log('sub prop data.b.c changed from', ov, 'to', v);
// })
// data.b.c = 9; // use original data, 1 -> 9
// ob.$set('f', 0); // add new property after inited.
// ob.$watcher('f', function (v, ov) {
// console.log('data.f changed from', ov, 'to', v);
// });
// ob.$data.f = 9; // use $data api same as `data`, 0 -> 9
// ob.$set('f', 8); // use $set api, 9 -> 8
var todo = new Vue({
data: {
list: {}
}
});
var todo$ = document.getElementById('todo');
todo.$watcher('list', function (v, ov) {
todo$.innerHTML = reanderList(this.$data.list);
})
function reanderList(listData) {
var html = '';
var statusHash = ['未完成', '已完成'];
var prop;
for (prop in listData) {
if (listData.hasOwnProperty(prop)) {
html += `<li><span>${listData[prop].thing}</span><button>${statusHash[listData[prop].status]}</button></li>`
}
}
return html;
}
todo.$set('list.1', {thing: '下午4点开会', status: 0});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment