Vue(v2.6.11)万行源码生啃,就硬刚!(下)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。

第 7473 行至第 7697 行


  • normalizeEvents // 为事件 多添加 change 或者input 事件加进去
  • createOnceHandler$1
  • add$1 // 为真实的dom添加事件
  • remove$2
  • updateDOMListeners // 更新dom事件
  • updateDOMProps // 更新真实dom的props属性
  • shouldUpdateValue // 判断是否需要更新value
  • isNotInFocusAndDirty
  • isDirtyWithModifiers // 判断脏数据修改脏数据概念


第 7699 行至第 7797 行


  • domProps
  • parseStyleText // 把style 字符串 转换成对象
  • normalizeStyleData // 在同一个vnode上合并静态和动态样式数据
  • normalizeStyleBinding // 将可能的数组/字符串值规范化为对象
  • getStyle


/**
    * parent component style should be after child's
    * so that parent component's style could override it
    * 父组件样式应该在子组件样式之后
    * 这样父组件的样式就可以覆盖它
    * 循环子组件和组件的样式,把它全部合并到一个样式对象中返回 样式对象 如{width:100px,height:200px} 返回该字符串。
    */
  • setProp // 设置 prop


第 7799 行至第 7995 行


  • normalize  // 给css加前缀。解决浏览器兼用性问题,加前缀
  • updateStyle // 将vonde虚拟dom的css 转义成并且渲染到真实dom的csszhong
  • addClass // 为真实dom 元素添加class类
  • removeClass // 删除真实dom的css类名
  • resolveTransition // 解析vonde中的transition的name属性获取到一个css过度对象类
  • autoCssTransition // 通过 name 属性获取过渡 CSS 类名   比如标签上面定义name是 fade  css就要定义  .fade-enter-active,.fade-leave-active,.fade-enter,.fade-leave-to 这样的class
  • nextFrame // 下一帧


第 7997 行至第 8093 行


  • addTransitionClass // 获取 真实dom addTransitionClass 记录calss类
  • removeTransitionClass // 删除vonde的class类和删除真实dom的class类
  • whenTransitionEnds // 获取动画的信息,执行动画。
  • getTransitionInfo // 获取transition,或者animation 动画的类型,动画个数,动画执行时间


这一部分关于:对真实 dom 的操作,包括样式的增删、事件的增删、动画类等。

回过头再理一下宏观上的东西,再来亿遍-虚拟DOM:模板 → 渲染函数 → 虚拟DOM树 → 真实DOM


image.png


那么这一部分则处在“虚拟DOM树 → 真实DOM”这个阶段


第 8093 行至第 8518 行


  • getTimeout
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors
// 根据本地的依赖方式,Chromium 的旧版本(低于61.0.3163.100)格式化浮点数字,使用逗号而不是点。如果逗号未用点代替,则输入将被四舍五入而导致意外行为


// activeInstance will always be the <transition> component managing this
// transition. One edge case to check is when the <transition> is placed
// as the root node of a child component. In that case we need to check
// <transition>'s parent for appear check.
// activeInstance 将一直作为<transition>的组件来管理 transition。要检查的一种边缘情况:<transition> 作为子组件的根节点时。在这种情况下,我们需要检查 <transition> 的父项的展现。


  • leave // 离开动画
  • performLeave
  • checkDuration // only used in dev mode : 检测 val 必需是数字
  • isValidDuration
  • getHookArgumentsLength // 检测钩子函数 fns 的长度
  • _enter
  • createPatchFunction // path 把vonde 渲染成真实的dom:创建虚拟 dom - 函数体在 5845 行
  • directive // 生命指令:包括 插入 和 组件更新

更新指令 比较 oldVnode 和 vnode,根据oldVnode和vnode的情况 触发指令钩子函数bind,update,inserted,insert,componentUpdated,unbind钩子函数


此节前部分是 transition 动画相关工具函数,后部分关于虚拟 Dom patch、指令的更新。


第 8520 行至第 8584 行


  • setSelected // 设置选择 - 指令更新的工具函数
  • actuallySetSelected // 实际选择,在 setSelected() 里调用
  • hasNoMatchingOption // 没有匹配项 - 指令组件更新工具函数
  • getValue // 获取 option.value
  • onCompositionStart // 组成开始 - 指令插入工具函数
  • onCompositionEnd // 组成结束-指令插入工具函数:防止无故触发输入事件
  • trigger // 触发事件


第 8592 行至第 8728 行


// 定义在组件根内部递归搜索可能存在的 transition

  • locateNode
  • show // 控制 el 的 display 属性
  • platformDirectives // 平台指令
  • transitionProps // 过渡Props对象
// in case the child is also an abstract component, e.g. <keep-alive>
    // we want to recursively retrieve the real component to be rendered
    // 如果子对象也是抽象组件,例如<keep-alive>
    // 我们要递归地检索要渲染的实际组件
  • getRealChild
  • extractTransitionData // 提取 TransitionData
  • placeholder // 占位提示
  • hasParentTransition // 判断是否有 ParentTransition
  • isSameChild // 判断子对象是否相同


第 8730 行至第 9020 行


  • Transition // !important

前部分以及此部分大部分围绕 Transition 这个关键对象。即迎合官网 “过渡 & 动画” 这一节,是我们需要关注的重点!


Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库,如 Velocity.js

在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的管理过渡状态


vue - transition 里面大有东西,这里有一篇“细谈”推荐阅读。

  • props
  • TransitionGroup // TransitionGroup
  • callPendingCbs // Pending 回调
  • recordPosition // 记录位置
  • applyTranslation // 应用动画 - TransitionGroup.updated 调用


// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.
//我们将工作分为三个 loops,以避免将 DOM 读取和写入混合在一起
//在每次迭代中-有助于防止布局冲撞。


  • platformComponents // 平台组件


// 安装平台运行时指令和组件
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);


Q: vue自带的内置组件有什么?

A: Vue中内置的组件有以下几种:

  1. component


component组件:有两个属性---is inline-template

渲染一个‘元组件’为动态组件,按照'is'特性的值来渲染成那个组件

  1. transition


transition组件:为组件的载入和切换提供动画效果,具有非常强的可定制性,支持16个属性和12个事件

  1. transition-group


transition-group:作为多个元素/组件的过渡效果

  1. keep-alive


keep-alive:包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

  1. slot


slot:作为组件模板之中的内容分发插槽,slot元素自身将被替换


第 9024 行至第 9207 行


// install platform specific utils // 安装平台特定的工具


  • Vue.config.x
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;
复制代码
  • Vue.prototype.$mount // public mount method 安装方法 实例方法挂载 vm
// public mount method
Vue.prototype.$mount = function (
    el, // 真实dom 或者是 string
    hydrating //新的虚拟dom vonde
) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};


devtools global hook // 开发环境全局 hook Tip


  • buildRegex // 构建的正则匹配
  • parseText // 匹配view 指令,并且把他转换成 虚拟dom vonde 需要渲染的函数,比如指令{{name}}转换成 _s(name)
  • transformNode // 获取 class 属性和:class或者v-bind的动态属性值,并且转化成字符串 添加到staticClass和classBinding 属性中
  • genData // 初始化扩展指令 baseDirectives,on,bind,cloak方法,dataGenFns 获取到一个数组,数组中有两个函数 genData(转换 class) 和 genData$1(转换 style),
  • transformNode$1 // transformNode$1 获取 style属性和:style或者v-bind的动态属性值,并且转化成字符串 添加到staticStyle和styleBinding属性中
  • genData$1 // 参见 genData
  • style$1 // 包含 staticKeys、transformNode、genData 属性


第 9211 行至第 9537 行


  • he
  • isUnaryTag // 工具函数
  • canBeLeftOpenTag // 工具函数
  • isNonPhrasingTag // 工具函数Regular Expressions
  • parseHTML // 解析成 HTML !important


parseHTML 这个函数实现大概两百多行,是一个比较大的函数体了。

parseHTML 中的方法用于处理HTML开始和结束标签。

parseHTML 方法的整体逻辑是用正则判断各种情况,进行不同的处理。其中调用到了 options 中的自定义方法。


options 中的自定义方法用于处理AST语法树,最终返回出整个AST语法树对象。

贴一下源码,有兴趣可自行感受一二。附一篇详解Vue.js HTML解析细节学习


function parseHTML(html, options) {
    var stack = [];
    var expectHTML = options.expectHTML;
    var isUnaryTag$$1 = options.isUnaryTag || no;
    var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
    var index = 0;
    var last, lastTag;
    while (html) {
        last = html;
        // 确保我们不在像脚本/样式这样的纯文本内容元素中
        if (!lastTag || !isPlainTextElement(lastTag)) {
            var textEnd = html.indexOf('<');
            if (textEnd === 0) {
                // Comment:
                if (comment.test(html)) {
                    var commentEnd = html.indexOf('-->');
                    if (commentEnd >= 0) {
                        if (options.shouldKeepComment) {
                            options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
                        }
                        advance(commentEnd + 3);
                        continue
                    }
                }
                // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
                if (conditionalComment.test(html)) {
                    var conditionalEnd = html.indexOf(']>');
                    if (conditionalEnd >= 0) {
                        advance(conditionalEnd + 2);
                        continue
                    }
                }
                // Doctype:
                // 匹配 html 的头文件
                var doctypeMatch = html.match(doctype);
                if (doctypeMatch) {
                    advance(doctypeMatch[0].length);
                    continue
                }
                // End tag:
                var endTagMatch = html.match(endTag);
                if (endTagMatch) {
                    var curIndex = index;
                    advance(endTagMatch[0].length);
                    parseEndTag(endTagMatch[1], curIndex, index);
                    continue
                }
                // Start tag:
                // 解析开始标记
                var startTagMatch = parseStartTag();
                if (startTagMatch) {
                    handleStartTag(startTagMatch);
                    if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
                        advance(1);
                    }
                    continue
                }
            }
            var text = (void 0),
                rest = (void 0),
                next = (void 0);
            if (textEnd >= 0) {
                rest = html.slice(textEnd);
                while (
                    !endTag.test(rest) &&
                    !startTagOpen.test(rest) &&
                    !comment.test(rest) &&
                    !conditionalComment.test(rest)
                ) {
                    // < in plain text, be forgiving and treat it as text
                    next = rest.indexOf('<', 1);
                    if (next < 0) {
                        break
                    }
                    textEnd += next;
                    rest = html.slice(textEnd);
                }
                text = html.substring(0, textEnd);
            }
            if (textEnd < 0) {
                text = html;
            }
            if (text) {
                advance(text.length);
            }
            if (options.chars && text) {
                options.chars(text, index - text.length, index);
            }
        } else {
            //  处理是script,style,textarea
            var endTagLength = 0;
            var stackedTag = lastTag.toLowerCase();
            var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
            var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
                endTagLength = endTag.length;
                if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
                    text = text
                        .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
                        .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
                }
                if (shouldIgnoreFirstNewline(stackedTag, text)) {
                    text = text.slice(1);
                }
                if (options.chars) {
                    options.chars(text);
                }
                return ''
            });
            index += html.length - rest$1.length;
            html = rest$1;
            parseEndTag(stackedTag, index - endTagLength, index);
        }
        if (html === last) {
            options.chars && options.chars(html);
            if (!stack.length && options.warn) {
                options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), {
                    start: index + html.length
                });
            }
            break
        }
    }
    // Clean up any remaining tags
    parseEndTag();
    function advance(n) {
        index += n;
        html = html.substring(n);
    }
    function parseStartTag() {
        var start = html.match(startTagOpen);
        if (start) {
            var match = {
                tagName: start[1],
                attrs: [],
                start: index
            };
            advance(start[0].length);
            var end, attr;
            while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
                attr.start = index;
                advance(attr[0].length);
                attr.end = index;
                match.attrs.push(attr);
            }
            if (end) {
                match.unarySlash = end[1];
                advance(end[0].length);
                match.end = index;
                return match
            }
        }
    }
    function handleStartTag(match) {
        var tagName = match.tagName;
        var unarySlash = match.unarySlash;
        if (expectHTML) {
            if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
                parseEndTag(lastTag);
            }
            if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
                parseEndTag(tagName);
            }
        }
        var unary = isUnaryTag$$1(tagName) || !!unarySlash;
        var l = match.attrs.length;
        var attrs = new Array(l);
        for (var i = 0; i < l; i++) {
            var args = match.attrs[i];
            var value = args[3] || args[4] || args[5] || '';
            var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ?
                options.shouldDecodeNewlinesForHref :
                options.shouldDecodeNewlines;
            attrs[i] = {
                name: args[1],
                value: decodeAttr(value, shouldDecodeNewlines)
            };
            if (options.outputSourceRange) {
                attrs[i].start = args.start + args[0].match(/^\s*/).length;
                attrs[i].end = args.end;
            }
        }
        if (!unary) {
            stack.push({
                tag: tagName,
                lowerCasedTag: tagName.toLowerCase(),
                attrs: attrs,
                start: match.start,
                end: match.end
            });
            lastTag = tagName;
        }
        if (options.start) {
            options.start(tagName, attrs, unary, match.start, match.end);
        }
    }
    function parseEndTag(tagName, start, end) {
        var pos, lowerCasedTagName;
        if (start == null) {
            start = index;
        }
        if (end == null) {
            end = index;
        }
        // Find the closest opened tag of the same type
        if (tagName) {
            lowerCasedTagName = tagName.toLowerCase();
            for (pos = stack.length - 1; pos >= 0; pos--) {
                if (stack[pos].lowerCasedTag === lowerCasedTagName) {
                    break
                }
            }
        } else {
            // If no tag name is provided, clean shop
            pos = 0;
        }
        if (pos >= 0) {
            // Close all the open elements, up the stack
            for (var i = stack.length - 1; i >= pos; i--) {
                if (i > pos || !tagName &&
                    options.warn
                ) {
                    options.warn(
                        ("tag <" + (stack[i].tag) + "> has no matching end tag."), {
                            start: stack[i].start,
                            end: stack[i].end
                        }
                    );
                }
                if (options.end) {
                    options.end(stack[i].tag, start, end);
                }
            }
            // Remove the open elements from the stack
            stack.length = pos;
            lastTag = pos && stack[pos - 1].tag;
        } else if (lowerCasedTagName === 'br') {
            if (options.start) {
                options.start(tagName, [], true, start, end);
            }
        } else if (lowerCasedTagName === 'p') {
            if (options.start) {
                options.start(tagName, [], false, start, end);
            }
            if (options.end) {
                options.end(tagName, start, end);
            }
        }
    }
}


第 9541 行至第 9914 行


Regular Expressions // 相关正则

  • createASTElement // Convert HTML string to AST.
  • parse // !important


parse 函数从 9593 行至 9914 行,共三百多行。核心吗?当然核心!

引自 wikipedia:


在计算机科学和语言学中,语法分析(英语:syntactic analysis,也叫 parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。

语法分析器(parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成。

parse 的整体流程实际上就是先处理了一些传入的options,然后执行了parseHTML 函数,传入了template,options和相关钩子。

具体实现这里盗一个图:

image.png

parse中的语法分析可以看这一篇这一节

  1. start
  2. char
  3. comment
  4. end


parse、optimize、codegen的核心思想解读可以看这一篇这一节

这里实现的细节还真不少!


阶段小结(重点)


噫嘘唏!来到第 20 篇的小结!来个图镇一下先!


还记得官方这样的一句话吗?


下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。


看了这么多,我们再回头看看注释版。


image.png

link


上图值得一提的是:Has "template" option? 这个逻辑的细化


碰到是否有 template 选项时,会询问是否要对 template 进行编译:即模板通过编译生成 AST,再由 AST 生成 Vue 的渲染函数,渲染函数结合数据生成 Virtual DOM 树,对 Virtual DOM 进行 diff 和 patch 后生成新的UI。


如图(此图前文也有提到,见 0 至 5000 行总结):


image.png

将 Vue 的源码的“数据监听”、“虚拟 DOM”、“Render 函数”、“组件编译”、结合好,则算是融会贯通了!


一图胜万言

image.png

好好把上面的三张图看懂,便能做到“成竹在胸”,走遍天下的 VUE 原理面试都不用慌了。框架就在这里,细化的东西就需要多多记忆了!


第 9916 行至第 10435 行


🙌 到 1w 行了,自我庆祝一下!


  • processRawAttrs // parse 方法里用到的工具函数 用于将特性保存到AST对象的attrs属性上
  • processElement// parse 方法工具函数 元素填充


export function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  processKey(element)
  // determine whether this is a plain element after
  // removing structural attributes
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )
  processRef(element)
  processSlotContent(element)
  processSlotOutlet(element)
  processComponent(element)
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  processAttrs(element)
  return element
}


可以看到主要函数包括:processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs 和最后遍历执行的transforms。


processElement完成的slotTarget的赋值,这里则是将所有的slot创建的astElement以对象的形式赋值给currentParent的scopedSlots。以便后期组件内部实例话的时候可以方便去使用vm.?slot。


  • processKey
  • processRef
  1. 首先最为简单的是processKey和processRef,在这两个函数处理之前,我们的key属性和ref属性都是保存在astElement上面的attrs和attrsMap,经过这两个函数之后,attrs里面的key和ref会被干掉,变成astElement的直属属性。
  2. 探讨一下slot的处理方式,我们知道的是,slot的具体位置是在组件中定义的,而需要替换的内容又是组件外面嵌套的代码,Vue对这两块的处理是分开的。


先说组件内的属性摘取,主要是slot标签的name属性,这是processSlotOutLet完成的。


  • processFor
  • parseFor
  • processIf
  • processIfConditions
  • findPrevElement
  • addIfCondition
  • processOnce
  • processSlotContent
  • getSlotName
  • processSlotOutlet
// handle <slot/> outlets
function processSlotOutlet (el) {
  if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name') // 就是这一句了。
    if (process.env.NODE_ENV !== 'production' && el.key) {
      warn(
        `\`key\` does not work on <slot> because slots are abstract outlets ` +
        `and can possibly expand into multiple elements. ` +
        `Use the key on a wrapping element instead.`,
        getRawBindingAttr(el, 'key')
      )
    }
  }
}
// 其次是摘取需要替换的内容,也就是 processSlotContent,这是是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和 slotTarget,也就是目标slot。
  • processComponent // processComponent 并不是处理component,而是摘取动态组件的is属性。 processAttrs是获取所有的属性和动态属性。
  • processAttrs
  • checkInFor
  • parseModifiers
  • makeAttrsMap


这一部分仍是衔接这 parse function 里的具体实现:start、end、comment、chars四大函数。


流程再回顾一下:


一、普通标签处理流程描述

  1. 识别开始标签,生成匹配结构match。

const match = { // 匹配startTag的数据结构 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 复制代码 2. 处理attrs,将数组处理成 {name:'xxx',value:'xxx'} 3. 生成astElement,处理for,if和once的标签。 4. 识别结束标签,将没有闭合标签的元素一起处理。 5. 建立父子关系,最后再对astElement做所有跟Vue 属性相关对处理。slot、component等等。


二、文本或表达式的处理流程描述。


  1. 截取符号<之前的字符串,这里一定是所有的匹配规则都没有匹配上,只可能是文本了。
  2. 使用chars函数处理该字符串。
  3. 判断字符串是否含有delimiters,默认也就是${},有的话创建type为2的节点,否则type为3.


三、注释流程描述


  1. 匹配注释符号。
  2. 使用comment函数处理。
  3. 直接创建type为3的节点。

参考 link


阶段小结


parseHTML() 和 parse() 这两个函数占了很大的篇幅,值得重点去看看。的确也很多细节,一些正则的匹配,字符串的操作等。从宏观上把握从 template 到 vnode 的 parse 流程也无大问题。


第 10437 行至第 10605 行


  • isTextTag // function chars() 里的工具函数
  • isForbiddenTag //  function parseHTML() 用到的工具函数用于检查元素标签是否合法(不是保留命名)
  • guardIESVGBug // parse start() 中用到的工具函数
  • checkForAliasModel // checkForAliasModel用于检查v-model的参数是否是v-for的迭代对象
  • preTransformNode // preTransformNode 方法对el进行预处理,便于后续对标签上的指令和属性进行处理,然后进行树结构的构建,确定el的root, parent, children等属性。总结下来就是生成树节点,构建树结构(关联树节点)。
  • cloneASTElement // 转换属性,把数组属性转换成对象属性,返回对象 AST元素
  • text // 为虚拟dom添加textContent 属性
  • html // 为虚拟dom添加innerHTML 属性
  • baseOptions

var baseOptions = {
  expectHTML: true, //标志 是html
  modules: modules$1, //为虚拟dom添加staticClass,classBinding,staticStyle,styleBinding,for,
                      //alias,iterator1,iterator2,addRawAttr ,type ,key, ref,slotName
                      //或者slotScope或者slot,component或者inlineTemplate ,plain,if ,else,elseif 属性
  directives: directives$1, //根据判断虚拟dom的标签类型是什么?给相应的标签绑定 相应的 v-model 双数据绑定代码函数,
                            //为虚拟dom添加textContent 属性,为虚拟dom添加innerHTML 属性
  isPreTag: isPreTag, // 判断标签是否是 pre
  isUnaryTag: isUnaryTag, // 匹配标签是否是area,base,br,col,embed,frame,hr,img,input,
                          // isindex,keygen, link,meta,param,source,track,wbr
  mustUseProp: mustUseProp,
  canBeLeftOpenTag: canBeLeftOpenTag,// 判断标签是否是 colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source
  isReservedTag: isReservedTag, // 保留标签 判断是不是真的是 html 原有的标签 或者svg标签
  getTagNamespace: getTagNamespace, // 判断 tag 是否是svg或者math 标签
  staticKeys: genStaticKeys(modules$1) // 把数组对象 [{ staticKeys:1},{staticKeys:2},{staticKeys:3}]连接数组对象中的 staticKeys key值,连接成一个字符串 str=‘1,2,3’
};
  • genStaticKeysCached


第 10607 行至第 10731 行


/**
  * Goal of the optimizer: walk the generated template AST tree
  * and detect sub-trees that are purely static, i.e. parts of
  * the DOM that never needs to change.
  *
  * Once we detect these sub-trees, we can:
  *
  * 1. Hoist them into constants, so that we no longer need to
  *    create fresh nodes for them on each re-render;
  * 2. Completely skip them in the patching process.
  */
  // 优化器的目标:遍历生成的模板AST树检测纯静态的子树,即永远不需要更改的DOM。
  // 一旦我们检测到这些子树,我们可以:
  // 1。把它们变成常数,这样我们就不需要了
  // 在每次重新渲染时为它们创建新的节点;
  // 2。在修补过程中完全跳过它们。


  • optimize // !important:过 parse 过程后,会输出生成 AST 树,接下来需要对这颗树做优化。即这里的 optimize // 循环递归虚拟node,标记是不是静态节点 // 根据node.static或者 node.once 标记staticRoot的状态
  • genStaticKeys$1
  • markStatic$1 // 标准静态节点
  • markStaticRoots // 标注静态根(重要)
  • isStatic // isBuiltInTag(即tag为component 和slot)的节点不会被标注为静态节点,isPlatformReservedTag(即平台原生标签,web 端如 h1 、div标签等)也不会被标注为静态节点。
  • isDirectChildOfTemplateFor


阶段小结


简单来说:整个 optimize 的过程实际上就干 2 件事情,markStatic(root) 标记静态节点 ,markStaticRoots(root, false) 标记静态根节点。


那么被判断为静态根节点的条件是什么?


  1. 该节点的所有子孙节点都是静态节点(判断为静态节点要满足 7 个判断,详见
  2. 必须存在子节点
  3. 子节点不能只有一个纯文本节点


其实,markStaticRoots()方法针对的都是普通标签节点。表达式节点与纯文本节点都不在考虑范围内。


markStatic()得出的static属性,在该方法中用上了。将每个节点都判断了一遍static属性之后,就可以更快地确定静态根节点:通过判断对应节点是否是静态节点 且 内部有子元素 且 单一子节点的元素类型不是文本类型。


只有纯文本子节点时,他是静态节点,但不是静态根节点。静态根节点是 optimize 优化的条件,没有静态根节点,说明这部分不会被优化。


Q:为什么子节点的元素类型是静态文本类型,就会给 optimize 过程加大成本呢?

A:optimize 过程中做这个静态根节点的优化目是:在 patch 过程中,减少不必要的比对过程,加速更新。但是需要以下成本


  1. 维护静态模板的存储对象 一开始的时候,所有的静态根节点 都会被解析生成 VNode,并且被存在一个缓存对象中,就在 Vue.proto._staticTree 中。 随着静态根节点的增加,这个存储对象也会越来越大,那么占用的内存就会越来越多 势必要减少一些不必要的存储,所有只有纯文本的静态根节点就被排除了
  2. 多层render函数调用 这个过程涉及到实际操作更新的过程。在实际render 的过程中,针对静态节点的操作也需要调用对应的静态节点渲染函数,做一定的判断逻辑。这里需要一定的消耗。


纯文本直接对比即可,不进行 optimize 将会更高效。

参考link


第 10733 行至第 10915 行


// KeyboardEvent.keyCode aliases


  • keyCodes // 内置按键
  • keyNames
  • genGuard // genGuard = condition => if(${condition})return null;
  • modifierCode //m odifierCode生成内置修饰符的处理
  • genHandlers
  • genHandler // 调用genHandler处理events[name],events[name]可能是数组也可能是独立对象,取决于name是否有多个处理函数。
  • genKeyFilter // genKeyFilter用于生成一段过滤的字符串:
  • genFilterCode // 在 genKeyFilter 里被调用
  • on
  • bind$1
  • baseDirectives // CodegenState 里的工具函数


不管是组件还是普通标签,事件处理代码都在genData的过程中,和之前分析原生事件一致,genHandlers用来处理事件对象并拼接成字符串。


第 10921 行至第 11460 行


// generate(ast, options)


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
  }
}


  • CodegenState
  • generate // !important
  • genElement

export function genElement (el: ASTElement, 
state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  if (el.staticRoot && !el.staticProcessed) {
    // 如果是一个静态的树, 如 <div id="app">123</div>
    // 生成_m()方法
    // 静态的渲染函数被保存至staticRenderFns属性中
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    // v-once 转化为_o()方法
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    // _l()
    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,处理子节点
    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))) {
        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
  }
}
  • genStatic // genStatic会将ast转化为_m()方法
  • genOnce // 如果v-once在v-for中,那么就会生成_o()方法, 否则将其视为静态节点
  • genIf // genIf会将v-if转换为表达式,示例如下
  • genIfConditions
  • genFor // v-for会转换为_l()
  • genData$2
  • genDirectives // genData() 里调用
  • genInlineTemplate // genData() 里调用
  • genScopedSlots // genData() 里调用
  • genScopedSlot
  • genChildren // 处理子节点
  • getNormalizationType // 用于判断是否需要规范化
  • genNode // 处理 Node
  • genText // 处理 Text
  • genComment
  • genSlot // 处理插槽
  • genComponent // 处理组件
  • genProps // 处理 props
  • transformSpecialNewlines

这里面的逻辑、细节太多了,不做赘述,有兴趣了解的童鞋可以去看推荐阅读


阶段小结


generate方法内部逻辑还是很复杂的,但仅做了一件事情,就是将ast转化为render函数的字符串,形成一个嵌套结构的方法,模版编译生成的_c(),_m(),_l等等其实都是生成vnode的方法,在执行vue.$mount方法的时候,会调用vm._update(vm._render(), hydrating)方法,此时_render()中方法会执行生成的render()函数,执行后会生成vnode,也就是虚拟dom节点。


第 11466 行至第 11965 行


  • prohibitedKeywordRE // 正则校验:禁止关键字
  • unaryOperatorsRE // 正则校验:一元表达式操作
  • stripStringRE // 正则校验:脚本字符串
  • detectErrors // 检测错误工具函数
  • checkNode // 检查 Node
  • checkEvent // 检查 Event
  • checkFor // 检查 For 循环
  • checkIdentifier // 检查 Identifier
  • checkExpression // 检查表达式
  • checkFunctionParameterExpression // 检查函数表达式
  • generateCodeFrame
  • repeat$1
  • createFunction // 构建函数
  • createCompileToFunctionFn // 构建编译函数
  • compile // !important
return function createCompiler (baseOptions) {
function compile (
    template,
    options
) {
    var finalOptions = Object.create(baseOptions);
    var errors = [];
    var tips = [];
    var warn = function (msg, range, tip) {
    (tip ? tips : errors).push(msg);
    };
    if (options) {
    if (options.outputSourceRange) {
        // $flow-disable-line
        var leadingSpaceLength = template.match(/^\s*/)[0].length;
        warn = function (msg, range, tip) {
        var data = { msg: msg };
        if (range) {
            if (range.start != null) {
            data.start = range.start + leadingSpaceLength;
            }
            if (range.end != null) {
            data.end = range.end + leadingSpaceLength;
            }
        }
        (tip ? tips : errors).push(data);
        };
    }
    // 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
        );
    }
    // copy other options
    for (var key in options) {
        if (key !== 'modules' && key !== 'directives') {
        finalOptions[key] = options[key];
        }
    }
    }
    finalOptions.warn = warn;
    var compiled = baseCompile(template.trim(), finalOptions);
    {
    detectErrors(compiled.ast, warn);
    }
    compiled.errors = errors;
    compiled.tips = tips;
    return compiled
}


再看这张图,对于“模板编译”是不是有一种新的感觉了。


image.png


  • compileToFunctions

// 最后的最后

return Vue;

哇!历时一个月左右,我终于完成啦!!!

完结撒花🎉🎉🎉!激动 + 释然 + 感恩 + 小满足 + ...... ✿✿ヽ(°▽°)ノ✿

这生啃给我牙齿都啃酸了!!


总结



emmm,本来打算再多修补一下,但是看到 vue3 的源码解析已有版本出来啦(扶朕起来,朕还能学),时不我待,Vue3 奥利给,干就完了!

后续仍会完善此文,您的点赞是我最大的动力!也望大家不吝赐教,不吝赞美~

最最最最后,还是那句老话,与君共勉:


纸上得来终觉浅 绝知此事要躬行


相关文章
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
6天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
6天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
6天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
5天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
7天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
5天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
7天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
12天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发
|
12天前
|
存储 JavaScript
Vue 状态管理工具vuex
Vue 状态管理工具vuex