vue2源码简单实现stage2
前面我们已经实现了虚拟dom转变为真实dom,并且挂载到页面的逻辑了,其实已经很接近vue源码的思想了,但是我们还是稍微调整一下代码,让我们调用的时候更加像vue。现在我们要声明一个vue函数,做以下几件事:
- 声明一个Vue函数,让我们可以new调用这个函数
- 获取传入的options参数
- 初始化数据
- 挂载生成的dom元素
function Vue(options){
this.$options = options;
// 劫持处理数据,
initData(this)
//挂载元素
this.mount(document.querySelector(options.el))
}
这里说明以下,为什么我们不直接使用Class类去实现vue,下面是vue源码里面关于vue初始化的代码
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,如果用class类实现vue,那么实现这些逻辑会非常费劲。
// 现在我们开始调用Vue
;(function(){
function Vue (options) {
this.$options = options;
console.log(this);
initData(this);
//先初始化数据再考虑挂载
//this.mount(document.querySelector(options.el))
}
new Vue({
el: '#app',
data: {
message: 'Hello world'
},
render () {
return createElement(
'div',
{
attrs: {
'class': 'wrapper'
}
},
[
createElement(
'p',
{
attrs: {
'class': 'inner'
}
},
this.message
)
]
)
}
})
})();
上面console.log(this)结果如下图:
我们可以注意到vue 中 render 函数调用的是this.message,但是我们调用的vue的时候,明明message是在this.$options.data对象里面的。接下来我们就要接触vue2的一个核心思想了,那就是通过Object.defineProperty这个方法初始化data数据的时候将this.$options.data 转变成vm.message,同样为了好理解,我们只考虑data下面的message是字符串的情况。
;(function(){
function initData (vm) {
//这里将vm指向了vue本身,再vm下生成了$data
var data = vm.$data = vm.$options.data;
var keys = Object.keys(data);
var i = keys.length
// proxy data so you can use `this.key` directly other than `this.$data.key`
while (i--) {
proxy(vm, keys[i])
}
}
function proxy (vm, key) {
// Object.defineProperty 将 vm.$options.data 下面的值赋值给了vm下面
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function () {
return vm.$data[key]
},
set: function (val) {
vm.$data[key] = val
}
})
}
function Vue (options) {
this.$options = options;
console.log(this);
initData(this);
//先初始化数据再考虑挂载
//this.mount(document.querySelector(options.el))
}
new Vue({
el: '#app',
data: {
message: 'Hello world'
},
render () {
return createElement(
'div',
{
attrs: {
'class': 'wrapper'
}
},
[
createElement(
'p',
{
attrs: {
'class': 'inner'
}
},
this.message
)
]
)
}
})
})();
我们来看看initData前后Vue函数中this的变化
好了,经过了初始化数据我们可以再render函数中使用this.message这个变量了,我们就开始挂载元素了,这个昨天已经分析过了,只不过稍稍改动了一点代码而已,下面直接贴出完整的代码。
/**
* apply virtual dom to real dom
*/
;(function () {
function vnode (tag, data, children, text, elm) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
}
function normalizeChildren (children) {
if (typeof children === 'string') {
return [createTextVNode(children)]
}
return children
}
function createTextVNode (val) {
return new vnode(undefined, undefined, undefined, String(val))
}
function createElement (tag, data, children) {
return new vnode(tag, data, normalizeChildren(children), undefined, undefined);
}
function createElm (vnode) {
var tag = vnode.tag;
var data = vnode.data;
var children = vnode.children;
var text = vnode.text
if (tag !== undefined) {
vnode.elm = document.createElement(tag);
if (data.attrs !== undefined) {
var attrs = data.attrs;
for (var key in attrs) {
vnode.elm.setAttribute(key, attrs[key])
vnode.elm.innerHTML = text!==undefined? text:''
}
}
if (children) {
createChildren(vnode, children)
}
} else {
vnode.elm = document.createTextNode(vnode.text);
}
return vnode.elm;
}
function createChildren (vnode, children) {
for (var i = 0; i < children.length; ++i) {
vnode.elm.appendChild(createElm(children[i]));
}
}
function initData (vm) {
var data = vm.$data = vm.$options.data;
var keys = Object.keys(data);
var i = keys.length
// proxy data so you can use `this.key` directly other than `this.$data.key`
while (i--) {
proxy(vm, keys[i])
}
}
function proxy (vm, key) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function () {
return vm.$data[key]
},
set: function (val) {
vm.$data[key] = val
}
})
}
function Vue (options) {
this.$options = options;
initData(this);
this.mount(document.querySelector(options.el))
}
Vue.prototype.mount = function (el) {
this.$el = el;
var vnode = this.$options.render.call(this)
this.patch(this.$el, vnode)
}
Vue.prototype.patch = function (oldVnode, vnode) {
var isRealElement = oldVnode.nodeType !== undefined; // virtual node has no `nodeType` property
createElm(vnode);
if (isRealElement) {
var parent = oldVnode.parentNode;
if (parent) {
parent.insertBefore(vnode.elm, oldVnode);
parent.removeChild(oldVnode);
}
}
return vnode.elm
}
new Vue({
el: '#app',
data: {
message: 'Hello world'
},
render () {
return createElement(
'div',
{
attrs: {
'class': 'wrapper'
}
},
[
createElement(
'p',
{
attrs: {
'class': 'inner'
}
},
this.message
)
]
)
}
})
})();