50.Vue组件挂载的过程分析
上一篇文章理了一下vue初始化的过程,本章来理一下vue的挂载过程。上一篇初始化相关内容->看这里
同样还是从入口文件开始:
//入口
function Vue(options) {
//进行一些初始化操作
this._init(options);
}
1.初始化 src\core\instance\init.js
可以看到在初始化完成之后会调用mount
方法进行挂载,接下来我们看看mount
中做了哪些事情?
$mount
是挂载到vm上的方法,因此我们去Vue的原型上找这个方法,发现是在entry-runtime-with-compiler.js
中进行声明的,接下来看看mount中做了什么事情。
export function initMixin(Vue: Class<Component>) {
//在initMinxin里面定义Vue的原型方法_init
Vue.prototype._init = function (options?: Object) {
// expose real self
vm._self = vm; //把vm放在_self属性上暴露出去
//一些初始化的工作
callHook(vm, "created");
//调用mount方法进行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
2.编译阶段 src\platforms\web\entry-runtime-with-compiler.js
- a.首先判断传入的
el
的合法性,获取到的元素不能是body
标签和html
标签 - b.判断是否有
render
,后面我们会看到最终的模板编译最终也是要生成一个render
函数 - c.判断有模板的合法性之后,
compileToFunctions
这个函数将模板字符串编译成渲染render
函数 - d.进行挂载操作
Vue.prototype.$mount = function (
el ? : string | Element,
hydrating ? : boolean
): Component {
el = el && query(el)
//a.判断el是否是body或者html标签
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
}
const options = this.$options
// resolve template/el and convert to render function
//b.判断是否有render,render函数的优先级大于一切
if (!options.render) {
let template = options.template
if (template) {
//获取模板的操作。。。省略
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const {
render,
staticRenderFns
} = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this) //c.把模板语法变编译render函数,把编译好的东西挂载到render函数上
options.render = render
options.staticRenderFns = staticRenderFns
}
}
//d.进行挂载
return mount.call(this, el, hydrating)
}
3.编译阶段->将template字符串编译成ast src\compiler\to-function.js
判断有模板的合法性之后,compileToFunctions
这个函数将模板字符串编译成渲染render函数
compileToFunctions
进去可以看到这个函数其实是由createCompileToFunctionFn
创建的来的
-
createCompileToFunctionFn
这个函数里面一个很重要的方法是compile
方法,而compiler又是由createCompilerCreator
创建来的 -
createCompilerCreator
这里面的内容没有什么好看的,大致就是重写options,然后将模板和配置项传给编译器进行编译->编译器(baseCompile) - 接下来使用
parse
方法将模板编译成ast
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
// compile--重点来了
const compiled = compile(template, options)
return (cache[key] = res)
}
}
src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
//编译模板,将编译结果返回
const compiled = baseCompile(template.trim(), finalOptions)
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
4.生成vnode src\compiler\index.js
- 接下来使用
parse
方法将模板编译成ast
- parseHTML将html字符串转成ast,大致思路就是,循环html字符串,逐个进行解析,直到html字符串解析完成,将结果进行返回。
- 里面解析过程大多都是使用正则表达式来判断的,当匹配到是开始标签的时候,调用
handleStartTag
方法,处理开始标签,并将标签的属性和标签名等信息解析处理,存入stack中
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
//生成ast
const ast = parse(template.trim(), options);
//没有配置优化项
if (options.optimize !== false) {
//自动进行优化,标记静态节点等
optimize(ast, options);
}
//将抽象语法树编译为render函数->目前只是_c写出需要创建什么
//返回内容是一个with函数with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('child',{attrs:{\"test\":test}})],1)}
const code = generate(ast, options);
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns,
};
});
src\core\vdom\patch.js
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void {
const stack = [];
//核心代码
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
//遇到开始标签使用start进行解析
start(tag, attrs, unary, start, end) {
//遇到开始标签,就创建一个ast对象
let element: ASTElement = createASTElement(tag, attrs, currentParent);
},
end(tag, start, end) {
closeElement(element);
},
chars(text: string, start: number, end: number) {},
comment(text: string, start, end) {
},
});
//将重构的ast返回
return root;
}
5.挂载阶段 src\compiler\index.js
const code = generate(ast, options);->最终的返回结果code就是_c('div',{attrs:{"id":"app"}},[_c('child',{attrs:{"test":test}})],1)
最后到了mount.call(this, el, hydrating)
进行挂载的地方
调用mountComponent()
方法-->开始创建Vnode
第一次的时候prevNode
为空,patch
的时候oldVnode
为空,创建一个空的vnode节点
,接下来根据最新的vnode创建node节点
。
然后根据vnode.componentInstance
来判断是否需要创建子组件?
如果需要走createComponent
方法进行初始化子组件,否则进行原生标签创建。
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//挂载组件
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
//组件挂载前调用的钩子函数
callHook(vm, "beforeMount");
let updateComponent;
//用户$mount时,定义updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
//在执行组件实例时,每一个实例都有一个watcher与之对应,如果一个组件的数据发生改变,我们只会调用改变的组件里面的渲染函数和更新函数,合理的切割组件粒度
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
//hydrating:watcher api使用的,用来判断是否加载最新的内容
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;
}
createComponent创建组件,怎样判断是否是一个子组件呢?看下面代码,注释中其实已经告诉我们答案了
src\core\vdom\patch.js
在此判断是否需要创建子组件
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
//如果vnode是一个子组件,子组件实例已经创建并且已经挂载了,子组件也设置了是否是vnode的标识符。
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
//初始化组件,从子组件到父组件
initComponent(vnode, insertedVnodeQueue);
//插入html内容
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
}
}
Q:怎么判断需要创建组件呢?
A:isDef(vnode.componentInstance)
Q:怎么走到创建元素的?
A:构建完vnode之后,需要根据vnode构建出node节点
Q:patch的时候为什么要创建元素?
A:patch的过程就是将vnode转成真实node的过程,在patch的过程通过vnode反映出最新节点的变化情况,方便对真实节点进行操作。
在mount的时候发现有需要创建的子组件,此时再重新开始对子组件进行初始化,创建子组件。
挂载时可以看到会执行两次的vm.$mount(vm.$options.el);
第一次是创建vue的时候,第二次是进行子组件挂载的时候。子组件挂载的时机是在mount阶段的,此时el
元素是没有值的,在mount.call(this, el, hydrating)
时el为undefined
。
在挂载子组件的时候会创建一个watcher,此时watcher是用来监听数据的改变,将通知发送到对应的组件
挂载完成后,开始进行初始化组件工作initComponent(vnode, insertedVnodeQueue);
6.插入元素&渲染阶段
组件创建完成后,将子组件插入到父组件中,在src\core\vdom\patch.js->createComponent
可以看出来。
在createElm insert()
有子组件的时候,判断是否需要创建子组件需要return,返回结果就是一个vnode,然后又通过createElement进行创建子组件节点。
挂载的顺序时先父后子,插入的顺序时先子后父,调用完insert之后,会发现界面上已经出现了child。
PS:对源码的理解可能还不是很深入,有问题欢迎进行指证与我进行讨论,共同学习,加油鸭!
最后附上我的demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<child :test="test"></child>
</div>
</body>
<script>
//定义全局组件
let child = Vue.component("child", {
template: "<div>{{test}}</div>",
props: ["test"],
});
// let componentA = new Vue({
// })
const vm = new Vue({
el: "#app",
// component: child,
data() {
return {
test: "child",
};
},
});
</script>
</html>