让我们忘记 Vue 的所有代码,仅仅从 new Vue() 开始分析,尝试着能不能自己也来写一个类似 Vue 的框架。
<div id="app"></div>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue'
},
template: '<div>{{msg}}</div>'
})
</script>
首先,我们需要创建一个闭包,然后给 window 上面扩展一个 Vue 的接口,因为我们在外层是需要创建一个 Vue 的实例,所以我们需要定义一个 Vue 的构造函数,然后在 return 出去。
(function(root, factory) {
root.Vue = factory()
})(this, function() {
function Vue() { }
return Vue
})
我们在创建一个Vue实例的时候,会传入一个对象,这个对象包含了很多我们在Vue实例创建的时候它要用到配置项,那么这些配置项怎么接收呢?我们可以用 options来接收。
(function(root, factory) {
root.Vue = factory()
})(this, function() {
function Vue( options ) {
this.$options = options || {}
}
return Vue
})
然后我们知道,通过 vm,也是可以直接改变 data 里面的值的,比如 vm.msg = 'Hello 大木',那么是怎么做的呢?
(function(root, factory) {
root.Vue = factory()
})(this, function() {
function Vue( options ) {
this.$options = options || {}
// 我们需要将 data 上面的属性代理到 Vue 的实例上面去
var data = this._data = options.data
var _this = this
Object.keys(data).forEach(function(key) {
// 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
_this._proxy(key)
})
}
Vue.prototype._proxy = function(key) {
// 给 Vue 的实例(this) 上所有的 key 增加钩子
Object.defineProperty(this, key, {
get: function() {
return this._data[key]
},
set: function(newVal) {
this._data[key] = newVal
}
})
}
return Vue
})
然后我们需要做的就是去监听这个 data 数据的变化,也就是响应式系统。
(function(root, factory) {
root.Vue = factory()
})(this, function() {
function observer(value) {
// 要保证传过来的 data 是个对象
if (!value || typeof value !== 'object') return
// 如果有值,并且又是对象,那么我们就创建一个 Observer 的实例
return new Observer(value)
}
function Observer(value) {}
function Vue( options ) {
this.$options = options || {}
// 我们需要将 data 上面的属性代理到 Vue 的实例上面去
var data = this._data = options.data
var _this = this
Object.keys(data).forEach(function(key) {
// 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
_this._proxy(key)
// 监听 data 数据,加入响应式系统
observer(data)
})
}
Vue.prototype._proxy = function(key) { ... }
return Vue
})
所以我们可以看到,在整个设计 Vue 的过程中,我们的 data 它里面要加入响应式系统,其实就是靠 Observer 来做的。然后我们继续来完善 Observer 这个构造函数。
(function(root, factory) {
root.Vue = factory()
})(this, function() {
function observer(value) {
// 要保证传过来的 data 是个对象
if (!value || typeof value !== 'object') return
// 如果有值,并且又是对象,那么我们就创建一个 Observer 的实例
return new Observer(value)
}
function Observer(value) {
this.value = value
// 用 walk 方法来遍历 value
this.walk(value)
}
Observer.prototype = {
walk: function(value) {
var _this = this
Object.keys(value).forEach(function(key) {
_this.convert(key, value[key])
})
},
convert: function(key, val) {
// this.value 就是 data,key 就是 data 上面的属性,val 是对应的值
this.defineReactive(this.value, key, val)
},
// 监听对象属性值的变化
defineReactive: function(obj, key, val) {
Object.defineProperty(obj, key, {
get: function() {
return val
},
set: function(newVal) {
// 如果我们的值发生了改变,做这个判断,原值 === 新值,说明没有更改,则终止
if (val === newVal) return
// 记录新的值
val = newVal
}
})
}
}
function Vue( options ) {
this.$options = options || {}
// 我们需要将 data 上面的属性代理到 Vue 的实例上面去
var data = this._data = options.data
var _this = this
Object.keys(data).forEach(function(key) {
// 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
_this._proxy(key)
// 监听 data 数据,加入响应式系统
observer(data)
})
}
Vue.prototype._proxy = function(key) { ... }
return Vue
})
那么我们通过给 vm.msg 赋值为 'Hello 大木' 的过程中,我们首先会触发 Vue.prototype._proxy 上面的代理的 set 钩子函数,然后我们会把新的值给到 this.data.msg,也就意味着 data 它上面的钩子函数触发了,也就是 Observer.prototype.defineReactive 上面的 set 钩子。然后我们在打印 vm.msg 的时候,就通过它的 get 返回出去,也就是 'Hello 大木' 了。
其实我们只是给 Vue 的实例增加了一层代理,我们先不管你有没有拓展,先给你代理起来,万一哪天你要更改我,那么我就把你这个更改后的值,通知 data 上面所对应的属性,你的值发生了改变,然后你在去通知视图就 OK 了。
我们要这么做的原因是因为,我们想要在一些生命周期的钩子函数,或者说一些计算属性里面,我们想要去更改 data 里面所对应的属性的时候,我们可以直接通过 Vue 的实例的方式来给它进行改变,我们不需要去找到 data 它里面所对应的 msg 等等,这样找会非常的麻烦,那如果说我把它里面这些响应式的属性,我们都挂载在 vm 上面了,那我们在一些钩子里面进行访问或更改,那是不是更加的方便呢?这就是我们要做一层代理的原因。
那么除了这个东西之外,我们是不是还要思考另外的事情?比如初始化一些事件,依赖注入,还有生命周期等等,那么接下来也就意味,在我们创建一个 Vue 的实例的时候,我们除了通过 observer 来监听 data 数据加入响应式系统,我们还需要调用一个 init 的方法。如果你看过源码就知道 Vue 在初始化的时候 init 了很多方法,那么我们先来完成第一个。
(function(root, factory) {
root.Vue = factory()
})(this, function() {
// 定义对象默认的属性值
// obj对象 prop属性 value值 def默认值
function defineProperty(obj, prop, value, def) {
// 没值就用 el 所挂载的 DOM 元素做为我们要编译的模板, 就是$el.outerHTML
// 有值就用该值
if(value === undefined) {
obj[prop] = def
} else {
obj[prop] = value
}
}
var noop = function() { }
function observer(value) { ... }
function Observer(value) { ... }
Observer.prototype = { ... }
function Vue( options ) {
this.$options = options || {}
// 我们需要将 data 上面的属性代理到 Vue 的实例上面去
var data = this._data = options.data
var _this = this
// 我们在初始化的时候,让它生成一个 render 的函数
defineProperty(this, '$render', this.$options.render, noop)
Object.keys(data).forEach(function(key) {
// 我们让 Vue 的实例通过 proxy 来代理在遍历 data 的时候它所有可枚举的属性
_this._proxy(key)
// 监听 data 数据,加入响应式系统
observer(data)
this.init()
})
}
Vue.prototype.init = function() {
// init 的主要作用是来做一些初始化的操作,通过官方的生命周期图表我们知道,首先就是 $mount 的挂载,那么我们就需要来检测是否有 el 这个选项
var el = this.$options.el
if (el !== undefined) {
this.$mount(el)
}
}
Vue.prototype.$mount = function(el) {
// el 只是一个字符串,但是我们需要获取 DOM
this.$el = typeof el === 'string' ? document.querySelector(el) : document.body
if (this.$el == null) {
new Error('元素' + this.$options.el + '没有被找到')
}
// 检测完 el 之后,我们还在要检测 template 模板是否配置
// 通过 defineProperty 方法来检测 this 有没有 'template' 这个属性
// 如果有,就把 this.$options.template 的值赋值给你
// 如果没有,就找到 this.$el.outerHTML 的值当作一个模板进行编译
defineProperty(this, '$template', this.$options.template, this.$el.outerHTML)
if (this.$render == noop) {
// 编译当前模板
// 如果有传 template 属性, 就编译
// 如果没传, 就编译实例所挂载的元素, 也就是把 #app 当作一个模板来编译
// 将编译完成后的值再赋值给 $render, 也就是我们所说的 render function
this.$render = Vue.compile(this.$template)
}
}
Vue.compile = function(temp) {
// 编译...
}
Vue.prototype._proxy = function(key) { ... }
return Vue
})
以上就是我们通过 new Vue() 做的一个即兴发挥,根据 Vue 实例是怎么创建的,然后按照我们自己的想法和思路去写一部分代码。
如果上面的代码你发了很多时间去看的话,希望你不要打我\(^o^)/,因为上面的代码很多地方都是有问题的。
那么从下一篇博客开始,我们就对照着源码,一步步分析 + 编写,看看 Vue 到底是怎么做的架构。