前奏

在紧张的一个星期的整理,笔者的前端小组每个人都整理了一篇文章,笔者整理了Vue编译模版到虚拟树的思想这一篇幅。建议读者看到这篇之前,先点击这里预习一下整个流程的思想和思路。

本文介绍的是Vue编译中的parse部分的源码分析,也就是从template 到 astElemnt的解析到程。

正文

从笔者的 Vue编译思想详解一文中,我们已经知道编译个四个流程分别为parse、optimize、code generate、render。具体细节这里不做赘述,附上之前的一张图。

本文则旨在从思想落实到源代码分析,当然只是针对parse这一部分的。

一、 源码结构。

笔者先列出我们在看源码之前,需要先预习的一些概念和准备。

准备

1.正则

parse的最终目标是生成具有众多舒心的astElement,而这些属性有很多则摘自标签的一些属性。 如 div上的v-for、v-if、v-bind等等,最终都会变成astElement的节点属性。 这里先给个例子:

<div v-for="(item,index) in options" :key="item.id"></div>

{alias: "item"attrsList: [],attrsMap: {"v-for": "(item,index) in options", :key: "item.id"},children: (2) [{…}, {…}],end: 139,for: "options",iterator1: "index",key: "item.id",parent: {type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},plain: false,rawAttrsMap: {v-for: {…}, :key: {…}},start: 15,tag: "div",type: 1,
}
复制代码

可以看到v-for的属性已经被解析和从摘除出来,存在于astElement的多个属性上面了。而摘除的这个功能就是出自于正则强大的力量。下面先列出一些重要的正则预热。


const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/  // 重要1
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要二
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND? /^v-|^@|^:|^\./: /^v-|^@|^:/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g  // 在v-for中去除 括号用的。
const dynamicArgRE = /^\[.*\]$/  // 判断是否为动态属性const argRE = /:(.*)$/ // 配置 :xxx
export const bindRE = /^:|^\.|^v-bind:/  // 匹配bind的数据,如果在组件上会放入prop里面  否则放在attr里面。
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/gconst slotRE = /^v-slot(:|$)|^#/const lineBreakRE = /[\r\n]/
const whitespaceRE = /\s+/gconst invalidAttributeRE = /[\s"'<>\/=]/
复制代码

正则基础不太好的同学可以先学两篇正则基础文章,特别详细:

    1. 轻松入门正则表达式
    1. 正则一条龙

并且附带上两个网站,供大家学习正则。

    1. 正则测试
    1. 正则图解

一次性看到这么多正则是不是有点头晕目眩。不要慌,这里给大家详细讲解下比较复杂多正则。

1)获取属性多正则

attribute 和 dynamicArgAttribute 分别获取普通属性和动态属性的正则表达式。 普通属性大家一定十分属性了,这里对动态属性做下解释。

动态属性,就是key值可能会发生变动对属性,vue对写法如 v-bind:[attrName]="attrVal" 相当于控制你想要传递对参数。

我们先对attribute做一个详细对讲解:

const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"])"+|'([^'])'+|([^\s"'=<>`]+)))?/ 一共分为五个分组:

  • 1.([^\s"'<>/=]+) 不匹配 空格、<、>、/、= 等符号。 因为我们配置对是属性。
  • 2.\s*(=)\s* 这个是 匹配 = 号,当然了空格页一并匹配了。
  • 3."([^"])" 、'([^'])' 、([^\s"'=<>`]+) . 这三个则分别匹配三种情况 "val" 、'val' 、val。

这样的话应该比较清晰了,我们来概括下:

attribute匹配的一共是三种情况, name="xxx" name='xxx' name=xxx。能够保证属性的所有情况都能包含进来。 需要注意的是正则处理后的数组的格式是:

['name','=','val','','']
或者
['name','=','','val','']
或者
['name','=','','','val']
复制代码

正则的图:

而关于dynamicArgAttribute, 则是大同小异:

主要是多了\[[^=]+\][^\s"'<>\/=]* 也就是 [name] 或者 [name]key 这类情况,附上正则详解图:

2)标签处理正则

标签主要包含开始标签 (如<div>)和结束标签(如</div>),正则分别为以下两个:

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
复制代码

能够看到标签的匹配是以qnameCapture为基础的,那么这玩意又是啥呢? 其实qname就是类似于xml:xxx的这类带冒号的标签,所以startTagOpen是匹配<div<xml:xxx的标签。 endTag匹配的是如</div>或</xml:xxx>的标签

3)处理vue的标签
export const onRE = /^@|^v-on:/ 处理绑定事件的正则
export const dirRE = process.env.VBIND_PROP_SHORTHAND? /^v-|^@|^:|^\./  // v-   | @click | :name | .stop  指令匹配: /^v-|^@|^:/
复制代码

for 标签比较重要,匹配也稍微复杂点,这里做个详解:

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
复制代码

首先申明这里的正则是依赖于attribute正则的,我们会拿到v-for里面的内容,举个例子v-for="item in options",我们最终会处理成一个map的形式,大致如下:

const element = {attrMap: {'v-for':'item in options',...}
}
复制代码

先看forAliasRE的分组,一共两个分组分别([\s\S]?)和([\s\S]) 会分别匹配 item 和 options。 但是 in或of之前内容可能是比较复杂的,如(value,key) 或者(item,index)等,这个时候就是forIteratorRE开始起作用了。 它一共两个分组都是([^,}]]*),其实就是拿到alias的最后两个参数,大家都知道对于Object我们是可以这么做的:

<div v-for="(value,key,index)">
复制代码

而数组则是为了获取key 和index的。最终会放在astElement的iterator1 和 iterator2。

好了关于正则就说这么多了,具体的情况还是得自己去看看源码的。

2.源码结构

依然是在开始讲源码前,先大致介绍下源码的结构。先贴个代码出来

function parse() {模块一:初始化需要的方法模块二: 初始化所有标记模块三: 开始识别并创建 astElement 树。
}
复制代码

模块一大致是:

  platformIsPreTag = options.isPreTag || no  //判断是否为 pre 标签platformMustUseProp = options.mustUseProp || no // 判断某个属性是否是某个标签的必要属性,如selected 对于optionplatformGetTagNamespace = options.getTagNamespace || no  // 判断是否为 svg or math标签 对函数const isReservedTag = options.isReservedTag || no // 判断是否为该平台对标签,目前vue源码只有 web 和weex两个平台。maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) //是否可能为组件transforms = pluckModuleFunction(options.modules, 'transformNode')  // 数组,成员是方法, 用途是摘取 staticStyle styleBinding staticClass classBindingpreTransforms = pluckModuleFunction(options.modules, 'preTransformNode') // ??postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') // ??delimiters = options.delimiters // express标志function closeElement() {...} // 处理astElement对结尾函数function trimEndingWhitespace() {...} // 处理尾部空格function checkRootConstraints() {...} // 检查root标签对合格性
复制代码

模块二大致为:

 const stack = [] // 配合使用的栈 主要目的是为了完成树状结构。let root // 根节点记录,树顶let currentParent // 当前父节点let inVPre = false // 标记是否在v-pre节点 当中let inPre = false // 是否在pre标签当中let warned = false
复制代码

模块三大致为:

parseHTML(template,options)
复制代码

options 是关键,包括很多平台配置和 传入的四个处理方法。大致如下:

options = {warn,expectHTML: options.expectHTML, // 是否期望和浏览器器保证一致。isUnaryTag: options.isUnaryTag, // 是否为一元标签的判断函数canBeLeftOpenTag: options.canBeLeftOpenTag, // 可以直接进行闭合的标签shouldDecodeNewlines: options.shouldDecodeNewlines,shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,shouldKeepComment: options.comments,outputSourceRange: options.outputSourceRange,// 这里分开,上面是平台配置、下面是处理函数。start, (1)end, (2)chars, (3)commend (4)
}
复制代码

笔者之前的parse思想,已经介绍过两个处理函数start和end了,一个是创建astElement另一个是建立父子关系,其中细节会在下文中,详细介绍,这也是本文的重点。切记这四个函数至关重要,下面会用代号讲解。

二、各模块重点功能。

Vue的html解析并非一步到位,先来介绍一些重点的函数功能

1.parseHTML函数功能。

(1)解析开始标签和 处理属性,生成初始化match。代码如下:

  /*** 创建match数据结构* 初始化的状态* 只有* tagName* attrs*    attrs自己是个数组 也就是 正则达到的效果。。* start* end*/function parseStartTag () {const start = html.match(startTagOpen)if (start) {const match = { // 匹配startTag的数据结构tagName: start[1],attrs: [],start: index}advance(start[0].length)let end, attr// 取属性值while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {attr.start = indexadvance(attr[0].length)attr.end = indexmatch.attrs.push(attr)}if (end) {match.unarySlash = end[1] // 是否为 一元标记 直接闭合advance(end[0].length)match.end = indexreturn match}}}
复制代码

parseStartTag的目标是比较原始的,获得类似于

const match = { // 匹配startTag的数据结构tagName: 'div',attrs: [{ 'id="xxx"','id','=','xxx' },...],start: index,end: xxx}
复制代码

match大致可以概括为获取标签、属性和位置信息。并将此传递给下个函数。

(2)handleStartTag处理parseStartTag传递过来的match。

 // parseStartTag 拿到的是 matchfunction handleStartTag (match) {const tagName = match.tagNameconst unarySlash = match.unarySlashif (expectHTML) { // 是否期望和浏览器的解析保持一致。if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag)}if (canBeLeftOpenTag(tagName) && lastTag === tagName) {parseEndTag(tagName)}}const unary = isUnaryTag(tagName) || !!unarySlash // 一元判断const l = match.attrs.lengthconst attrs = new Array(l)for (let i = 0; i < l; i++) { // 将attrs的 数组模式变成  { name:'xx',value:'xxx' }const args = match.attrs[i]const value = args[3] || args[4] || args[5] || ''const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'? options.shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesattrs[i] = {name: args[1],value: decodeAttr(value, shouldDecodeNewlines)}if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {attrs[i].start = args.start + args[0].match(/^\s*/).lengthattrs[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)}}复制代码

handleStartTag的本身效果其实非常简单直接,就是吧match的attrs重新处理,因为之前是数组结构,在这里他们将所有的数组式attr变成一个对象,流程大致如下:

从这样:

attrs: [{ 'id="xxx"','id','=','xxx' },...
],
复制代码

变成这样:

attrs: [{name='id',value='xxx' },...
],
复制代码

那么其实还有些特殊处理expectHTML一元标签

expectHTML 是为了处理一些异常情况。如 p标签的内部出现div等等、浏览器会特殊处理的情况,而Vue会尽量和浏览器保持一致。具体参考 p标签标准。

最后handleStartTag会调用 从parse传递的start(1)函数来做处理,start函数会在下文中有详细的讲解。

(3) parseEndTag

parseEndTag本身的功能特别简单就是直接调用options传递进来的end函数,但是我们观看源码的时候会发现源码还蛮长的。

function parseEndTag (tagName, start, end) {let pos, lowerCasedTagNameif (start == null) start = indexif (end == null) end = index// Find the closest opened tag of the same typeif (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 shoppos = 0}if (pos >= 0) {// Close all the open elements, up the stackfor (let i = stack.length - 1; i >= pos; i--) {if (process.env.NODE_ENV !== 'production' &&(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 stackstack.length = poslastTag = 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)}}}
}复制代码

看起来还蛮长的,其实主要都是去执行options.end, Vue的源码有很多的代码量都是在处理特殊情况,所以看起来很臃肿。这个函数的特殊情况主要有两种:

  • 1.编写者失误,有标签没有闭合。会直接一次性和检测的闭合标签一起进入options.end。 如:
    <div><span><p></div>
复制代码

在处理div的标签时,根据pos的位置,将pos之前的所有标签和匹配到的标签都会一起遍历的去执行end函数。

    1. p标签和br标签

可能会遇到</p></br>标签 这个时候 p标签会走跟浏览器自动补全效果,先start再end。 而br则是一元标签,直接进入end效果。

2.start、end、comment、chars四大函数。

1)start函数

start函数非常长。这里截取重点部分

start() {...let element: ASTElement = createASTElement(tag, attrs, currentParent)...if (!inVPre) {processPre(element)if (element.pre) {inVPre = true}}if (platformIsPreTag(element.tag)) {inPre = true}if (inVPre) {processRawAttrs(element)} else if (!element.processed) {// structural directivesprocessFor(element)processIf(element)processOnce(element)}if (!root) {root = elementif (process.env.NODE_ENV !== 'production') {checkRootConstraints(root)}}if (!unary) {currentParent = elementstack.push(element)} else {closeElement(element)}
}
复制代码
  • 1).创建astElement节点。

结构如下:

{type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),rawAttrsMap: {},parent,children: []}
复制代码
  • 2)处理属性 当然在这里只是处理部分属性,且分为两种情况:

    (1)pre模式 直接摘取所有属性

    (2)普通模式 分别处理processFor(element) 、processIf(element) 、 processOnce(element)。

2)end函数

end函数非常短

end (tag, start, end) {const element = stack[stack.length - 1]// pop stackstack.length -= 1currentParent = stack[stack.length - 1]if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {element.end = end}closeElement(element)},
复制代码

end函数第一件事就是取出当前栈的父元素赋值给currentParent,然后执行closeElement,为的就是能够创建完整的树节点关系。 所以closeElement才是end函数的重点。

下面详细解释下closeElement

function closeElement (element) {trimEndingWhitespace(element) // 去除 未部对空格元素if (!inVPre && !element.processed) {element = processElement(element, options) // 处理Vue相关对一些属性关系}// tree managementif (!stack.length && element !== root) {// allow root elements with v-if, v-else-if and v-elseif (root.if && (element.elseif || element.else)) {if (process.env.NODE_ENV !== 'production') {checkRootConstraints(element)}addIfCondition(root, { // 处理root到 条件展示exp: element.elseif,block: element})} else if (process.env.NODE_ENV !== 'production') {warnOnce(`Component template should contain exactly one root element. ` +`If you are using v-if on multiple elements, ` +`use v-else-if to chain them instead.`,{ start: element.start })}}if (currentParent && !element.forbidden) {if (element.elseif || element.else) { // 处理 elseif else 块级processIfConditions(element, currentParent)} else {if (element.slotScope) { // 处理slot, 将生成的各个slot的astElement 用对象展示出来。// scoped slot// keep it in the children list so that v-else(-if) conditions can// find it as the prev node.const name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element}currentParent.children.push(element)element.parent = currentParent}}// final children cleanup// filter out scoped slotselement.children = element.children.filter(c => !(c: any).slotScope)// remove trailing whitespace node againtrimEndingWhitespace(element)// check pre stateif (element.pre) {inVPre = false}if (platformIsPreTag(element.tag)) {inPre = false}// apply post-transformsfor (let i = 0; i < postTransforms.length; i++) {postTransforms[i](element, options)}}
复制代码

主要是做了五个操作:

  • 1.processElement。

processElement是closeElement非常重要的一个处理函数。先把代码贴出来。

export function processElement (element: ASTElement,options: CompilerOptions
) {processKey(element)// determine whether this is a plain element after// removing structural attributeselement.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。

processSlotContent是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和 slotTarget,也就是目标slot。 processSlotOutlet,则是简单的摘取 slot元素上面的name,并赋值给slotName。 processComponent 并不是处理component,而是摘取动态组件的is属性。 processAttrs是获取所有的属性和动态属性。

transforms是处理class和style的函数数组。这里不做赘述了。

  • 2.添加elseif 或else的block。

最终生成的的ifConditions块级的格式大致为:

[{exp:'showToast',block: castElement1},{exp:'showOther',block: castElement2},{exp: undefined,block: castElement3}
]
复制代码

这里会将条件展示处理成一个数组,exp存放所有的展示条件,如果是else 则为undefined。

  • 3.处理slot,将各个slot对号入座到一个对象scopedSlots。

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

  • 4.处理树到父子关系,element.parent = currentParent。
  • 5.postTransforms

不做具体介绍了,感兴趣的同学自己去研究下吧。

3)chars函数

chars(){...const children = currentParent.children...if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {child = {type: 2,expression: res.expression,tokens: res.tokens,text}} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {child = {type: 3,text}}
}
复制代码

chars主要处理两中文本情况,静态文本和表达式,举个例子:

<div>name</div>
复制代码

name就是静态文本,创建的type为3.

<div>{{name}}</div>
复制代码

而在这个里面name则是表达式,创建的节点type为2。

做个总结就是:普通tag的type为1,纯文本type为2,表达式type为3。

4)comment函数比较简单

comment (text: string, start, end) {// adding anyting as a sibling to the root node is forbidden// comments should still be allowed, but ignoredif (currentParent) {const child: ASTText = {type: 3,text,isComment: true}if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {child.start = startchild.end = end}currentParent.children.push(child)}}
复制代码

也是纯文本,只是节点加上了一个isComment:true的标志。

三、整体流程总结。

普通标签处理流程描述

  • 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的节点。

完结感言

时间仓促,希望多多支持。

转载于:https://juejin.im/post/5d01b954f265da1bbf69172e

Vue parse之 从template到astElement 源码详解相关推荐

  1. vue 源码详解(零):Vue 源码流程图

    vue 源码详解(零):Vue 源码流程图 最近在研究 Vue 的源码, 整理博客, 结果想到的.看到的内容实在是太多了, 不知道从何写起, 故整理了一个大致的流程图,根据这个顺序进行一一整理. 为了 ...

  2. Android编程之Intent源码详解

    Intent源码详解,直接开始入题: Intent源码6700多行代码,但真正核心代码 就那么几百行,大部分都用来定义常量字符串了 先来看一下 public class Intent implemen ...

  3. Vue-Watcher观察者源码详解

    源码调试地址 https://github.com/KingComedy/vue-debugger 什么是Watcher Watcher是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性 ...

  4. Spring事务源码详解

    一. 简介 事务: 事务是逻辑上的一组操作,要么都执行,要么都不执行,关于事务的基本知识可以看我的这篇文章:事务的基础知识 Spring事务: Spring 支持两种方式的事务管理:编程式事务管理.声 ...

  5. 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...

  6. 【Live555】live555源码详解系列笔记

    [Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...

  7. 【Live555】live555源码详解(八):testRTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的testRTSPClient实现的三个类所在的位置: ourRTSPClient.StreamClient ...

  8. 【Live555】live555源码详解(七):GenericMediaServer、RTSPServer、RTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: GenericMediaServer.RTSPServer.RTSPClient 14 ...

  9. 【Live555】live555源码详解(六):FramedSource、RTPSource、RTPSink

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: FramedSource.RTPSource.RTPSink 11.FramedSou ...

最新文章

  1. ASP.NET ViewState 初探
  2. 自学python还是报班-没有基础想学python为什么一定要报班?
  3. Hibernate的基本操作数据库,增加,修改,删除,查询
  4. 为什么要用RabbitMQ
  5. 纯CSS3文字Loading动画特效
  6. 七年级计算机室使用计划表,七年级信息技术教学工作计划
  7. CFileDialog
  8. orm mysql_PHP基于ORM方式操作MySQL数据库实例
  9. od找数据 遇到dll_OriginPro:最近比较烦,被360盯上了【数据绘图】
  10. 进程管理工具   htop
  11. RFGSD DF DS
  12. 小程序map地图多点定位
  13. ipad下载python_ipad python
  14. 如何修改电脑的ip地址
  15. 服务器使用笔记本网络连接外网
  16. java 修改exif_java – 操作图像而不删除其EXIF数据
  17. 基于redis实现活跃用户统计功能
  18. 炸⾦花棋牌游戏Python
  19. Qt如何自适应4k这些高分辨率屏幕
  20. Win10 笔记本 解决屏幕忽明忽暗,自动降低亮度问题

热门文章

  1. linux 如何下载svn插件安装,Linux SVN服务端安装和eclipse svn插件配置
  2. java throw与throws_基于Java中throw和throws的区别(详解)
  3. Vue2.0使用嵌套路由实现页面内容切换/公用一级菜单控制页面内容切换
  4. Package require os(darwin) not compatible with your platform(win32)
  5. 2014 Container技术大会:未来Linux Container会是PaaS平台的核心
  6. 旁瓣对消原理_雷达天线旁瓣对消技术
  7. for..in..遍历循环的简写模式
  8. Spring Boot集成CKFinder
  9. Bzoj1029 [JSOI2007]建筑抢修
  10. ClientDataSet建立索引和排序