Last active
November 4, 2016 06:44
-
-
Save iahu/90d405fd24d331bbc018fa8ddb86ba81 to your computer and use it in GitHub Desktop.
Vue 数据绑定原理 简单实现
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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