要说vue 和 react 最大的不同之一,我肯定会说 vue 是开箱即用的,也就是有的情况下,不需要 babel 转义就能直接运行,就好像是一个高级的 jquery 包一样
1、入口 $mount
光光看目录的名称就可以知道,这里是和平台有关的代码,也就是运行在 浏览器的代码,这里主要是 改造 $mount 函数
/* @flow */
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
// 获取 id 的同时,缓存一下 对应的 template
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 缓存 公共的 $mount 用来重写,方便稍后调用
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(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
// 把 template 转换为 render 函数
// 也就是说,如果设置了 render 函数,就直接执行 render
if (!options.render) {
let template = options.template
if (template) {
// 如果 template 是 字符串的话,先判断是不是 id
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
// 是 node 的话,直接获取 innerHTML
} 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) {
// 获取 outerHTML 作为模板
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)
// 这里就获取到 了 render 函数
options.render = render
// 静态节点的渲染函数
options.staticRenderFns = staticRenderFns
}
}
// 再在这里调用 全平台的 mount 函数
return mount.call(this, el, hydrating)
}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
// 获取 outerHTML
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
- 首先缓存 全平台的 mount 函数,并且重写 vue 的 mount 函数
- 如果传入了 el 参数的话,先获取 el 参数,如果 传进来的节点 是 body 或者 html 节点,那就 报错并停止执行
- 如果 没有传入 render 函数的话,检查 template 是否传入,并且获得对应的模板
- 如果 render 和 template 都没有传入,就 获取 传入 el 节点的 outerHTML 作为 template 模板
- 然后 把 template 执行 compileToFunctions 函数生成 render 函数,作为参数 放进 options 之中
- 最后调用 第一步缓存的 mount 函数,并且返回
- 所以 根据传入的参数,其实这里获取的 执行模板是有 优先级的 render > template > el
2、compileToFunctions
这个函数就很简单了,只是贴一下代码,直接进入 createCompiler 函数
/* @flow */
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
3、baseCompile 编译优化 模板的过程
/* @flow */
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
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)
}
// 把抽象语法树 转换成 js 的 代码
const code = generate(ast, options)
return {
ast,
render: code.render, // 这里的 render 是字符串形式的,需要使用 new Function 才行
staticRenderFns: code.staticRenderFns
}
})
先不管 外面的 createCompilerCreator,这个函数主要是处理一些配置和错误捕获用的 ,只关注 当前的 baseCompile 函数就好
把 template 模板编译成 AST 抽象语法树 (这一步过于复杂,先过了)
optimize 优化 抽象语法树 --> 这个过程 就是给 抽象语法树 增加静态节点 标志 isStatic ,如果熟悉 vue diff 的同学,肯定会记得 有一步 是判断 isStatic,然后直接就不比较了,这样可以优化 diff 的性能
把 抽象语法树 转换为 js 代码,注意的是,这里的 js 代码是字符串形式的,如下图所示
4、optimize 优化抽象语法树,给其 增加 isStatic 参数,方便后期 diff
/* @flow */
import { makeMap, isBuiltInTag, cached, no } from 'shared/util'
let isStaticKey
let isPlatformReservedTag
const genStaticKeysCached = cached(genStaticKeys)
// 用来标记 静态子树
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 标记静态节点
markStatic(root)
// second pass: mark static roots.
// 标记静态根节点
markStaticRoots(root, false)
}
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
(keys ? ',' + keys : '')
)
}
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) { // 元素节点
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
// 不要将组件插槽内容设为静态。这样可以避免
//1. 无法更改节点的插槽
//2. 静态插槽内容无法进行热重新加载
if (
// export const isReservedTag = (tag: string): ?boolean => {
// return isHTMLTag(tag) || isSVG(tag)
// }
!isPlatformReservedTag(node.tag) && // 不是 HTML 标签,也就是说,是组件
node.tag !== 'slot' && // 当前的 tag 标签 不是 slot
node.attrsMap['inline-template'] == null // 是否是 内嵌模板
) {
return // 如果当前是组件节点,而且是 slot 的话,继续执行
}
// 递归调用子节点
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) { // 如果当前有一个子节点 不是静态的,那这个节点就不是 static 的
node.static = false
}
}
// 处理 if
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) { // 元素节点,而且 属性是否有 static 和 once
if (node.static || node.once) {
node.staticInFor = isInFor // 在 for 循环中 是否是 静态的
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
// 如果 只有 一个子节点, 而且是 文本节点,那么这个就不是静态的
// 收益大于支出
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
// 遍历所有的子节点
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
function isStatic (node: ASTNode): boolean {
// 表达式
if (node.type === 2) { // expression
return false
}
// 文本内容
if (node.type === 3) { // text
return true
}
return !!(node.pre || ( // 是否是 v-pre (跳过当前节点的编译)
!node.hasBindings && // 没有 动态绑定的节点
!node.if && !node.for && // 不是 v-if v-for
!isBuiltInTag(node.tag) && // 不是 内部组件,slot component 组件
isPlatformReservedTag(node.tag) && // 是 html 标签,也就是说,不是 组件
!isDirectChildOfTemplateFor(node) && // 父组件是否是 v-for
Object.keys(node).every(isStaticKey) // 每个属性是否都在 'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle' 中
))
}
function isDirectChildOfTemplateFor (node: ASTElement): boolean {
while (node.parent) {
node = node.parent
// 如果父组件 不是 template ,返回false
if (node.tag !== 'template') {
return false
}
// 如果父组件 是 v-for 返回 true
if (node.for) {
return true
}
}
return false
}
- 调用 markStatic 标记当前节点是否是 静态节点
- 调用 markStaticRoots 当前 节点是否是 静态 根节点
markStatic
- 调用 isStatic 函数判断当前节点是否是 静态节点
- 如果当前是 表达式,就不是静态的,如果当前是 一个 文本节点,那就是 静态的
- 如果当前是 一个 node 节点的话
- 查看是否标记了 v-pre (跳过当前节点的 编译 )
- 没有动态绑定的节点 && 不是 v-if v-for && 不是 内部的 slot/component 组件 && 是 html 节点(不是组件) && 父组件不是 template && 父组件 不是 v-for && 每个属性 都在 [type, tag, attrsList, attrsMap, plain, parent, children, attrs, start, end, rawAttrsMap, staticClass, staticStyle ] 中
- 接下来进一步 解析 static
- 判断是否是 组件的 slot ,是的话,直接跳过(在第5步中,遇到 slot 则不设置 static ),所以 slot 就不会被设置为 静态节点
- 递归调用子节点,然后 通过 markStatic 进行标记
如果当前有一个子节点 不是静态的,那这个父节点就不是 static 的
- 标记 v-if 节点是否是 静态节点
markStaticRoots
如果当前节点是 node 元素节点的话
如果 只有 一个子节点, 而且是 文本节点,那么这个就不是静态的 (英文注释写的很明白,收益大于支出,还不如每次都 动态渲染一遍)
遍历 所有的子节点,以及 v-if
5、generate 把 AST 转换为 js 代码
generate 函数 不做过多地解读,就是各种条件的判断,对 AST 的处理
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) { // v-pre 如果父节点有这个,子节点也是 静态的
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) { // 静态根节点
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) { // 当前节点是 template,而且 不是 slot,不是 v-pre
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // 当前节点是 slot
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) { // 当前节点是 组件
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 生成元素的属性/指令/事件等
// 处理各种指令,包括 module text html
data = genData(el, state)
}
//
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
不过 读到这里,可能会对 这里出现的 _c _l 等辅助函数感到奇怪,这个是在 vue 初始化的时候,注入进入当前实例的
_c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
export function installRenderHelpers (target: any) { // 给 vue 的实例注入 下面的方法
// 渲染相关,在 render 中执行
target._o = markOnce // .once
target._n = toNumber // 转换为 数字
target._s = toString // 转换为字符串
target._l = renderList // v-for
target._t = renderSlot // 渲染 slot,每个slot都是 一个 vnode ,获取对应的 slot 并渲染
target._q = looseEqual // 两个值是否大致相等,如果是对象的话,里面的属性是否相等
target._i = looseIndexOf // 传入一个数组,一个对象,如果 数组有大致相等的对象或者值,就返回第一个
target._m = renderStatic // 处理静态节点
target._f = resolveFilter // filter 过滤器
target._k = checkKeyCodes // keycode
target._b = bindObjectProps // 处理 props 以及 .sync 修饰符
target._v = createTextVNode // 创建文本节点
target._e = createEmptyVNode // 创建空白的 vnode 节点
target._u = resolveScopedSlots // 作用域插槽,就是在 把 slot.fn 拿出来,作为返回值
target._g = bindObjectListeners // 把绑定的 函数 放到 类似于 on: {click: []} 的数组中去
target._d = bindDynamicKeys // 处理 v-bind v-on 中的 动态属性绑定 :[key]="value"
target._p = prependModifier // 可以将修饰符运行时标记动态附加到事件名称
// 就是 将 click.capture 标记为 !click click.once 标记为 ~click click.passive 标记为 &click
}
6、createFunction 将上面的 字符串 js 转换为 可以执行的 js 函数
再贴一下 返回的 js
// 通过字符串的方法,new Function 转换为 一个函数
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
这里的 with(this) 传入的是当前的 vue 实例,作用看下图,就是 把 内部的 变量 指向 this
7、推荐几个网站
vue 2.0 将 template 转换为 vnode 的 render 函数
vue 3.0 将 template 转换为 vnode 的 render 函数