小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

Vue源码浅析(三)-render函数

render函数涉及到前端模板的原理和实现,所以开场前隆重推荐司徒正美的一篇文章:

前端模板的原理与实现

这是一篇由浅入深讲解前端模板的文章,值得仔细品尝。

================================

template的获取

在Vue开发过程中,可以不使用template,直接写render函数,如:

new Vue({
    render: function () {
        return this._h('div', {
            attrs:{
                a: 'aaa'
            }
        }, [
           this._h('div')
        ])
    }
})

当然这样是非常不直观,写起来肯定很不痛快,因此比较好的方式是写template,然后将它编译成render函数,实例如下:

new Vue({
    template: "<div class=\"aa\">Hello World! </div>"
})

这样写起来就舒服很多。Vue也提供了.vue文件格式来帮助开发者更方便的书写,其最终也是先转换为template,再转换为render函数。

Vue会根据开发者的需求编译出不同的版本,这本质是根据使用不同render函数输出不同的版本,代码体现在$mount函数的差异性,我们可以在entity目录查看各种方案的差异,本次源码分析关注的是web-runtime-with-compiler.js

// 先保存原本的$mount实现,再装饰独有的功能
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  // el 不能是body或html...
  const options = this.$options
  // 使用者如果自己写了render函数,那就不走编译环节
  if (!options.render) {
    let template = options.template
    if (template) {
      // 提供了template属性
      if (typeof template === 'string') {
        // template传值可以是id或字符串
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        // template传值可以是node
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 如果没有template,有el,则取el的outerHTML
      template = getOuterHTML(el)
    }
    if (template) {
      // 编译成render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        warn,
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      // 将赋值到options的render属性上
      options.render = render
      // staticRenderFns是为了优化,提取那些后期不用去更新的节点
      options.staticRenderFns = staticRenderFns
    }
  }
  // 调用原本的$mount实现
  return mount.call(this, el, hydrating)
}

这一步也就是在原本的$mount前加上了获取template和编译成render函数这两个功能。编译功能被单独抽取到compileToFunctions方法中去了。

render函数编译流程

先说一下render函数的编译的主要几个步骤,这可以帮助我们从整体上把握它:

  1. 给编译options添加web平台特性
  2. 将template字符串解析成ast
  3. 优化:将那些不会被改变的节点(statics)打上标记
  4. 生成render函数字符串,并用with包裹(最新版本有改为buble)
  5. 通过new Function的方式生成render函数并缓存

options添加平台特性

compileToFunctions位于platforms/web下

export function compileToFunctions (
  template: string,
  options?: CompilerOptions,
  vm?: Component
): CompiledFunctionResult {
  //...
  // 有缓存的话就直接在缓存里面拿
  const key = options && options.delimiters
    ? String(options.delimiters) + template
    : template
  if (cache[key]) {
    return cache[key]
  }
  const res = {}
  // 第1,2,3,4步
  const compiled = compile(template, options)
  // 第5步
  res.render = makeFunction(compiled.render)
  const l = compiled.staticRenderFns.length
  res.staticRenderFns = new Array(l)
  for (let i = 0; i < l; i++) {
    res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
  }
  //...
  // 添加到缓存
  return (cache[key] = res)
}

接下来的compile

// 这是web平台特性下需要给compile添加的options
export const baseOptions: CompilerOptions = {
  isIE,
  expectHTML: true,
  modules, // web平台才有的module, 这个用于virtual dom
  staticKeys: genStaticKeys(modules),
  directives,  // web平台才有的指令
  isReservedTag, // 保留节点
  isUnaryTag, // 自闭和节点
  mustUseProp, // 必须用固有属性来做绑定
  getTagNamespace, // tag的命名空间
  isPreTag
}

export function compile (
  template: string,
  options?: CompilerOptions
): CompiledResult {
  // merge传入的option的baseOptions
  options = options
    ? extend(extend({}, baseOptions), options)
    : baseOptions
  return baseCompile(template, options)
}

处理完options之后才进入真正的compile函数,在compile目录中

export function compile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 第2步,ast解析
  const ast = parse(template.trim(), options)
  // 第3步, 优化
  optimize(ast, options)
  // 第4步, 拼装render函数代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

ast解析

ast全称abstract syntax tree,是将template解析成一颗树状结构。这个树就是所谓的virtual dom,每个节点被命名为ASTElement,借助flow,还是很容易知道这个element具体有些什么的。具体执行解析的是parseHTML函数,这个函数取自John Resig的HTML Parse并加以修改。parseHTML的使用如下:

parseHTML(template, {
    start (tag, attrs, unary){
        // 节点开始
    },
    end () {
       // 节点结束
    },
    chars (text: string) {
       // 文本节点
    }
})

花点时间读懂这个函数对我们的编程能力提升会有很大帮助的,在这之前,我们先介绍几个只是点或者浏览器的bug:

isNonPhrasingTag

某一些标签,如address,article,aside,base,他们不能被p标签包裹,因此我们在遇到这些标签时需要小心处理,作者把这些标签全都放入到了isNonPhrasingTag这个map对象中。

canBeLeftOpenTag

像colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source这些节点,不会直接包裹同类型的节点,即<td><td>...</td></td>是错误的,所以我们对于这类节点,我们遇到相同类型tag时,应该结束上一个tag,即<td>xxx<td>xxx</td>应该被解析为<td>xxx</td><td>xxx</td>

* shouldDecodeNewlines *

这是IE上的一个bug, 如果dom节点的属性分多行书写,那么它会把'\n'转义成&#10;,而其它浏览器并不会这么做,因此需要手工处理。

function shouldDecode (content, encoded) {
  var div = document.createElement('div');
  // 传入我们想判断的内容
  div.innerHTML = "<div a=\"" + content + "\">";
  // 取出innerHTML,看它会不会被转义
  return div.innerHTML.indexOf(encoded) > 0
}

var shouldDecodeNewlines = inBrowser ? shouldDecode('\n', '&#10;') : false;

* IS_REGEX_CAPTURING_BROKEN *

这是火狐浏览器关于正则的一个bug,详情可以见https://bugzilla.mozilla.org/show_bug.cgi?id=369778, 不过个人对正则的实现并不了解,所以读完后也不知所云,这里暂且把Vue的识别代码给贴上来:

let IS_REGEX_CAPTURING_BROKEN = false
'x'.replace(/x(.)?/g, function (m, g) {
  IS_REGEX_CAPTURING_BROKEN = g === ''
})

然后就可以去解读parseHTML源码了,其会遍历整个字符串,动过正则匹配出comment、doctype、end tag、start tag、 char。当然,对于style和script需要单独处理。 源代码还对conditionalComment进行处理,不过好像对于目前的使用场景太小,我就忽略对它的分析了。

对于comment、doctype, 直接丢掉并调用advance函数调整当前index,advance的实现为下:

function advance (n) {
  index += n
  html = html.substring(n)
}

对于start tag,调用parseStartTag进行处理:

// 属性解析的正则
const singleAttrIdentifier = /([^\s"'<>/=]+)/
const singleAttrAssign = /(?:=)/
const singleAttrValues = [
  // attr value double quotes
  /"([^"]*)"+/.source,
  // attr value, single quotes
  /'([^']*)'+/.source,
  // attr value, no quotes
  /([^\s"'=<>`]+)/.source
]
const attribute = new RegExp(
  '^\\s*' + singleAttrIdentifier.source +
  '(?:\\s*(' + singleAttrAssign.source + ')' +
  '\\s*(?:' + singleAttrValues.join('|') + '))?'
)
// tag名正则
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')'
// 标签解析
const startTagOpen = new RegExp('^<' + qnameCapture)
const startTagClose = /^\s*(\/?)>/
function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      // 匹配start tag的开始
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      // 匹配属性
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      // 匹配start tag的结束
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
}

上述只是对start tag的匹配,我们通过上面的解析,可以获得一个包含tag名、属性、是否自闭和标签、开始index等信息的对象, 我们还需要对齐进行处理:

function handleStartTag (match) {
    const tagName = match.tagName
    let unarySlash = match.unarySlash

    if (expectHTML) {
      // 如果tag不能被p包裹,但模板中又在p内,那么就先结束p标签
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag('', lastTag)
      }
      // canBeLeftOpenTag类型节点处理
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag('', tagName)
      }
    }

    // 是否是自闭和标签
    const unary = isUnaryTag(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash

    // 属性处理
    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
        if (args[3] === '') { delete args[3] }
        if (args[4] === '') { delete args[4] }
        if (args[5] === '') { delete args[5] }
      }
      const value = args[3] || args[4] || args[5] || ''
      attrs[i] = {
        name: args[1],
        value: decodeAttr(
          value,
          options.shouldDecodeNewlines
        )
      }
    }
    // 因为我们的parse必定是深度优先遍历,所以我们可以用一个stack来保存还没闭合的标签的父子关系,并且标签结束时一个个pop出来就可以了
    if (!unary) {
      stack.push({ tag: tagName, attrs: attrs })
      lastTag = tagName
      unarySlash = ''
    }

    // 将解析结果交给使用这处理
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
}

对于end tag:

function parseEndTag (tag, tagName, start, end) {
    let pos
    if (start == null) start = index
    if (end == null) end = index

    // 从stack中逆序找到最近的相同tagName的元素,不能简单pop,因为模板不一定能保证都是节点闭合的。
    if (tagName) {
      const needle = tagName.toLowerCase()
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].tag.toLowerCase() === needle) {
          break
        }
      }
    } else {
      pos = 0
    }

    if (pos >= 0) {
      // 如果在stack中存在相同name的元素,则闭合element内部所有的open elements,然后更新stack、lastTag信息
      for (let i = stack.length - 1; i >= pos; i--) {
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
    } else if (tagName.toLowerCase() === 'br') {
      // 如果是br,则属于自闭和元素,交给使用这用start回调处理
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (tagName.toLowerCase() === 'p') {
      // 如果是p标签, 但是在stack中并不存在open tag,则创造一个start tag,然后在闭合它
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }

对于style和script:

var stackedTag = lastTag.toLowerCase()
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
var endTagLength = 0
var rest = html.replace(reStackedTag, function (all, text, endTag) {
    endTagLength = endTag.length
    if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
      text = text
        .replace(/<!--([\s\S]*?)-->/g, '$1')
        .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
    }
    // 会以char的形式交给外界调用
    if (options.chars) {
      options.chars(text)
    }
    return ''
})
index += html.length - rest.length
html = rest
parseEndTag('</' + stackedTag + '>', stackedTag, index - endTagLength, index)

对于text,就会直接交给外界使用,这里就不贴代码了。

现在parseHTML的各种节点的解析已经说清楚了,那么其主体函数就是负责遍历字符串,然后识别各种节点了。

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  let index = 0
  let last, lastTag
  // 其实质是对字符串的循环,然后解析字符
  while (html) {
    last = html
    // 确保不再script或者style内
    if (!lastTag || !isSpecialTag(lastTag, options.sfc, stack)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment
        if (comment.test(html)) {
          //...
        }
        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          //...
        }

        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          // ...
        }

        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          // ...
        }
      }
      // text
      // ...
    } else {
      // style、script处理
    }

    if (html === last && options.chars) {
      // template最后的text处理
    }
}

了解了parseHEML源码后,我们就来看看Vue是如何利用parseHEML来解析template了。我们分开start、end、char三个回调来看:

start (tag, attrs, unary) {
  // 命名空间, 具有继承性
  const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
  // handle IE svg bug
  // ...

  // 虚拟的ASTElement
  const element: ASTElement = {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs, options.isIE),
    parent: currentParent,
    children: []
  }
  if (ns) {
    element.ns = ns
  }
  //v-pre相当于avalon的skip,即在它控制的作用域下不编译指令或者表达式
  if (!inVPre) {
    // 解析v-pre
    processPre(element)
    if (element.pre) {
      inVPre = true
    }
  }
  // 可以根据平台定义pre 标签,如web端添加了<pre>标签
  if (platformIsPreTag(element.tag)) {
    inPre = true
  }
  if (inVPre) {
    // 在v-pre作用范围内,简单的处理原始信息就好了
    processRawAttrs(element)
  } else {
    // 处理各个指令
    processFor(element)
    processIf(element)
    processOnce(element)
    processKey(element)

    // determine whether this is a plain element after
    // removing structural attributes
    element.plain = !element.key && !attrs.length

    processRef(element)
    processSlot(element)
    processComponent(element)
    processAttrs(element)
  }

  if (!root) {
    // 根节点,需要检查是否能作为根节点
    root = element
    checkRootConstraints(root)
  } else if (!stack.length) {
    // 允许v-if和v-else组成的两个root元素:
    if (root.if && element.else) {
      checkRootConstraints(element)
      root.elseBlock = element
    } else if (process.env.NODE_ENV !== 'production' && !warned) {
      warned = true
      warn(
        `Component template should contain exactly one root element:\n\n${template}`
      )
    }
  }
  // 构建父子关系
  if (currentParent && !element.forbidden) {
    if (element.else) {
      processElse(element, currentParent)
    } else {
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }
  // 如果不是自闭和,已经要用stack来周转,与parseHTML内部一样
  if (!unary) {
    currentParent = element
    stack.push(element)
  }
}

在start回调中,我们会构造一个ASTElement,然后解析回调出来的属性,得到其绑定关系并放到ASTElement中, 我们以processFor为例:

// v-for的正则:支持v-for="* in list"和v-for="* of list"的形式
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
// v-for支持的多参数形式:可以有至多三个参数
export const forIteratorRE = /\(([^,]*),([^,]*)(?:,([^,]*))?\)/
function processFor (el) {
  let exp
  // 获取其v-for指令的内容
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const inMatch = exp.match(forAliasRE)
    if (!inMatch) {
      // warn...
      return
    }
    // 匹配内容
    el.for = inMatch[2].trim()
    const alias = inMatch[1].trim()
    const iteratorMatch = alias.match(forIteratorRE)
    // 多参数情况的处理,分别放到el.alias、el.iterator1、el.iterator2
    if (iteratorMatch) {
      el.alias = iteratorMatch[1].trim()
      el.iterator1 = iteratorMatch[2].trim()
      if (iteratorMatch[3]) {
        el.iterator2 = iteratorMatch[3].trim()
      }
    } else {
      // 但参数
      el.alias = alias
    }
  }
}

例外需要提到的一点是,如果一个el没有key绑定,也没有其他attr,那么这个el会加上plain的标志,这个可以用于后续优化。

看完了start,那让我们来看看end:

end () {
  //第一步: 移除内容为空的文本节点
  const element = stack[stack.length - 1]
  const lastNode = element.children[element.children.length - 1]
  if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
    element.children.pop()
  }
  // 第二步: pop stack
  stack.length -= 1
  currentParent = stack[stack.length - 1]
  // 第三步: 如果是v-pre控制节点,那么需要将inVPre重置为false
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
}

最后是文本节点char的处理:

chars (text: string) {
  if (!currentParent) {
    // 需要父节点,warn...
    return
  }
  // decode
  text = inPre || text.trim()
    ? decodeHTMLCached(text)
    // only preserve whitespace if its not right after a starting tag
    : preserveWhitespace && currentParent.children.length ? ' ' : ''
  if (text) {
    let expression
    // 解析, 提取expression
    if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
      // 如果有表达式,则设置节点type为2
      currentParent.children.push({
        type: 2,
        expression,
        text
      })
    } else {
      // 如果没有表达式,则type为3,用于优化
      text = text.replace(specialNewlineRE, '')
      currentParent.children.push({
        type: 3,
        text
      })
    }
  }
}

如果是文本,会涉及到表达式的解析:

export function parseText (
  text: string,
  delimiters?: [string, string] //界定符,默认是{{ 和 }}
): string | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) { // 没有表达式
    return
  }
  const tokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index
  while ((match = tagRE.exec(text))) {
    index = match.index
    // 表达式前面的文本
    if (index > lastIndex) {
      tokens.push(JSON.stringify(text.slice(lastIndex, index)))
    }
    // 表达式解析
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    lastIndex = index + match[0].length
  }
  // 表达式后的文本
  if (lastIndex < text.length) {
    tokens.push(JSON.stringify(text.slice(lastIndex)))
  }
  return tokens.join('+')
}

其中表达式解析还会涉及filter的解析,有兴趣的可以去读读,这里就不贴代码了。

经过非常复杂的html解析,模板终于被解析成一颗AST树了,但还没有结束,因为我们的AST树种会有很多界定诞生后就不会改变了,我们需要进行一些优化。

ast优化

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  // staticKeys 是那些认为不会被更改的ast的属性
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || (() => false)
  // 第一步:标记static节点
  markStatic(root)
  // 第二步: 标记子元素都是static的节点,patch时子元素都不会被检查了。
  markStaticRoots(root, false)
}

那我们先看看如何检测一个节点是static的?其实就是用穷举法,把所有可能会引起变化的条件全都排除:

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // 文本节点,有表达式
    return false
  }
  if (node.type === 3) { // 文本节点,没有表达式
    return true
  }
  return !!(node.pre || ( // 被v-pre保护
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) && // 不是template或者v-for的child
    Object.keys(node).every(isStaticKey) // 所有属性都是静态的
  ))
}

然后我们需要做的就是遍历节点,给相应的节点打上标记了:

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child) // 递归调用
      if (!child.static) { // 如果有child不是static,那么自身就不能是static
        node.static = false
      }
    }
  }
}

markStaticRoots的流程大体相似,不过增加了对v-for内的元素处理

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    if (node.static) {
      node.staticRoot = true
      return
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
  }
}

经过优化,我们的AST树终于构建好了,那么下一步就是去生成render函数的代码了。

render函数代码生成

在这之前,我们需要了解vue为我们提供的生成vnode节点的几个函数:

Vue.prototype._h = createElement // create vnode的函数简写
Vue.prototype._s = _toString
Vue.prototype._n = toNumber
Vue.prototype._m = ... //  render static tree by index
Vue.prototype._o = ... // v-once
Vue.prototype._e = emptyVNode
...

vue提供的这些方法挺多的,这些方法都被运用到了render函数的代码生成中,在阅读代码过程中,要逐步去了解它们的工作原理。

我们通过字符串去拼接render函数,然后通过new Function的方式去创造:

var render =  makeFunction(compiled.render)
function makeFunction (code) {
  try {
    return new Function(code)
  } catch (e) {
    return noop
  }
}

我们又需要render函数里面的变量是指向Vue实例的,所以需要采取with去包裹code字符串从而纠正变量作用域,这部分可以看看文章开端提供的参考文章:

compiled.render =  `with(this){return ${code}}`

接下来的工作就是拼接字符串了:

const code = ast ? genElement(ast) : '_h("div")'

function genElement (el: ASTElement): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el)
  } else if (el.for && !el.forProcessed) {
    return genFor(el)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el)
    } else {
      const data = el.plain ? undefined : genData(el)

      const children = el.inlineTemplate ? null : genChildren(el)
      code = `_h('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < transforms.length; i++) {
      code = transforms[i](el, code)
    }
    return code
  }
}

这里全部是些精工细活,鉴于本文的篇幅已经够长了,我这里就不详细的解读这个函数了。此外还设计到对static node的处理,本文都暂且略过。

vnode应用到dom

我们上一篇文章有说过, 在数据产生变化时,会通知到vm.watcher,最终调用vm.update,那我们看看vm._update是如何工作的:

 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
     const vm: Component = this
     if (vm._isMounted) {
       callHook(vm, 'beforeUpdate')
     }
     const prevEl = vm.$el
     const prevActiveInstance = activeInstance
     activeInstance = vm
     const prevVnode = vm._vnode
     vm._vnode = vnode
     // 将vnode patch到真实节点上去
     if (!prevVnode) {
       vm.$el = vm.__patch__(vm.$el, vnode, hydrating)
     } else {
       vm.$el = vm.__patch__(prevVnode, vnode)
     }
     activeInstance = prevActiveInstance
     // 更新__vue__ 引用
     if (prevEl) {
       prevEl.__vue__ = null
     }
     if (vm.$el) {
       vm.$el.__vue__ = vm
     }
     //...
     if (vm._isMounted) {
       callHook(vm, 'updated')
     }
   }

其代码很主要是调用了vm.patch来实现vnode向dom的应用。而Vue的patch功能是fork了Snabbdom这个virtual dom库并加以适配。 由于Vue因为自身的的复杂性使得patch逻辑复杂了很多,所以建议大家在读patch函数时先去读读Snabbdom, 这有助于我们了解一个纯virtual-dom的实现。

至此, Vue的核心内容就已经解析完了,要想更深入的去细读源码,虽然很多人说阅读源码要把握主体而不要死磕细节。但我觉得深入细节后,你才能把握主体,才能比别人走得更远。继续加油~

←支付宝← →微信 →
comments powered by Disqus