今天跟大家一起简单学习下,Vue从引入到最终实现页面呈现的过程,中间会涉及到一些比较重要的知识点,比如Watcher等,所以会放在一起便于理解。
1、创建实例
要想使用 Vue 等第三方JS库,都得先从引入开始。一般情况下,单页面应用里,我们会在 main.js 里引入 vue,并创建 vue 实例。
import Vue from 'vue'
new Vue({
render: h => h(App),
}).$mount('#app')
这两段代码其实都执行了自己的流程。
首先,import 的 vue 文件,会执行文件内定义执行函数去完成一个初始化的功能。其中包括:
initMixin(Vue); // 初始化_init方法,挂载到vue实例上,后面会详细讲解
stateMixin(Vue); // 初始化data,props,watcher等属性
eventsMixin(Vue); // 初始化event事件,v-on等
lifecycleMixin(Vue); // 绑定一些vue事件,比如destory等
renderMixin(Vue); // 绑定_render函数,用以获取vnode
此处需要注意的是,不管引入多次模块,由于 webpack 的优化,里面的执行方法只会运行一次。
其次,new 一个 Vue 实例对象。Vue方法见下方源码。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
可以清楚的看到,其中只是对生产环境进行了判断,然后执行了 _init 方法,上面讲过是在初始化时,将该方法挂在到了vue方法上,所以直接 this._init()。
重点,_init 方法在两个地方会用到,此处是一个,下面会详细讲到。
总结一下大致流程:
- 首先,import 的 vue 文件执行一次
initMixin等方法,将 _init 挂到 Vue 方法的原型上,这样就可以直接调用了; - 然后在
new Vue()时,会执行该对象里面的方法,有多少次new Vue()就会执行几次(第一个 _init ); - 一般我们的项目里肯定不止在第一次(main.js)里去创建 Vue 对象,项目里定义每个组件都会作为一个独立的 Vue 组件去创建,在组件注册时,通过
vue.extend()(继承自 vue 的构造器,内部第二个 _init 方法)创建组件实例。主要原理就是通过Vue.extend方法把自定义的组件对象传入,然后创建这个组件对应的构造函数。
2、创建Vue组件
先放上关键代码
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
//。。。。。。
var Sub = function VueComponent (options) {
this._init(options); // 此处是第二次使用
};
//。。。。。。
if (Sub.options.props) {
initProps$1(Sub);
}
if (Sub.options.computed) {
initComputed$1(Sub);
}
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.options = mergeOptions( // 父类Vue.options和传入的组件对象进行了合并,这样二者的Sub.options就包含了全局的和局部的属性了。
Super.options,
extendOptions
);
//。。。。。。
return Sub
}
举个例子,最外层的 app 组件的 extendOptions 打印结果如下:
由此可见,这个对象就是 Vue 构造器的子类,所以,此方法也是对传入的属性对象进行 _init 处理成为组件。好的,到这边我们最基本的入口部分的知识才初步有了了解,咱一鼓作气,顺着这个方法继续往下。
这边的 initMixin 方法定义了_init方法:
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++; // 每个组件都有个自己的唯一的uid
var 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);
}
// 一个阻止它被依赖收集的标志
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(
//将一些vue组件的属性重新处理一下添加到$options上
//比如normalizeProps(child, vm),处理了props的值
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
//_renderProxy 会在创建VNode时候使用到,它的作用就是控制调用render函数时所指向的上下文,
//其实就是我们所说的this指向
if (process.env.NODE_ENV !== 'production') {
initProxy(vm); // 创建VNode的时候,开发环境下,会设置代理,去处理一些handler事件
} else {
vm._renderProxy = vm;
}
// expose real self
vm._self = vm;
initLifecycle(vm); //处理生命周期
initEvents(vm); //处理事件监听
initRender(vm); //创建虚拟节点
callHook(vm, 'beforeCreate'); //调用beforeCreate生命周期
initInjections(vm); // resolve injections before data/props
initState(vm); //初始化data,props,computed,watcher
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); //调用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); // 挂载虚拟节点
}
};
}
我们可以看到 _init 函数最后调用了 $mount,该方法就是把模板渲染成被浏览器识别的 DOM,挂载到页面元素上,接下来我们看看 $mount 方法做了什么。
// public mount method
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
$mount 方法的第一个参数是绑定挂载节点的,如果是字符串则去 document.querySelector 查找该节点,否则直接返回该节点或 undefined。举例:比如我们的 vue 项目的 html 文件的根节点是 #app,自然将虚拟节点挂载这个上面。
有个注意点, $mount 方法除了这边(初始化 Vue 实例时)用到以外,还会再第一次渲染组件或更新组件内容的时候去重新挂载,下面在遇到 createComponent 方法时会讲到。此处就涉及到 diff 算法,patch等,要学的太多了,不能乱了脑子,回到我们研究的地方。
1丶其中首次调用是在实例化时去调用一下。
new Vue({
render: h => h(App),
}).$mount('#app')
下面再继续研究下 mountComponent 方法。
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) { // 没有render函数的情况
vm.$options.render = createEmptyVNode; // 先创建空的vode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
);
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
);
}
}
}
callHook(vm, 'beforeMount'); // beforeMount钩子函数
var updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // 涉及到性能埋点与分析,先不考虑
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var vnode = vm._render(); // 调用_render函数
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating); // 调用_update函数
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else { // 主要看到这边,updateComponent就是_update方法。
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
//创建watcher实例对象,把刚刚定义的updateComponent方法传入进去
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
在继续理解主线流程前,我们还需要先搞懂一下前面提到的几个函数哪儿来的。
(1) render函数
_render 方法主要做了一件事,调用 render 方法来生成虚拟 DOM 对象。简单讲下方法流程,代码太多会乱,先不考虑这么多。
这边主要通过 vnode = render.call(vm._renderProxy, vm.$createElement);,
调用之前绑在 vue 实例上的 render 方法,主要是在 initRender 方法里。
initRender 主要代码如下:
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
//。。。。。
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
可以看到主要调用了 createElement,这个方法里主要就是自定义了 _createElement 方法,判断几种情况,生成 VNode。
if (typeof tag === 'string') { // 如果是普通的标签
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 组件形式的情况下
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// 其他情况
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
createComponent 方法在创建组件(处理 data , props 等)的同时,会去看组件内是否有子组件,有的话,再此去对这些子组件去执行 $mount 操作。
//。。。。。。
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
//。。。。。。
由调用该 _render 方法的 $mount 的机制可以看的出来:我们所定义的一个组件,则分别对应一个 VNode对象,同时里面的每个标签一直会循环到最里层,生成一个父子形式的 VNode 结构,代表整个页面的结构与数据。不难看出,当 Vue 机制内的一些方法去运行时,比如比较更新,先对虚拟节点进行处理才将结果输出就可以减少DOM机制的消耗。在拿到我们想要的节点后,紧接着会去执行 _update方法。
(2) _update函数
创建好虚拟DOM后,接下来就是将虚拟DOM转换为真实DOM并挂载。最终挂载真实DOM的逻辑主要就是在这边完成。它在两个地方被调用,一个是首次渲染,另一个是数据更新的时候,这里我们主要还是看我们这边涉及到的第一次渲染。
function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode; // 第一次触发的话,_vnode是null,prevVnode 也是null
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode; // 给_vnode 赋值,下次在进来时,这个参数就是有值的
// 此处的作用是,将VNode转换成真实DOM,赋值给$el参数
if (!prevVnode) { // 第一次初始化
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else { // 更新时触发
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
。。。。。。
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
};
这边的主要代码是 patch 方法,定义在Vue.prototype.__patch__ = inBrowser ? patch : noop; ,判断浏览器和非浏览器的情况下,该调用哪种方法。在源码里,是通过createPatchFunction方法来创建这个方法的。_patch__方法的主要逻辑:
- 如果已存在vnode,那么会调用patchVnode函数来比较之间的区别
- 如果不存在vnode,实际上会使用挂载点的DOM对象和vnode来进行处理 最后就是经过一些列的比较、更新等操作,创建真实DOM节点并插入的过程。(如果存在子组件的话,会通过递归先创建子组件的内容,再去创建父组件)
那么接下来,我们回到正题。 在实例化 Watcher 对象时,触发了我们上方讲到的一系列函数。其实这边的一个Vue从编译模板到挂载DOM对象的逻辑就跑通了。但我觉得 Watcher 对象的内容帮助我们更加深入理解Vue的机制有很大的帮助,再继续往下深入看一点。
接下来,我们重点看一下 new Watcher 做了什么。
3、Watcher
(注意,此处只更具这一条线,梳理简单的流程,有些知识点这边不会讲到) Watcher 是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性更新时作相应的操作,即实例化时传入的回调函数;在Vue实现依赖收集操作的时候,会记录所有的 Watcher,当属性更新时,通知 watcher 执行更新dom操作。
Watcher在Vue里是在三个地方被调用:
- 一个是计算属性 computed 创建的 computedWatcher;
- 一个是监听属性watch创建的 Watcher;
- 还有一个是用于渲染更新dom的 renderWatcher;
一个组件只有一个 renderWatcher,有多个 computedWatcher 和 userWatcher 。下面针对renderWatcher的情况的事件也都是以 只有一个 renderWatcher 实例对象 来实现的。
我们本文接触到的就是 renderWatcher,它主要就是运行了注册的回调函数 updateComponent。
主要代码:
var Watcher = function Watcher (
vm,
expOrFn, // renderWatcher的情况下,传入的就是updateComponent函数
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: '';
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== 'production' && warn(
"Failed watching path: "" + expOrFn + "" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
};
_watcher 就是 isRenderWatcher ,然后所有的 Watcher 对象会存进 _watchers 数组里。
此处代码的最后,通过调用Watcer原型的get()方法,触发updateComponent方法的同时,将该Watcer实例赋值给Dep.target,给后来使用。
Watcher.prototype.get = function get () {
pushTarget(this); // Dep.target 赋值
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm); // 触发getter(updateComponent)方法
} catch (e) {
if (this.user) { // 自己创建的watcher
handleError(e, vm, ("getter for watcher "" + (this.expression) + """));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget(); // 清除 Dep.target
this.cleanupDeps(); // 清理并将newDeps赋值给Wathcer内部的deps,确保内部deps里只存在本次新加入的Watchers
}
return value
};
- 当我们创建 Watcher 时,便会触发
get方法; - 给 Dep.target = 当前的Watcher,在触发
depend方法时,存入 Dep 对象的 subs 数组,用来完成通知DOM元素更新的操作,同时也会存进 Watcher的newDeps数组里; - 当是 isRenderWatcher 的情况下,
getter是函数updateComponent; cleanupDeps,清理并将newDeps赋值给Wathcer内部的deps;
我们来看一下depend 方法:
Dep.prototype.depend = function depend () { // Dep.target是唯一值,用来定义当前在处理
if (Dep.target) {
Dep.target.addDep(this); // 此处的this是Dep
}
};
/**
* Add a dependency to this directive.
*/
Watcher.prototype.addDep = function addDep (dep) {
//dep就是创建的Dep对象,在获取get事件值的触发的
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id); // 加入newDepIds
this.newDeps.push(dep); // 加入newDeps,后面会存入deps,然后清空newDeps
if (!this.depIds.has(id)) { // 对subs内的watcher去重
dep.addSub(this); // depIds不存在的话,则加入subs
}
}
};
可以看到,其实 Dep 和 Watcher 是相互依赖的。当将 Watcer 存入 Dep 的 subs 数组的同时,Watcer 也会将该 Dep 存入本身的 deps 属性里面。
然后,当被监听的data发生改变时,会触发set方法,从而调用 dep.notify(),会通知对应 subs 里的 Watcher 调用对应的 update 方法;同时,根据上面说到的Watcer 本身的 deps 属性里面的Dep对象,它是干嘛的呢?根据所得的依赖关系,通过遍历计算属性里面的watcher对象的deps,再重新去订阅计算属性的变化。
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
notify函数:
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
举个例子:
<template>
<div>
{{msg}}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data(){
return {
msg: 'data'
}
},
watch:{
msg(newVal){
console.log(newVal)
}
},
}
</script>
Vue实例里的 _watchers 里有两个元素,一个时 renderWatcher 和自定义的 watcher。
1、自定义的 watcher
这张图对应的是自己创建的 watcher,其中的user属性为true。可以看到 deps: [Dep] , 从前面的总结不难看出,这里的Dep就是指的是,建立 msg(观察属性)时所依赖的data中的 msg 属性所对应 dep。
2、renderWatcher
这张图是创建的 renderWatcher,从 expression、getter属性可以看的出来。 可以看到其中的 deps 是有值的,有一个值,此值就是在编辑模板上 {{msg}} 时,所创建的dep,此时存在依赖关系。当data数据里的msg发生改变,触发 set 的同时,以此 dep 为媒介,通知它的 Watcher 去执行DOM更新操作。
4、最后
到这边,一个简单的关于 Vue 实例化并挂载到页面,到最后创建 Watcher 实现监听的流程就结束了。这边只是针对其中的一个小方向进行了学习和记录,所以会有很多没讲到的其他知识点。跟我一样刚开始学习Vue源码的有机会可以一起学习。