在后面两节中,别离阐明了vuejs中如何申明Vue类以及vue数据响应式如何实现:

  1. Vue申明过程
  2. 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办法,该办法起源有两个:

  1. 用户自定义render。
  2. 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.jsinitRender函数中:

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有以下几种状况:

  1. tag是字符串而且是Dom中的元素,间接生成一般元素的Vnode。
  2. tag是字符串,然而属于组件($options.components),调用createComponent生成Vnode。
  3. 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的整个渲染过程能够总结为以下几步:

  1. vue调用$mount挂载Dom。
  2. 判断创立Vue实例时是否传入render办法,如果没传,那么将依据模版生成render函数。
  3. 创立Watcher,传入updateComponent函数。
  4. Watcher实例化时,会判断此Watcher是否是渲染Watcher,如果是,则调用updateComponent。
  5. updateComponent函数中会调用_render办法生成虚构Dom,调用_update办法依据传入的虚构Dom渲染实在Dom。
  6. 如果数据发生变化,会通过__ob__属性指向的Dep告诉第四步中创立的Watcher,Watcher外部会再次调用updateComponent执行更新渲染。

模版编译

只有在完整版的vuejs中才蕴含模版编译局部的代码,如果是通过vue-cli创立的我的项目,将没有此局部性能。

模版编译的过程蕴含如下几步:

  1. 解析Dom的ast语法树。
  2. 依据ast生成render字符串。
  3. 将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,该函数中将编译细化为三步:

  1. 生成ast。
  2. 优化ast
  3. 生成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次要是找到并标记动态根节点,一旦标记动态根节点,那么就会带来两个益处:

  1. 把它们变成常数,这样咱们就不须要了在每次从新渲染时为它们创立新的节点。
  2. 在patch过程中可能跳过这些动态根节点。

那么,什么是动态根节点呢?

动态根节点是指永远不会发生变化的Dom树,在Vuejs中,如果满足上面三个条件,就认为是动态根节点:

  1. 必须存在子节点。
  2. 如果子节点只有一个,该子节点不能是文本节点。
  3. 所有子节点都是动态节点(当数据发生变化的时候,节点不会产生扭转)。
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一些罕用实例办法的实现形式。