Skip to content

页面初始化

HTML 字符串 => AST

javascript
// HTML 字符串 => AST
// parseHTML 中通过正则 + 栈生成 AST,通过一个变量 root 记录 AST 的根,最后返回这
// 个根。具体做法是 while 循环中对 HTML 字符串不断进行正则匹配,根据匹配结果对栈进行处理同时不断
// 向下走,即 html.substring(),直到 HTML 字符串长度为0。

// htmlStr: '<div id="app">Hello World</div>'
// 先获取 < 在HTML字符串中的位置。如果索引为 0 说明现在是开始标签或结束标签,
// 即 <div> 或 </div>。
// 会调用 parseStartTag 尝试解析开始标签,如果能成功解析,返回一个对象,否则返回
// undefined,parseStartTag 内部通过正则匹配开始标签,如果有结果,创建一个对象 match 
// match 中通过 tagName 保存标签名,attrs 数组保存属性。之后调用 advance 跳过开始标
// 签。
// htmlStr: ' id="app">Hello World</div>'
// 再通过正则进行属性的匹配,每匹配到一对属性,就将匹配结果放入数组中,再跳过这对
// 属性。由于可能有多个属性,通过 while 循环,当没有匹配到开始标签的闭合(即 >)并且
// 能匹配到属性时不断将匹配结果放入数组中,然后跳过这对属性。
// htmlStr: '>Hello World</div>'
// 所有属性处理完后跳过开始标签的闭合并返回 match。
// htmlStr: 'Hello World</div>'
// 如果发现 parseStartTag 的返回值 match 为真,说明是开始标签,调用 handleStartTag
// 处理属性,遍历 match.attrs,取出每个 attr,就是每个匹配结果,将 match.attrs
// 每一项替换为对象,里面的属性 name 就是 attr[0],value 就是
// attr[3] || attr[4] || attr[5] || '',处理完属性后通过调用 start 并传入
// tagName、attrs 生成 AST,start 内部通过 createASTElement 并传入
// tagName、attrs、currentParent(即栈顶) 生成 AST,即一个对象
// 有属性 type,固定为 1,表示元素节点;此外还有属性 tag、attrsList、parent、children
// 生成 AST 后,看下 root 有没有值,没有说明这个 AST 就是根,让它挂载到 root 上,
// 还需要确定 AST 的 parent,即栈顶,如果 parent 有值,就通过
// parent.children.push(element)、element.parent = parent 确定它们的关系,
// 最后将生成的 AST 放入栈中,进行下一轮循环。

// 如果索引 > 0,说明要处理文本,通过变量 text 记录文本,文本内容就是字符串开始位置到
// textEnd,跳过文本内容。再调用 chars 生成文本 AST,内部取出 parent,即栈顶,对文本
// 通过 trim 过滤首尾空格,如果还有值,就 parent.children.push({ type: 3, text })。

// 如果调用 parseStartTag 没有匹配结果,还会匹配结束标签,如果有匹配结果就调用
// advance 跳过结束标签。
// htmlStr: ''
// 然后再调用 parseEndTag,内部调用 end,end 负责出栈,因为之后的生成的
//  AST 的 parent 是新的栈顶,然后进行下一轮循环。
function parseHTML(html, options: HTMLParserOptions) {
  const stack: any[] = []
  let index = 0
  while (html) {
    // 获取 < 的索引
    let textEnd = html.indexOf('<')
    // 索引为 0,说明是开始标签或结束标签 => <div> 或 </div>
    if (textEnd === 0) {
      // 尝试匹配结束标签
      const endTagMatch = html.match(endTag)
      // 如果能匹配到
      if (endTagMatch) {
        const curIndex = index
        // 跳过结束标签
        advance(endTagMatch[0].length)
        // 出栈,进行下一轮循环。
        parseEndTag(endTagMatch[1], curIndex, index)
        continue
      }

      // 尝试解析开始标签
      const startTagMatch = parseStartTag()
      // 如果有解析结果,生成 AST,进行下一轮循环。
      if (startTagMatch) {
        handleStartTag(startTagMatch)
        continue
      }
    }

    let text
    if (textEnd >= 0) { // 是文本
      // 索引0 到 textEnd 之间的内容就是文本
      text = html.substring(0, textEnd)
    }

    if (text) {
      // 跳过文本
      advance(text.length)
    }

    // 处理文本
    if (options.chars && text) {
      options.chars(text)
    }
  }

  // 向下走
  function advance(n) {
    index += n
    html = html.substring(n)
  }

  // 解析开始标签,返回一个对象,包含标签名和属性。
  function parseStartTag() {
    const start = html.match(startTagOpen)
    // 是开始标签
    if (start) {
      const match: any = {
        tagName: start[1], // 标签名
        attrs: [] // 属性
      }
      // 跳过标签名,接下来获取属性
      advance(start[0].length)
      let end, attr
      // 还没走到开始标签的闭合并且能匹配到属性
      while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(dynamicArgAttribute) || html.match(attribute))
      ) {
        // ' foo="foo" bar="bar"></div>'.match(attribute)
        // [
        //    0: " foo=\"foo\""
        //    1: "foo"
        //    2: "="
        //    3: "foo"
        //    4: undefined
        //    5: undefined
        //    groups: undefined
        //    index: 0
        //    input: " foo=\"foo\" bar=\"bar\"></div>"
        //    length: 6
        // ]

        // 跳过该属性
        advance(attr[0].length)
        // 保存该属性
        match.attrs.push(attr)
      }
      // 走到了开始标签的闭合
      if (end) {
        // 跳过开始标签的闭合
        advance(end[0].length)
        // 返回结果
        return match
      }
    }
  }

  function handleStartTag(match) {
    // 标签名
    const tagName = match.tagName

    // 生成属性
    const l = match.attrs.length
    const attrs: ASTAttr[] = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      attrs[i] = {
        name: args[1],
        value
      }
    }

    // 生成 AST
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  function parseEndTag(tagName?: any, start?: any, end?: any) {
    options.end(tagName)
  }
}

parseHTML(template, {
  start(tag, attrs) {
    // 传入 tag、attrs 生成 AST
    let element: ASTElement = createASTElement(tag, attrs, currentParent)
    
    // 如果 root 还没有值,那么生成的这个 AST 元素就是根
    if (!root) {
      root = element
    }

    stack.push(element)
    // 确定这个 AST 元素的 parent
    currentParent.children.push(element)
    element.parent = currentParent
  },

  end(tag, start) {
    const element = stack[stack.length - 1]
    // pop stack
    stack.length -= 1
    currentParent = stack[stack.length - 1]
  },

  chars(text: string) {
    child = {
      type: 3, // 文本节点
      text
    }
    // 添加进 children 中
    currentParent.children.push(child)
  }
})

AST => render 函数

javascript
// AST => render 函数
<div id="app" style="color: red; font-size: 20px;">
	<span>Hello {{ name }} World</span>
</div>

_c("div",{staticStyle:{"color":"red","font-size":"20px"},attrs:{"id":"app"}},[
	_c("span",[
		_v("Hello "+_s(name)+"World")
	])
])

genElement

javascript
// generate 中调用 genElement,genElement 中调用 genProps 生成属性的字符串对象
// 还会调用 genChildren 生成子元素的 render 函数字符串,最后返回
// `_c("${ el.tag }"${ res })`

function genElement (el) {
  let props,
    code,
    children = ''

  if (el.attrs.length > 0) {
    props = genProps(el.attrs)
  }

  if (el.children) {
    children = genChildren(el.children)
  }

  // 最终的 render 函数, 没有 props 或 children 直接不显示
  let res = ''
  if (props && children) {
    res = `,${props},${children}`
  } else if (props) {
    res = `,${props}`
  } else if (children) {
    res = `,${children}`
  } 

  code = `_c("${ el.tag }"${ res })`
  return code
}

genProps

javascript
// genProps 用于生成属性的字符串对象,即
// id="app" style="color: red; font-size: 20px;" => 
// {staticStyle:{"color":"red","font-size":"20px"},attrs:{"id":"app"}}
// 实现原理就是字符串拼接,遍历 attrs 数组的每一项,如果该对象的属性名是 style,
// 就通过 replace 取出样式名和样式值,拼接进去,如果不是 style 就通过对象取值的方式取出
// name 和 value,拼接进去,循环结束后分别得到样式和属性的拼接结果,去除它们的最后一个逗号,
// 二者再拼接一下并返回这个拼接结果即可。

function genProps (props) {
  let staticProps = "",
    staticStyle = "",
    attrs = "",
    i = 0,
    length = props.length,
    prop,
    attr = '',
    style = ''

  for (; i < length; i++) {
    prop = props[i]
    if (prop.name === 'style') {
      // style="color: red; font-size: 20px;"
      prop.value.replace(/\s*([^;:]+)\:\s*([^;:]+)/g, (_, $1, $2) => {
        style += `"${$1}":"${$2}",`
      })
    } else {
      attr += `"${prop.name}":"${prop.value}",`
    }
  }
  attrs = `attrs:{${attr.slice(0, -1)}}` // 去除最后多出来的一个,
  staticStyle = `staticStyle:{${style.slice(0, -1)}}`
  staticProps = `{${staticStyle},${attrs}}`
  return staticProps
}

genChildren

javascript
// genChildren 用于生成子元素的 render 函数字符串,遍历 children 每一项并调用
// genNode,因为孩子可能是元素 AST 也可能是文本 AST,genNode 中根据 AST.type 判断是哪种,
// 如果是元素 AST 就调用 genElement,否则调用 genText,不断拼接整个过程中生成的
// render 函数字符串并最终返回便得到了子元素的 render函数字符串。

function genChildren (children) {
  let result = '',
    i = 0,
    l = children.length

  if (l === 0) {
    // 没有children
    return result
  }

  for (; i < l; i++) {
    // 递归生成渲染函数
    result += genNode(children[i]) + ','
  }
  result = `[${result.slice(0, -1)}]` // 去除最后一个,
  return result
}

genText

javascript
// genText 用于生成文本字符串,因为可能使用到 {{}},需要将 mustache 表达中的变量写成
// _s(name) 的形式,通过正则判断有没有出现 {{}},如果为 false,说明没有使用到 {{}},
// 直接返回 _v("${ text }"),否则将 reg.lastIndex 置 0,通过 while 循环不断 exec,
// 用变量 lastIdx 记录上次指针位置,idx 记录当前指针位置,idx 总是指向 {,因此文本中
// lastIdx 到 idx 之间的内容就是纯文本,如果有值的话就放进数组中,再将匹配结果的第一项去
// 除空格,这就是 {{}} 中的变量,如果有值的话放进数组中,只不过形式是 _s(${tokenVal}),
// 最后更新 lastIdx 为 idx + 匹配结果的长度,进行下一轮循环。循环结束后,说明已经找完所有
// {{}},如果lastIdx < 文本长度,说明 lastIdx 到文本末尾之间的内容都是纯文本,添加进数组
// 中。最后数组通过 join('+') 拼接并返回结果 _v(${tokens.join('+')})。

function genText (node) {
  let text = node.text,
    tokens = [],
    tokenVal,
    lastIdx = 0, // 上次指针
    idx = 0, // 当前指针
    match

  // 没找到{{}}, 直接生成
  if (!defaultTagRE.test(text)) {
    return `_v("${text}")`
  }
  // test会更新lastIndex值, 需要置0
  defaultTagRE.lastIndex = 0
  while ((match = defaultTagRE.exec(text))) {
    idx = match.index // idx指向{的位置
    tokenVal = text.slice(lastIdx, idx) // {前的内容就是文本内容
    if (tokenVal) {
      // tokens.push(`"${tokenVal}"`)
      tokens.push(JSON.stringify(tokenVal))
    }
    // 获取{{}}中的内容
    tokenVal = match[1].trim()
    if (tokenVal) {
      // _s()渲染函数
      tokens.push(`_s(${tokenVal})`)
    }
    // 开始下一轮, lastIdx指向}后面一位
    lastIdx = idx + match[0].length
  }
  // 找不到{{}}了, lastIdx之后的内容就是text
  if (lastIdx < text.length) {
    tokenVal = text.substr(lastIdx)
    tokens.push(JSON.stringify(tokenVal))
  }
  return `_v(${tokens.join('+')})`
}

render 函数 => VNode

javascript
// 生成 render 函数字符串后,将 render 函数字符串修改为
//  `with (this) { return ${ code } }`,然后 new Function 产生 render 函数
// 因为 {{}} 中的遍历要去 vm 上找,我们通过 with 控制 this,之后调用 render函数时
// 只需要将 this 修改为 vm,内部使用到的变量都会去 vm 上找,最后生成的 render函数
// 挂载到 vm.$options.render 上。

// render 函数执行通过 new Function + with 实现(eval耗性能并且会有作用域问题)
const render = new Function(`with (this) { return ${ code } }`)

return render

// VNode 就是一个对象,通过 VNode 函数生成,有
// context、tag、data、children、text、elm、key、conponentOptions 属性
function VNode (context, tag, data, children, text, elm, key, conponentOptions) {
  return {
    context, 
    tag, 
    data, 
    children, 
    text, 
    elm,
    key,
    conponentOptions
  }
}

// render 函数内会执行 _c、_v、_s 函数,我们只需要在 Vue 原型上定义这些方法,
// 那么只需要将 render 函数执行并修改 this 为vm 就能生成 VNode,
// ƒ anonymous() {
//   _c("div",{staticStyle:{"color":"red","font-size":"20px"},attrs:{"id":"app","class":"/>"}},[_c("span",[_v("Hello "+_s(name)+" "+_s(age)+" World")])])
// }

// _s 会判断 {{}} 内的变量在 vm 上取到的值是不是对象,如果是转为 JSON 并返回,否则
// 直接返回。
Vue.prototype._s = function (val) {
    if (isObject(val)) {
      return JSON.stringify(val)
    } else {
      return val
    }
}

// _v 通过调用 createTextNode 生成文本 VNode,createTextNode 内部直接调用
// VNode 函数,传入 context 和 text。
Vue.prototype._v = function (text) {
	return createTextNode(this, text)
}

function createTextNode (context, text) {
  return VNode(context, undefined, undefined, undefined, text)
}

// _c 通过调用 createElement 生成元素 VNode,会判断标签是否是原生标签,这里运用了
// 闭包,是原生标签直接调用 VNode 函数并传入 context,tag,data,children 生成元素
// VNode,否则生成组件VNode。
Vue.prototype._c = function (tag, data, ...children) {
	return createElement(this, ...arguments)
}

function createElement (context, tag, data, children) {
  // _v('div', {...}, [...])
  // _v('div', {...})
  // _v('div', [...])
  // _v('div')
    
  if (Array.isArray(data)) {
    children = data
    data = undefined
  }
    
  if (isReservedTag(tag)) {
    return VNode(context, tag, data, children)
  } else {
    // 创建组件的vnode
  }
}
javascript
const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // HTML 字符串 => AST
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // AST => render 函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

VNode => DOM

javascript
// createElm 通过判断 vnode.tag 值的类型来决定渲染成元素节点还是文本节点
// 文本 Vnode 的 tag 值是 undefined,元素 Vnode 的 tag 值是字符串,如果不是字符串,
// 通过 document.createTextNode(text) 生成文本节点并挂载到 vnode.elm 上,
// 否则通过 document.createElement(tag) 生成元素节点并挂载到 vnode.elm 上,
// 元素 VNode 生成 DOM 后还会通过 updateStyle 和 updateAttrs 完成样式和属性的更新,
// 它们通过对新旧 VNode 的比较完成,在第一次生成 DOM 时 oldVnode 是一个空对象,
// 更新函数内部先取出 oldVnode 和 vnode 各自的 vnode.data.staticStyle,即
// oldStyle 和 newStyle,遍历 oldStyle 所有属性,如果某个属性在 newStyle 中没定义,
// 以新的为准,说明它被移除了,直接 el.style[name] = '',再遍历 newStyle 所有属性,
// 如果属性值和 oldStyle[name] 不相等,以新的为准,说明该样式被更新了,
// el.style[name] = newStyle[name]。
// updateAttrs 的流程和 updateStyle 流程一样,通过 setAttribute 完成属性的更新,
// 更新完样式和属性后再遍历 children,对 children 每一项递归调用 createElm 生成
// DOM并通过vnode.elm.appendChild 添加进去。
function createElm (vnode) {
  let { tag, data, children, text, context } = vnode
  
  if (typeof tag === 'string') {
    // 渲染成元素节点
    vnode.elm = document.createElement(tag)
    updateProps({}, vnode) // 初始化时更新样式、属性
    if (children) {
      for (let i = 0, l = children.length; i < l; i++) {
        vnode.elm.appendChild(createElm(children[i]))
      }
    }
  } else {
    // 渲染成文本节点
    vnode.elm = document.createTextNode(text)
  }

  return vnode.elm
}

function updateStyle(oldVnode: VNodeWithData, vnode: VNodeWithData) {
  const data = vnode.data
  const oldData = oldVnode.data

  // 如果都没有设置样式,不用处理。
  if (
    isUndef(data.staticStyle) &&
    isUndef(data.style) &&
    isUndef(oldData.staticStyle) &&
    isUndef(oldData.style)
  ) {
    return
  }

  let cur, name
  const el: any = vnode.elm
  const oldStyle = oldData.staticStyle
  const newStyle = getStyle(vnode, true)

  // 遍历旧的每个属性
  for (name in oldStyle) {
    // 如果新的里面没有,以新的为准,说明它被移除了。
    if (isUndef(newStyle[name])) {
      setProp(el, name, '')
    }
  }
  // 遍历新的每个属性
  for (name in newStyle) {
    cur = newStyle[name]
    // 如果旧的值和新的值不相等,需要更新,以新的为准。
    if (cur !== oldStyle[name]) {
      // ie9 setting to null has no effect, must use empty string
      setProp(el, name, cur == null ? '' : cur)
    }
  }
}

Vue 中的实现

javascript
// 以上就是页面初始化的流程,Vue 中通过 compileToFunction 将
// el.outerHTML => render 字符串,当然这一步会在 runtime 中完成。
// $mount('#app') 后,
// $mount 内部调用 mountComponent 完成页面的初始化。
// mountComponent 定义了一个 updateComponent 方法,用于实现页面的初始化渲染和后续
// 更新,它会将 render 函数 => VNode => DOM。

// render 函数 => VNode 通过 vm._render() 实现,内部只做一件事 render.call(vm)
Vue.prototype._render = function () {
  let vm = this
  let render = vm.$options.render
  let vnode = render.call(vm) // 让 wit h中的 this 是 vm
  return vnode
}

// vnode => dom 通过 vm._update(vnode) 实现,内部会先 vm._vnode = vnode 保存
// 之前的 VNode,用于后续 diff,然后调用 patch 将 VNode => dom 挂载到 vm.$el 上
// 并在 HTML 中 替换掉原来的 $el,第一次就是 #app。
Vue.prototype._update = function (vnode) {
  vm._vnode = vnode // 保存vnode, 用于diff
  
  // 虚拟DOM => 真实DOM并替换#app
  vm.$el = patch(vm.$el, vnode)
}

// patch 内部判断如果是原生标签就进行初始化渲染,先用变量 oldElm 保存 oldVnode,
// 第一次就是 #app,再用变量 parentElm 保存 oldElm.parentNode,即 body
// 再调用 createElm 并传入 vnode 生成 DOM,最后将这个 DOM插入到 #app 的前面
// 最后删除 #app 并返回生成的这个 DOM。
function patch (oldVnode, vnode) {
  if (isRealElement) { // 初始化
    let oldElm = oldVnode // #app
    let parentElm = oldElm.parentNode // body
    let elm = createElm(vnode)
    parentElm.insertBefore(elm, oldElm.nextSibling)
    parentElm.removeChild(oldElm)
    return elm
  }
}

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

function mountComponent(
  vm: Component,
  el: Element | null | undefined
): Component {
  vm.$el = el

  const updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  return vm
}