Appearance
页面初始化
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
}