0.前情提要
从(一)小菜鸡的Vue源码阅读——new Vue()干了件啥,我们说入口文件是
platforms/web/entry-runtime.js
,但这其实是运行时的vue,其实vue整个还包含了compiler(编译部分),因为vue允许我们以模板字符串的形式来声明组件,但最终渲染还是得需要一个render渲染函数,vue编译器主要就是做了一个把模板字符串转换成render函数的功能。
在初始化过程中的调用$mount
挂载组件实例
Vue.prototype._init = function (options) {
···
// 初始化一堆东西
// 挂载组件实例
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
复制代码
流程图
1. vue编译器的入口文件
platforms/web/entry-runtime-with-compiler.js
重写vue.prototype.$mount
,template
有三种编写方式,针对这三种方式需要做一个统一的转换成模板字符串
- 字符串模板
var vm = new Vue({
el: '#app',
template: '<div>模板字符串</div>'
})
复制代码
- 选择符匹配元素的
innerHTML
模板
<div id="app">
<div>test1</div>
<script type="x-template" id="test"></script>
</div>
var vm = new Vue({
el: '#app',
template: '#test'
})
Copy
复制代码
dom
元素匹配元素的innerHTML
模板
<div id="app">
<div>test1</div>
<span id="test"><div class="test2">test2</div></span>
</div>
var vm = new Vue({
el: '#app',
template: document.querySelector('#test')
})
复制代码
总结一下
- 统一转换成模板字符串
- 生成
render
和staticRenderFns
函数 - 和运行时逻辑一样,调用原来的
$mount
函数挂载
// platforms/web/runtime/index.js
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// platforms/web/entry-runtime-with-compiler.js
// 这里的$mount 其实在 runtime/index已经声明了,那是不需要编译情况的函数,先把他用变量保存下来
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {
el = el && query(el)
// ...
const options = this.$options
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
// 选择符匹配
if (template.charAt(0) === '#') {
template = idToTemplate(template)
// ...
}
// DOM元素匹配
} else if (template.nodeType) {
template = template.innerHTML
} else {
// ...
return this
}
// 如果没传template就以el为根节点做模板
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
// ...
// 生成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// ...
}
}
// 调用原来的逻辑
return mount.call(this, el, hydrating)
}
Vue.compile = compileToFunctions
复制代码
2. render函数的生成
流程图
compileToFunctions
将模板字符串转换render函数
该函数有三个参数,分别是模板字符串,编译配置,vm实例
Vue.prototype.$mount = function () {
···
if(!options.render) {
var template = options.template;
if (template) {
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
// 纯文本插入分隔符
delimiters: options.delimiters,
// 是否保留模板中的注释
comments: options.comments
}, this);
var render = ref.render;
}
...
}
}
复制代码
接下来我们要去找compileToFunctions
这个函数是怎么来的
createCompiler
生成编译器
// platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
// 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)
}
// 修改过的ast再重新生成回代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
复制代码
createCompilerCreator
生成编译器的创建器
createCompilerCreator
函数只有一个作用,利用偏函数的思想将baseCompile
这一基础的编译方法缓存,并返回一个编程器生成器.
// 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 errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// ...
// merge custom modules
if (options.modules) {
finalOptions.modules =(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// 复制其他的属性
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
// 这里实际上是执行了真正的编译函数
const compiled = baseCompile(template.trim(), finalOptions)
// ...
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
复制代码
createCompileToFunctionFn
createCompileToFunctionFn
利用了闭包的概念,将编译过的模板进行缓存,cache
会将之前编译过的结果保留下来,利用缓存可以避免重复编译引起的浪费性能。createCompileToFunctionFn
最终会将compileToFunctions
方法返回。
// compiler/to-function.js
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (template,options,vm) {
options = extend({}, options);
···
// 缓存的作用:避免重复编译同个模板造成性能的浪费
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 执行编译方法
var compiled = compile(template, options);
···
// turn code into functions
var res = {};
var fnGenErrors = [];
// 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
···
return (cache[key] = res)
}
}
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
复制代码
总结
createCompilerCreator(baseCompiler)(baseOptions).compileToFunctions(template,options,vm)
复制代码
这里的编译逻辑理解非常难受,笔者主要参考借鉴了实例挂载流程和模板编译 · 深入剖析Vue源码 (penblog.cn),这里偏函数的思想指的学习,但是编译最主要的部分还是createCompilerCreator
函数中的parse
和generate
函数,但是这里过于复杂,笔者这里就不继续深究了,浅尝辄止。
3. 从render函数到VDOM
无论是用template
还是手写render
函数,最终都已经经过上面的步骤变成了render
渲染函数,现在我们要做的工作是将渲染函数转换成VDOM
,在将VDOM
转换成真实DOM
并挂载到页面上,回到最开始的$mount
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {
// ...
return mount.call(this, el, hydrating)
}
复制代码
mountComponent
beforeMount
和mounted
钩子函数的调用- 创建一个watcher实例,实例中更新的回调函数是
updateComponent
,后面继续看updateComponent
这个函数的声明与实现
// core/instance/lifecycle.js
export function mountComponent (vm: Component,el: ?Element,hydrating?: boolean): Component {
vm.$el = el
// ...
// 调用beforeMount钩子
callHook(vm, 'beforeMount')
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
// 挂载完毕,调用mounted钩子
callHook(vm, 'mounted')
}
return vm
}
复制代码
vm._render
回顾(一)小菜鸡的Vue源码阅读——new Vue()干了件啥中renderMixin
阶段声明了原型方法_render
,该函数主要工作是将render
函数转化为Virtual DOM
.
export function renderMixin (Vue: Class<Component>) {
// ...
Vue.prototype._render = function (): VNode {
// ...
try {
currentRenderingInstance = vm
// 生成虚拟DOM
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
vnode = vm._vnode
} finally {
currentRenderingInstance = null
}
// ...
return vnode
}
}
复制代码
可以看出上面函数中的关键语句是vnode = render.call(vm._renderProxy, vm.$createElement)
,关于这个函数调用的两个参数做解释
vm.renderProxy
这个变量实际是在初始化实例的_init中添加的
Vue.prototype._init = function (options?: Object) {
// ...
if (process.env.NODE_ENV !== 'production') {
// 开发环境下数据过滤检测
initProxy(vm)
} else {
// 生产就是vue实例本身
vm._renderProxy = vm
}
// ...
}
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
复制代码
vm.$createElement
这个其实也是在初始化实例中_init
的initRender
中添加的$c
和$createElement
只有最后一个参数的区别
$c
: 内部调用,在通过模板字符串转换的render函数$createElement
: 自己手写render函数的时候作为参数传入
function initRender(vm) {
// ...
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); };
// ...
}
// core/vdom/create-element.js
function createElement (
context, // vm 实例
tag, // 标签
data, // 节点相关数据,属性
children, // 子节点
normalizationType,
alwaysNormalize // 区分内部编译生成的render还是手写render
) {
// 对传入参数做处理,如果没有data,则将第三个参数作为第四个参数使用,往上类推。
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
// 根据是alwaysNormalize 区分是内部编译使用的,还是用户手写render使用的
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
// 真正生成Vnode的方法
return _createElement(context, tag, data, children, normalizationType)
}
复制代码
_createELement
对传入的数据做校验,这些校验保证后续VDOM的生成
- 数据对象
data
不能是响应式数据 tag
用is
做动态的时候需要做特殊处理key
值必须是原始数据类型- 原生
DOM
上不应该用native
修饰符 - ...还有一些不做全部解读,意义好像也不是很大
根据render
是由用户输入还是由系统生成分成两个函数来转换children
normalizeChildren
: 用户自己输入,对文字节点的合并,递归调用simpleNormalizeChildren
: 由系统生成,主要是做一个数组的一级扁平化
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 1. 数据对象不能是定义在Vue data属性中的响应式数据。
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 2.tag用is做动态的时候需要做特殊处理
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
return createEmptyVNode()
}
// 3.key 值只能是原始数据类型
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// children的第一个元素是函数类型是当做默认插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 这个地方就是之前说的,自己写的render和框架生成的render不一样的地方
// 自己手写的数据结构可能会不一致,出现children是简单数据类型的情况,需要生成文本虚拟DOM
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// ...
}
// core/vdom/helpers/normalize-children.js
// 处理编译生成的render 函数
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
// 子节点为数组时,进行开平操作,压成一维数组。
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
// 处理用户定义的render函数
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
// 生成文字节点
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
// 遍历子节点
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// 当前子节点是数组
if (Array.isArray(c)) {
if (c.length > 0) {
// 对children递归
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// 当前遍历的子节点的第一个子节点如果是文字节点,而且当前最后一个字节点也是文字节点的,就将他们两个合并,并且剔除子节点的第一个字节点
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
// 当前子节点是简单数据类型
} else if (isPrimitive(c)) {
// 继续合并
if (isTextNode(last)) {
// 最后一个是文字节点就去和最后一个合并
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// 不是文字节点,那就把当前的文字创建一个文字节点,插到最后面去
res.push(createTextVNode(c))
}
} else {
// 当前遍历的就是一个文字节点,且最后也是文字节点,那么合并
if (isTextNode(c) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// key值不存在的时候,且是列表的时候加key值
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
复制代码
4. 从VDOM映射到真实DOM
updateComponent = function () {
// render生成虚拟DOM,update渲染真实DOM
vm._update(vm._render(), hydrating);
};
复制代码
vm._update
改方法在lifecycleMixin()
的时候被添加
function lifecycleMixin() {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
// prevVnode为旧vnode节点
var prevVnode = vm._vnode;
// 通过是否有旧节点判断是初次渲染还是数据更新
// 初次渲染
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// 数据更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
}
复制代码
__patch__
reatePatchFunction
方法传递一个对象作为参数,对象拥有两个属性,nodeOps
和modules
,nodeOps
封装了一系列操作原生DOM
对象的方法。而modules
定义了模块的钩子函createPatchFunction
函数有一千多行这里就不列出来了,说一下大概干了件什么事它的内部首先定义了一系列辅助的方法,而核心是通过调用createElm
方法进行dom
操作,创建节点,插入子节点,递归创建一个完整的DOM
树并插入到Body
中。并且在产生真实阶段阶段,会有diff
算法来判断前后Vnode
的差异,以求最小化改变真实阶段。后面会有一个章节的内容去讲解diff
算法。createPatchFunction
的过程只需要先记住一些结论,函数内部会调用封装好的DOM api
,根据Virtual DOM
的结果去生成真实的节点。其中如果遇到组件Vnode
时,会递归调用子组件的挂载过程
数。
// platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
// platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
// 将操作dom对象的方法合集做冻结操作
var nodeOps = /*#__PURE__*/Object.freeze({
createElement: createElement$1,
createElementNS: createElementNS,
createTextNode: createTextNode,
createComment: createComment,
insertBefore: insertBefore,
removeChild: removeChild,
appendChild: appendChild,
parentNode: parentNode,
nextSibling: nextSibling,
tagName: tagName,
setTextContent: setTextContent,
setStyleScope: setStyleScope
});
// 定义了模块的钩子函数
var platformModules = [
attrs,
klass,
events,
domProps,
style,
transition
];
var modules = platformModules.concat(baseModules);
复制代码
复制代码