共计 9871 个字符,预计需要花费 25 分钟才能阅读完成。
学习目标
- 模板编译整体过程
- 组件化机制
源码环境
"name": "vue",
"version": "2.6.11",
源码的目录结构,可以看下 vue 源码解析(一)——初始化流程及数据响应式过程梳理
我将源码分析分为三个模块进行了分析,可以互相结合看下,学习起来更轻松下。
模板编译整体过程
解析
编辑器可以将模板解析成 AST 抽象语法树;通过遍历这颗对象树生成代码
路径:src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {// 1. 将 html 字符串解析成 AST(语法抽象树)
const ast = parse(template.trim(), options)
if (options.optimize !== false) {optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
parse() 路径:src/compiler/parser/index.js
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 开始编译 解析相关变量
const stack = []
// 解析 html 的相关代码
parseHTML(template, {
// 遇到起始标签调用
start (tag, attrs, unary, start, end) {
.......
// 遇到一个起始节点就创建一个 ASTElement 元素,单根的一个对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {element.ns = ns}
.......
if (inVPre) {processRawAttrs(element)
} else if (!element.processed) {
// for if noce 需要提前处理
processFor(element)
processIf(element)
processOnce(element)
}
......
},
})
parseHTML路径:src/compiler/parser/index.js
因为要匹配 HTML,所以里面都是正则表达式,各种匹配
优化
优化器的作用是在 AST 中找出静态子树并打上标记。静态子树是在 AST 中永远不变的节点,如纯文本节点。
路径:src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {const ast = parse(template.trim(), options)
2. 优化的核心目标,标记静态节点
if (options.optimize !== false) {optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
什么是静态节点
两级包裹,纯文本,比如:
<div> 静态 <span> 节点 </span></div>
// 静态根节点里面是静态节点
递归找到静态根节点,如果 children 也是静态节点,那就定义为静态节点。未来就会生成静态函数,在做 patch 的之后会跳过vue 的处理工作方式:官方认为,如果只有一标签,消耗有些大,去占用内存。所以静态节点指的两层都为静态节点
代码生成
将 AST 转换成渲染函数中的内容,即代码字符串。
路径:src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {const ast = parse(template.trim(), options)
if (options.optimize !== false) {optimize(ast, options)
}
3. 将 ast 生成为函数字符串,还不是函数,而是函数的字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
generate 路径:src/compiler/codegen/index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {export function genElement (el: ASTElement, state: CodegenState): string {if (el.parent) {el.pre = el.pre || el.parent.pre}
// 从这里可以看出 for 的优先级比 if 高
// 一个标签从 if 和 for 首先执行 for 肚子里包着 if
if (el.staticRoot && !el.staticProcessed) {return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {return genOnce(el, state)
} else if (el.for && !el.forProcessed) {return genFor(el, state)
} else if (el.if && !el.ifProcessed) {return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {return genSlot(el, state)
} else {}}
genFor 路径:src/compiler/codegen/index.js
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
// v-for="item in items"
// exp===items
// el.alias === item
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // index
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' //key??
.........
el.forProcessed = true // avoid recursion
// 最后生成的代码是一串子字符串
// altHelper 通常情况不存在
// _l(exp,functin(alias,iterator1,iterator2){return})
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
组件化机制
组件声明:Vue.component()
去寻找为什么 Vue.component()声明的组建可以全局使用
例如:
<body> <div id="demo> <comp></comp> </div> </body> vue.component('comp',{template:'<div>this is comp</div>'}) const app= new Vue({el:'#demo'})
initAssetRegisters() 路径:src/core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
// ASSET_TYPES 是一个数组:['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {// 拿 component 举例:vue.component = function('id=comp',){}
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {if (!definition) {return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {validateComponentName(id)
}
// 组建声明的相关代码
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// _base 是 Vue
// Vue.extend({}) 返回的是构造函数 把组建的配置项转化为组建的构造函数
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {definition = { bind: definition, update: definition}
}
// 注册到 components 选项中去,
// 全局组建,就是在根组建上去创建一个组建挂载在 components 里
// 在 vue 原始选项上添加组建配置,将来其他组件继承,他们都会有这些组建注册
this.options[type + 's'][id] = definition
// 如果使用的是 component 最终返回的是 组建的构造函数
return definition
}
}
})
}
vm._render()路径:src/core/instance/render.js
import {createElement} from '../vdom/create-element'
export function initRender (vm: Component) {
..........
// 编译器生成渲染函数使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// 用户编写渲染函数使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
export function renderMixin (Vue: Class<Component>) {
.....
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {render, _parentVnode} = vm.$options
.....
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
// render 函数执行的的时候,传入的参数 $createElement,也就是才使用的时候的小 h
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {......} finally {......}
vnode.parent = _parentVnode
return vnode
}
}
createElement 路径:src/core/vdom/create-element.js
// 例如我们用的 h 函数(‘div',{},[child1,child2])export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// h('div')或者 h(Component)if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 比如 传进来的是,保留标签,原生元素:div、p 标签
if (config.isReservedTag(tag)) {
// platform built-in elements
............
// 直接 new 一下直接生成
vnode = new VNode(config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
// 尝试拿出构造函数
//isDef(Ctor = resolveAsset(context.$options, 'components', tag)
//resolveAsset 查找获取当前的组建
//context.$options 组建实例的上下文中的‘components’这个属性中去拿 tag(当前的标签)// vue.component 调用的时候在 已经在选项中注册了一个‘component', 值是一个构造函数,这个标签 tag 所对应的值就是构造函数
else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 自定义组建的处理
//Ctor 构造函数
vnode = createComponent(Ctor, data, context, children, tag)
} else {......}
}
createComponent 路径:src/core/vdom/create-component.js
// 返回自定义组建的 vnode
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
......
// 数据处理
data = data || {}
......
// 双向绑定的数据处理
if (isDef(data.model)) {transformModel(Ctor.options, data)
}
// 事件处理 组建上可能会有事件的监听,不是真正的去做事件监听;如果有事件绑定移到合适的选项上去
const listeners = data.on
// 如果是抽象组建如何处理
if (isTrue(Ctor.options.abstract)) {}
// 安装组建管理钩子:未来会做组建的初始化(实例创建, 挂载);createElement 中会执行这个钩子
installComponentHooks(data)
// 创建当前组建的 vnode
const vnode = new VNode(
// 创建的名称 vue-component 开头;-id
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{Ctor, propsData, listeners, tag, children},
asyncFactory
)
// 把当前的自定义组件的虚拟节点,把它返回了
return vnode
// 虚拟 dome 生成真实 dome 还是需要 patch
}
installComponentHooks() 路径:src/core/vdom/create-component.js
// 给 data 中添加一些钩子函数,未来等待 patch 的时候调用
// 用户传入的钩子和默认钩子的合并
function installComponentHooks (data: VNodeData) {const hooks = data.hook || (data.hook = {})
// hooksToMerge 等待合并的钩子
for (let i = 0; i < hooksToMerge.length; i++) {const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
componentVNodeHooks[key]路径:src/core/vdom/create-component.js
const componentVNodeHooks = {
// 实例化 挂载
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {}
if (
vnode.componentInstance && !vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
// 缓存的情况
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 全新的组建创建的情况
// 创建组建实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 子组建挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// 预打包 预补丁 打补丁之前执行
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
// 被插入之后执行
insert (vnode: MountedComponentVNode) {},
// 被销毁时执行
destroy (vnode: MountedComponentVNode) {}}
首次初始化的时候会触发 createElm 路径:src/core/vdom/patch.js
// 组建或者元素的创建
function createElm ( // 将 vnode 变成真实的 dome
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 自定义组建创建
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return}
// 后续都是原生标签
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag)
// 把当前标签创建一个元素
: nodeOps.createElement(tag, vnode)
setScope(vnode) // 设置样式
}
if (__WEEX__) {
// 创建一个元素 设置完样式之后,进行递归 孩子去创建
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {if (isDef(data)) {
// 处理 事件 属性的初始化
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 把节点插入
insert(parentElm, vnode.elm, refElm)
}
}
}
}
createComponent 路径:src/core/vdom/patch.js
// 自定义组建的创建过程
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// 获取数据
let i = vnode.data
if (isDef(i)) { // 在 data 中如果有定义,// 缓存情况,会看实例是否已经存在,keepAlive 的情况
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 如果不存在,就需要初始化一下 init
if (isDef(i = i.hook) && isDef(i = i.init)) {i(vnode, false /* hydrating */) // 这里面就是调用 init 的钩子
}
// 上面创建过程已完成,组建实例已存在
if (isDef(vnode.componentInstance)) {
// 初始化组建:组建上面有事件,属性。都需要提前执行,initComponent
initComponent(vnode, insertedVnodeQueue)
// 插入元素 插入 dom
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
从这里可以得出,父组建和子组建生命周期触发的时间段
1.parent.create
2.parent.beformount
3.child.create
4.child.beformount
5.parent.mounted
创建自上而下 挂载自下而上
组件化流程总结
1. 注册:Vue.component(id,{})
2. 实例化和挂载
-create-element.js | cereateElement() 可以按标签类型处理
-create-component.js | createComponent()-> 自定义组件 vnode
3. 转化为 dom
patch
createElm
createComponent
执行 createComponent()里面的 init hook 的钩子
学习资料
- 思维导图:https://www.processon.com/vie…
源码分为三块进行了整理:
可以在主页一一查看。