上一篇文章简单介绍了一下Vue是如何初始化的,这次我们简单分析一下$mount以后发生的事情
1. 渲染模版
上一篇我们讲到了在初始化以后,会调用vm.$mount方法
// 现在Vue.prototype.$mount指向的是src/web/index.js下的$mount,把她保存为mount,然后重
// 新赋值
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
var options = this.$options;
// 判断在组件内是否定义了render方法, 如果定义的话,会走本身的render方法
if (!options.render) {
var template = options.template;
// 判断组件内是否有template字段,如果没有的话调用getOuterHTML获取指定的html字符串
if (template) {
if (typeof template === 'string') {
...
} else if (template.nodeType) {
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
template = getOuterHTML(el);
}
}
if (template) {
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
mark('compile');
}
/**
* 调用compile方法,对模版以及你传入自定义的delimiters进行编译, 返回一个render函数
* function anonymous() {
* with(this){return _c('div',[_m(0),(message)?_c('p', *[_v(_s(message))]):_c('p',[_v("No message.")])])}
* }
*
*/
// 编译模版,返回render函数
var ref = compileToFunctions(template, {
shouldDecodeNewlines: shouldDecodeNewlines,
delimiters: options.delimiters
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
......
}
}
// 继续调用 mount方法,判断是否在浏览器环境,如果不是的话 el = undefined
return mount.call(this, el, hydrating)
};
2. mount会触发mountComponent(src/core/instance/lifecycle.js)函数
该函数主要做了如下几件事
- 调用beforeMount生命周期函数
- 定义updateComponent
- new Watcher() 渲染页面,收集依赖
- 调用mounted函数
3. 收集依赖 new Watcher(vm, updateComponent, noop)
Watcher类
Watcher是收集依赖的关键函数
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options
) {
this.vm = vm;
// 当前wather对象存储到vm上的_watchers数组内
vm._watchers.push(this);
// options
// watcher可以接受4个参数,可以参考$watch的第三个参数option
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
} 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 = [];
// 把所有依赖的dep的id都搜集起来,后面会用到,防止重复收集依赖
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: '';
// 把你传进来expOrFn赋值给this.getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function () {};
}
}
// 由于new Watcher的时候没有传入option,所以this.lazy为false, 会触发this.get方法
this.value = this.lazy
? undefined
: this.get();
};
this.get
this.get会触发你new Watcher的时候传进来的expOrFn,也就是this.getter函数 这里的getter的函数就是渲染dom结构的函数
get () {
// 存储到targetStacks数组中,并且Dep.target = this
pushTarget(this)
let value
const vm = this.vm
// this.user = true的时候是你在组件内定义watch/this.$watch方法的时候
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
// 现在会触发你传进来的回调函数expOrFn,也就是updateComponent方法
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
4. 渲染页面 updateComponent (src/core/instance/lifecycle.js)
该函数主要是调用_render方法,生成vnode 调用_update更新页面 如果是非生产环境并且在配置项里面配置了Performance,会mark页面渲染的用时
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
// 调用_render生成vnode
const vnode = vm._render()
mark(endTag)
measure(`${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`${name} patch`, startTag, endTag)
}
vm_render
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
let vnode
try {
// 调用render函数,如果页面里面获取了data里面定义的值
// 这里会触发data的get函数,会把对应的watcher放到
// 该data对应dep数组内
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
}
}
vm.__update
vm.__patch__更新ui的核心方法,初始化的时候会根据vnode创建dom树,如果更新的时候会__patch__会比对出来,然后渲染出来最新的dom结构
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 如果代码
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// 初始化的时候prevVnode是null
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
vm.__patch__调用createElm方法,创建dom
createElm用createElement创建标签,然后children的内容或者dom,会insert进该标签,当循环到最后一层的时候,会把最外层的标签insert进页面,然后删掉原有的dom标签
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // for transition enter check
// 如果当前的标签是自定义组件,那么就会进入初始化组件方法,调用 this._init方法
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
inPre++
}
if (
!inPre &&
!vnode.ns &&
!(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
...
} else {
// createChildren会继续调用createElm函数创建
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// vnode里面的data存放该标签绑定的事件,此方法是给指定的标签绑定对应的方法
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 当createChildren循环完成以后,会插入到执行的父级别标签
// 当所有循环都走完insert会把创建好的dom结构插入到dom中
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
inPre--
}
} else if (isTrue(vnode.isComment)) {
// 创建注释
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
总结:
- 我们以2篇文章的篇幅把vue从初始化到最后生成页面,以及如何依赖收集简单阐述了一下
- 其中需要简单说明一下的是,我们在方法里面
this.name = '4'
this.nameCopy = '3'
这么赋值的时候,是不是会重复连着两次更新页面ui呢? 其实是不会的,每一个watcher有一个唯一id,当我调用this.name的时候,会把watcher对应的cb函数放到callback的数组内,然后放到eventloop最后面执行(通过promise/setTimeout/MutationObserver实现的).接着执行this.nameCopy的时候,他会首先检查watcher id是否已经存在,如果存在则不会继续push到callback数组内。当两个赋值操作完成以后,才会循环执行callback里面的函数,以当前为例子,也就是调用vm.update(vm._render(), false)函数 3. 接着会继续补充一些重点api的文档,会以短文的形式发出,以上