new Vue做了啥?
new Vue({
el: '#app',
render: h => h(App),
data() {
return {
message: 'hello vue'
}
}
}).$mount('#app')
打开源码,先找到Vue的构造函数,在vue/src/core/instance/index.js里面,
function Vue
可以看到这个function Vue(){}很简单,几行代码,首先判断用户是否没有用 new Vue(); 是就警告用户必须new来实例化,不能直接
const app = Vue({
...
})
必须要new
const app = new Vue({
...
})
然后, 调用原型方法 this._init(options)
_init方法在哪里?
找一下,在上图的initMixin(Vue)里面,
打开initMixin方法来源 :vue/src/core/instance/init.js
initMixin.png
这里做了啥?其实就是合并传入的选项和一些初始化工作,主要包括:
initMixin2.png
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化状态
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用created钩子
最后就是mount挂载真实dom了
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这么多方法一排排下来,难道我要一个个去看?非也,看源码很忌讳这样子看,因为你一个个看的话,函数里面套函数,你就蒙了,函数调用栈这么深,你相当于深度遍历,吃力不讨好。所以最好带着目的看,回到开始的代码new Vue({...})
new Vue({
el: '#app',
render: h => h(App),
data() {
return {
message: 'hello vue'
}
}
}).$mount('#app')
el是挂载dom,后面看,然后是data,所以看一下它对传入的data做了啥。嗯,说干就干,我们找到initState(vm) 初始化状态 看看它是如何处理data的,找到vue/src/core/instance/state.js
又是初始化props,methods,data,computed,watch等,细心的同学发现这里的props,methods,data,computed,watch顺序是有讲究的,在实际项目开发中,后面的可以通过this.xxx访问前面的提供方法、属性。我们主要看initData如下图
initData.png
敲黑板:面试常考:为什么data推荐函数,而不是对象形式?
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
源码告诉你这里可以是函数也可以对象,推荐函数是为了解决组件实例公用同一个data的问题。所以,就vue来说,如果是根组件,你可以直接用data:{key:val}形式,非根组件就应该用data(){return {key:val}}形式。
然后继续回到源码这里是 data = vm._data,data的结果挂到了vm._data上,_data就是用户定义的data了, 注意业内规范是下划线_ 开头的变量都是私有属性,作者自己用的,用户最好不用访问它。然后看getData函数做了啥?
getData.png
return data.call(vm, vm)
很简单其实就是执行了用户传入的选项中的data方法,这里用call来绑定this到本实例组件,还不懂call啥意思,可以参考我的另外一篇文章 js模拟实现call、apply、bind函数,所以说看源码要原生js基础牢固过得去才行。
接着往下看:
1处,拿data里面的key和props的key,methods的key,判断如果有三者之间有重名的就报错。所以,我们在定义数据的时候不能重名。再看keys循环里面的2处,这里调用方法 proxy(vm,
_data
, key),其实就是把data的数据代理到实例上,所以我们才能够在其他地方直接this.message访问data(){return {message:'hello, vue'}}的message。而不是this._data.message。具体实现看proxy方法:
proxy.png
所以,当我们this.message时就是走了 return this[sourceKey][key], sourceKey是_data, key是message,看上面图提到2处的proxy(vm,
_data
, key)。从设计模式的角度来看,这里就是典型的代理模式,用户不直接访问this._data.message,而是访问this.message。访问时,通过Object.defineProperty(target, key, sharedPropertyDefinition)来进行拦截代理。vue响应式原理也是通过这里进行拦截, 这些以后再扯,至此,data这块算过了,接下来看mount。
$mount做了啥?
其实看$mount可以从runtime-only版本和runtime-compiler版本这两个方面入手,
入口文件分别在vue/src/platfoms/web/entry-runtime-with-compiler.js 和
vue/src/platfoms/web/entry-runtime.js
但是他们都是引入了同级下的runtime/index
runtime/index
runtime/index这里定义了mount方法,runtime-only版本直接使用它,而runtime-with-compiler就引入它再包装一层,这一层的目的就是编译template模板为render函数。看Vue.prototype.$mount干了啥?
还是调用了mountComponenth方法,在vue/core/instance/lifecycle里面,去找它,如下图
image.png
我们主要看let updateComponent这里,updateComponent根据不同环境赋值不同,但是它的核心作用就是一个更新组件的过程,内部调用了vm._update(vm._render(), hydrating),接着往下看,就是
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
这是实例化一个渲染观察者,继续看Watcher是干嘛,找到它,其实它就是一个观察者模式,按照上面的参数传入的话,先做一些依赖收集的工作,最后赋值this.value就是执行updateComponent这个方法,如下图顺序123执行,1处的expOrFn就是updateComponent
Watcher
watcher get
而上面也提到updateComponent内部执行vm._update(vm._render(), hydrating),顾名思义其实就是:
1、vm._render()创建生成virtual dom;
2、vm._update把virtual dom渲染成真实dom
virtua dom 具体怎么生成真实dom的,继续看vm._update这个方法,在vue/src/core/instance/lifecycle.js里面,可以看到这个vue.protype._update不仅仅是初始化渲染,也可能是更新渲染,如图红框里面的if else
_update
继续看__patch__方法干了啥,如下图,这里又做了判断,浏览器端才赋值patch,否则就是noop,noop是一个空函数,啥也不干。
__patch__
我们继续找patch,如下图
patch看到它又是一个动态生成的函数,靠createPatchFunction生成,这是一个高阶函数,这里传入参数{ nodeOps, modules }可以看出实际上createPatchFunction是可以生成不同的patch函数的,就目前来看,这里是传入web平台的参数,那么里面的返回的patch就是专门针对浏览器处理的,还有可能就是传入weex平台的,这里就不展开了。看一下createPatchFunction的实现,打开vdom/patch.js可以看到这个方法很大 【70 - 804行】,里面定义了很多辅助方法,最后return了一个patch方法。
image.png
实际上就是看这个patch方法的逻辑,里面的方法调方法,最后我们找到insert这个方法,这里就是生成真实dom
insert
image.png
里面的nodeOps.insertBefore(parent, elm, ref)、nodeOps.appendChild(parent, elm)把virtual dom 通过原始的 parentNode.insertBefore方法插入dom,我们看nodeOps里面怎么实现的,回到最开始的createPatchFunction({ nodeOps, modules })这里,顺藤摸瓜,找到了vue/src/platforms/web/runtime/node-ops.js,可以看到里面就是一些对原生dom操作的封装而已,很简单,至此我们就摸清了整个mount流程了。
总结一下
所以,new Vue做了啥?
1、对Vue进行_init初始化
2、$mount,里面做了compiler[可选]、生成render函数、render函数创建vnode、vnode里面再进行patch操作,patch操作里面就是更新virtual dom和插入真实dom了。
更多细节得自己看一遍才更有体会,看博客再多没意义,只有你看了源码,再对照别人的博客,互相证道才有更大的收获,谢谢观看。
网友评论