在后面两节中,别离阐明了 vuejs 中如何申明 Vue 类以及 vue 数据响应式如何实现:
- Vue 申明过程
- Vue 数据响应式实现过程
本节将探讨虚构 Dom 及模版解析过程。
虚构 Dom
vuejs 中的虚构 Dom 实现基于 snabbdom,在其根底上增加了组件等插件,对于 snabbdom 如何创立虚构 Dom 及 patch 比照更新过程能够参考 Snabbdom 实现原理。
_render
在 Vue 实例执行 $mount 办法挂载 Dom 的时候,在其外部执行了 mountComponent 函数。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
在 mountComponent 函数外部创立了 Watcher 对象(参考 Vue 数据响应式实现过程),当首次渲染和数据变动的时候会执行 updateComponent 函数。
updateComponent = () => {vm._update(vm._render(), hydrating)
}
该函数外部调用了 Vue 的实例办法_render 和_update。其中_render 办法的作用是生成虚构 Dom。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {render, _parentVnode} = vm.$options
// 拜访 slot 等占位节点
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
// 调用传入的 render 办法
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {// 处理错误} finally {currentRenderingInstance = null}
// ... 非凡状况判断
// 设置父子关系
vnode.parent = _parentVnode
return vnode
}
其实,通过代码能够看出,这个办法的次要作用是调用 $options 中的 render 办法,该办法起源有两个:
- 用户自定义 render。
- vue 依据模版生成的 render。
_renderProxy
通过 call 扭转了 render 的 this 指向,让其指向 vm._renderProxy
,_renderProxy 实例定义在core/instance/init.js
的 initMixin 中:
if (process.env.NODE_ENV !== 'production') {initProxy(vm)
} else {vm._renderProxy = vm}
通过代码能够看出,vm._renderProxy 指向的就是 vm。
$createElement
vm.$createElement 是 render 办法的第一个参数,也就是咱们开发过程中罕用的 h 函数,其定义在 core/instance/render.js
的initRender
函数中:
export function initRender(vm: Component) {vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
其调用的就是 /vdom/create-element.js
文件中的 createElement 函数,因为 vdom 文件夹下寄存的都是虚构 Dom 无关的操作。
createElement
createElement 用于生成 Vnodes:
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 解决参数,针对不同参数个数进行初始化解决
return _createElement(context, tag, data, children, normalizationType)
}
其外部调用_createElement 函数进行具体逻辑操作:
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 1. 判断传入参数是否符合要求,如果不合要求应该怎么解决
// ... 省略
if (normalizationType === ALWAYS_NORMALIZE) {children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {children = simpleNormalizeChildren(children)
}
// 2. 创立 vnode
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {vnode = createComponent(tag, data, context, children)
}
// 3. 对生成的 vnode 进行判断,如果不合要求,进行解决
// ...
}
创立 Vnodes 有以下几种状况:
- tag 是字符串而且是 Dom 中的元素,间接生成一般元素的 Vnode。
- tag 是字符串,然而属于组件($options.components),调用 createComponent 生成 Vnode。
- tag 是一个对象,那么默认该对象代表一个组件,调用 createComponent 生成 Vnode。
- createComponent
定义在 core/instance/vdom/create-component.js
文件中:
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 1. 应用 Vue.extend 将组件选项生成一个继承自 Vue 的组件类
const baseCtor = context.$options._base
if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor)
}
// 2. 解决组件中的非凡定义
// 3. 合并 patch 过程中应用到的生命周期 hook
installComponentHooks(data)
// 4. 依据后面生成的数据,调用 new VNode 生成虚构 Dom
const name = Ctor.options.name || tag
const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{Ctor, propsData, listeners, tag, children},
asyncFactory
)
return vnode
}
其中第三步合并生命周期 hook 函数在组件渲染挂载过程会被用到,这个在后续的 Vue.component 定义组件时持续探讨。
- new VNode
VNode 是一个类,蕴含一些形容该节点信息的实例成员,生成 Vnode 就是将一组数据组合成一个 VNode 实例。
_update
_update 办法的作用是将虚构 Dom 渲染到页面视图上:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevVnode = vm._vnode
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 数据更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
不论是首次渲染还是数据更新,其调用的都是__patch__办法。
__patch__
过程会操作 Dom,因而属于 web 平台上的特有操作,因而其定义在 platforms/web/runtime/index.js
中:
Vue.prototype.__patch__ = inBrowser ? patch : noop
其实现调用了 platforms/web/runtime/patch.js
中导出的 patch 函数。
patch
patch 过程和 snabbdom 的 patch 过程十分相近,只是针对 vuejs 非凡语法做了一些批改,此处不再具体阐明,能够参考 Snabbdom 实现原理。
总结
虚构 Dom 的整个渲染过程能够总结为以下几步:
- vue 调用 $mount 挂载 Dom。
- 判断创立 Vue 实例时是否传入 render 办法,如果没传,那么将依据模版生成 render 函数。
- 创立 Watcher,传入 updateComponent 函数。
- Watcher 实例化时,会判断此 Watcher 是否是渲染 Watcher,如果是,则调用 updateComponent。
- updateComponent 函数中会调用_render 办法生成虚构 Dom,调用_update 办法依据传入的虚构 Dom 渲染实在 Dom。
- 如果数据发生变化,会通过
__ob__
属性指向的 Dep 告诉第四步中创立的 Watcher,Watcher 外部会再次调用 updateComponent 执行更新渲染。
模版编译
只有在完整版的 vuejs 中才蕴含模版编译局部的代码,如果是通过 vue-cli 创立的我的项目,将没有此局部性能。
模版编译的过程蕴含如下几步:
- 解析 Dom 的 ast 语法树。
- 依据 ast 生成 render 字符串。
- 将 render 字符串转换为 render 函数。
编译入口
在 platforms/web/entry-runtime-with-compiler.js
文件中,会判断 $mount 时是否传入了 render 函数,如果没有传入,会依据模版编译 render 函数。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
// 编译 template 为 render 函数
const {render, staticRenderFns} = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// ...
}
其中,compileToFunctions 返回的 render 函数就是最终生成虚构 Dom 用的 render 函数,staticRenderFns 为动态树优化,用于优化 patch 性能。
- compileToFunctions
定义在 platforms/web/compiler/index.js
中:
const {compile, compileToFunctions} = createCompiler(baseOptions)
是由高阶函数 createCompiler 函数执行返回的。
- createCompiler
定义在 compilter/index.js
文件中:
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 1. 生成 ast
const ast = parse(template.trim(), options)
// 2. 针对 ast 进行优化
if (options.optimize !== false) {optimize(ast, options)
}
// 生成 render 函数
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
是调用 createCompilerCreator 生成的,在调用时传入了编译外围函数 baseCompile,该函数中将编译细化为三步:
- 生成 ast。
- 优化 ast
- 生成 render 函数
- createCompilerCreator
定义在 compiler/reate-compiler.js
文件中,
export function createCompilerCreator(baseCompile: Function): Function {return function createCompiler(baseOptions: CompilerOptions) {
// 编译函数:补充 options,调用 baseCompiler 编译
function compile(
template: string,
options?: CompilerOptions
): CompiledResult {
// 1. 合并用户 options 和默认 options,创立谬误和提醒数组对象。// 2. 调用 baseCompile 执行具体编译工作
const compiled = baseCompile(template.trim(), finalOptions)
// 3. 将编译过程中的谬误和提醒增加到编译后果上。return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
其外部扩大了编译函数,增加了默认配置和谬误收集,而后调用 createCompileToFunctionFn 生成最终的编译函数。
- createCompileToFunctionFn
定义在 compiler/to-function.js
中:
export function createCompileToFunctionFn(compile: Function): Function {
// 1. 增加编译后果缓存
const cache = Object.create(null)
return function compileToFunctions(
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 2. 依据编译失去的 render 字符串调用 new Function 生成编译函数
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {return createFunction(code, fnGenErrors)
})
return (cache[key] = res)
}
}
此办法持续扩大编译办法,提供了缓存和将 render 字符串转换成 render 函数性能。
ast 语法
生成 ast 语法树:
const ast = parse(template.trim(), options)
其本质是调用 parse 函数将 html 字符串转换为一般 js 对象形容的树结构数据,外部调用的是 http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
这个工具库,有趣味的能够本人看一下。
优化 ast
优化 ast 次要是找到并标记动态根节点,一旦标记动态根节点,那么就会带来两个益处:
- 把它们变成常数,这样咱们就不须要了在每次从新渲染时为它们创立新的节点。
- 在 patch 过程中可能跳过这些动态根节点。
那么,什么是动态根节点呢?
动态根节点是指永远不会发生变化的 Dom 树,在 Vuejs 中,如果满足上面三个条件,就认为是动态根节点:
- 必须存在子节点。
- 如果子节点只有一个,该子节点不能是文本节点。
- 所有子节点都是动态节点(当数据发生变化的时候,节点不会产生扭转)。
if (options.optimize !== false) {optimize(ast, options)
}
在编译的时候调用 optimize 函数执行具体的优化操作。
- optimize
定义在 compiler/optimize.js
文件中:
export function optimize(root: ?ASTElement, options: CompilerOptions) {
// ...
// 1. 找到并标记所有的动态节点
markStatic(root)
// 2. 找到并标记所有动态根节点
markStaticRoots(root, false)
}
- markStatic
function markStatic (node: ASTNode) {
// 1. 直接判断 node 节点是不是动态节点
node.static = isStatic(node)
if (node.type === 1) {
if (!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {return}
// 2. 遍历子节点,如果子节点其中一个为非动态节点,那么批改本节点为非动态节点。for (let i = 0, l = node.children.length; i < l; i++) {const child = node.children[i]
markStatic(child)
if (!child.static) {node.static = false}
}
// 3. if 节点的解决和第 2 步雷同
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}
}
}
}
}
如何判断是否是动态节点?
简略来讲,如果数据变动的时候,该节点会发生变化,那么此节点就不是动态节点,感兴趣的可自行查看 isStatic 外部实现。
- markStaticRoots
用于查找并标记所有的动态根节点,判断根据能够参考后面提到的如何判定一个节点是动态根节点。
function markStaticRoots(node: ASTNode, isInFor: boolean) {if (node.type === 1) {
// 节点被标记为动态节点,阐明所有子节点都为动态节点
if (node.static || node.once) {node.staticInFor = isInFor}
// 蕴含至多一个子节点,如果蕴含一个子节点,此节点不是文本节点
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 节点标记
if (node.ifConditions) {for (let i = 1, l = node.ifConditions.length; i < l; i++) {markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
那么优化的动态根节点在理论过程中如何应用呢?
以上面的模版为例:
<div id="app">
<span>
<strong> 文本 </strong>
</span>
<span>{{msg}}</span>
</div>
编译成 render 函数外部如下:
with (this) {
return _c('div',
{attrs: { "id": "app"} },
[_m(0),
_v(" "),
_c('span', [_v(_s(msg))])
])
}
在 render 函数中_m(0)返回的虚构 Dom 代表的就是动态根节点:
<span>
<strong> 文本 </strong>
</span>
动态根节点的 Vnode 后果缓存在 staticRenderFns
中,_m 函数就是依据元素索引去获取缓存的后果,这样每次调用_render 生成虚构 Dom 的时候就能够应用缓存,防止反复渲染。
生成 render 函数
首先生成 render 字符串
const code = generate(ast, options)
generate 外部依据 ast 生成 render 函数字符串:
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
}
}
而后在 createCompileToFunctionFn
函数中调用 createFunction
函数将 render 函数字符串转换为 render 函数:
function createFunction(code, errors) {
try {return new Function(code)
} catch (err) {errors.push({ err, code})
return noop
}
}
至此,整个渲染过程功败垂成。
下一节会探讨 vuejs 一些罕用实例办法的实现形式。