学习使人进步。
怀着这份心情耐心的看了一下vue
的最初版本,虽然最开始也不太懂里面写的什么,到底是怎么处理的,但是通过大量的查阅资料和看视频解读,也是勉强的看的连贯。再知道vue
的构建过程后就有了自己仿写的想法,顺便也能为自己的代码打下基础和强化自己的编程能力以及编程思想。要说仿写vue
的代码,网上确实不少,有很多出色的代码,比如MVVM.js
,在GitHub
上也能搜到,本文中将实现的代码有很多也是参考了上面的代码和思想。
在看vue
的时候一定要注意知道一点,它的处理过程,初始化在做了那些东西,请看下图:
基本上算是做了三步走:
- 为
data
里面的所有属性值做数据代理,使其能this.xx
能访问。 - 为每一个
data
数据里面的属性值创建observe
数据劫持,并且为每一个数据添加一个Dep()
- 最后就是编译模板
compile
,大致就是编译解析{{}}
和v-指令
语法。
其中2, 3 两个关键步骤里面会涉及到很多巧妙的关联,在第二步的劫持数据中给每个data
下面的数据加上dep
(这里要注意,由于是先执行observe
然后再执行compile
,所有dep
是比watcher
先创建好的),然后在第三步中给每个表达式中添加一个对应的watcher
,两者的联系是:初始化的时候当有表达式 {{}} 或者 “v-指令” 时,就会调用observe
的get()
方法,将当前的绑定的所有watcher
都添加到dep
中;当改变data
里面的值后,调用了observe
的set()
方法,将通知dep
下面的所有watcher
, 然后循环调用watcher
下面的update()
去更新页面。
先说大的框架,我们 new Vue()
时就像自己写框架一样,
function Vue() {}
var app = new Vue();
这样就算完成了vue的初始化,是不是很简答,虽然有初始化,但我们还没有传入参数,接下来我们要加参数。
function Vue(option) {
// 获取绑定的区间 dom
this.$el = (typeof options.el === 'string') ? document.querySelector(options.el) : (options.el || document.body);
// 获取参数
this.$options = options;
var data = this.$data = options.data;
// 数据代理(proxy data)使 data 、 method 、 computed 中的属性在此MVVM对象上可访问
this.proxyData(options);
// 监视,劫持数据(observe data)使新添加的数据都可以在此MVVM对象上可访问
var observe = new Observer(data);
// 编译模版、指令(compile template,direcitves)
var compile = new Compiler(this.$el, this);
}
var app = new Vue({
el: '#app',
data: {},
methods: {},
computed: {}
});
1. 数据代理(proxy data)使 data 、 method 、 computed 中的属性在此MVVM对象上可访问
看上面代码 this.proxyData(options)
这里就是上文所说的做数据代码,让数据可以 this.xx
可访问。接下来我们具体看一下 Vue
下面的 proxyData
方法。(多于的代码就不重复写了)
function Vue(option) {
this.proxyData(options)
}
Vue.prototype = {
proxyData: function(data) {
var self = this;
// 需要代理的项
var proxy = ['data', 'methods', 'computed'];
proxy.forEach(function (item) {
if(data[item]) {
Object.keys(data[item]).forEach(function(key){
// 如果是方法的话就不做 get(), set() 操作
if(item === 'methods') {
self[key] = self.$options.methods[key];
} else {
Object.defineProperty(self, key, {
enumerable: true, // 可以枚举
configurable: false, // 不可以重新再定义
get: function gettter() {
if (typeof self.$data[key] !== 'undefined') {
return self.$data[key];
}
},
set: function setter(newVal) {
if (self.$data.hasOwnProperty(key)) {
self.$data[key] = newVal;
}
}
})
}
})
}
}
}
}
// 以上就完成了对数据 data、methods、computed 的相关绑定。
2. 监视,劫持数据(observe data)使新添加的数据都可以在此MVVM对象上可访问
function Observer(data) {
this.observe(data);
}
Observer.prototype = {
observe: function(data) {
var self = this
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
self.observeObject(data, key, data[key]);
})
},
observeObject: function(data, key, val) {
var self = this;
// 创建dep
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不可重新定义
get: function gettter() {
// 收集依赖,建立 dep 与 watcher 之间的关系
if (_target) {
dep.addDep(_target);
}
return val;
},
set: function setter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 数据改变,通知所有的 watcher 更新
dep.notify();
}
});
// 递归给data数据里面的所有层次属性的数据加上 observe 劫持
self.observe(val);
}
}
3. 编译模版、指令(compile template,direcitves)
compile 也是很简单的操作,分三步:
- 取出 el 元素中所有子节点保存到一个 fragment 对象中
- 编译 fragment 中所有层次子节点
- 将编译好的 fragment 一次性添加到页面的 el 元素中
其中在第2中会复杂一点,分两种情况,需要解析文本内 大括号表达式 {{}}
,另外要解析标签内 “v-指令”
function Compiler(el, vm) {
this.$el = el;
// 保存 vm 到 compiler 对象上
this.vm = vm;
if (this.$el) {
// 1. 取出 el 元素中所有子节点保存到一个 fragment 对象中
this.$fragment = this.nodeToFragment(this.$el);
// 2. 编译 fragment 中所有层次子节点
this.compile(this.$fragment);
// 3. 将编译好的 fragment 一次性添加到页面的 el 元素中
this.$el.appendChild(this.$fragment);
}
}
Compiler.prototype = {
// 复制 node 节点到 documentFragment
nodeToFragment: function(node) {
// 创建一个空的 fragment
var fragment = document.createDocumentFragment(), child;
// 循环将 node 中所有的子节点都转移到 fragment 中
while (child = node.firstChild) {
// 将子节点从原来的位置移动到 fragment 中
fragment.appendChild(child);
}
return fragment;
},
compile: function(node, scope, profor) {
var self = this;
// 是否有子节点
if(node.childNodes && node.childNodes.length) {
// 子节点遍历
node.childNodes.forEach(function(child) {
// 是否是文本节点
if(child.nodeType === 3) {
self.compileTextNode(child, scope, profor);
}
// 是否是标签节点
else if (child.nodeType === 1) {
self.compileElementNode(child, scope, profor);
}
})
}
},
// 编译文本节点
compileTextNode: function(node, scope, profor) {
var text = node.textContent.trim();
if(!text) return;
// 提取表达式的内容,去掉 {{}}
var exp = parseTextExp(text, profor);
scope = scope || this.vm;
this.textHandler(node, scope, exp);
},
// 编译标签节点
compileElementNode: function(node, scope, profor) {...},
// v-text
textHandler: function(node, scope, exp) {
this.bindWatcher(node, scope, exp, 'text');
},
// 给每一个 exp 添加一个 watcher
bindWatcher: function(node, scope, exp, dir, prop) {
var updateFn = updater[dir];
// 通过 Watcher, 获取 exp 表达式字段在 data 对应的值 newVal
var watcher = new Watcher(exp, scope, function(newVal) {
if(updateFn) {
// 更新 fragment 的值,替换成 data 里面对应的字段
updateFn(node, newVal, prop);
}
})
}
}
var updater = {
// 更新节点的 textContent 属性值
text: function (node, newVal) {
node.textContent = typeof newVal === 'undefined' ? '' : newVal;
},
...
}