共计 7795 个字符,预计需要花费 20 分钟才能阅读完成。
当学习成为了习惯,常识也就变成了常识。 感激各位的 关注 、 点赞 、 珍藏 和评论。
新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn
文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。
前言
接下来就要正式进入手写 Vue2 系列了。这里不会从零开始,会基于 lyn-vue
间接进行降级,所以如果你没有浏览过 手写 Vue 系列 之 Vue1.x,请先从这篇文章开始,依照程序进行学习。
都晓得,Vue1 存在的问题就是在大型利用中 Watcher 太多,如果不分明其原理请查看 手写 Vue 系列 之 Vue1.x。
所以在 Vue2 中通过引入了 VNode 和 diff 算法来解决该问题。通过升高 Watcher 的粒度,一个组件对应一个 Watcher(渲染 Watcher),这样就不会呈现大型页面 Watcher 太多导致性能降落的问题。
在 Vue1 中,Watcher 和 页面中的响应式数据一一对应,当响应式数据产生扭转,Dep 告诉 Watcher 实现对应的 DOM 更新。然而在 Vue2 中一个组件对应一个 Watcher,当响应式数据产生扭转时,Watcher 并不知道这个响应式数据在组件中的什么地位,那又该如何实现更新呢?
浏览过之前的 源码系列,大家必定都晓得,Vue2 引入了 VNode 和 diff 算法,将组件 编译 成 VNode,每次响应式数据发生变化时,会生成新的 VNode,通过 diff 算法比照新旧 VNode,找出其中产生扭转的中央,而后执行对应的 DOM 操作实现更新。
所以,到这里大家也能明确,Vue1 和 Vue2 在外围的数据响应式局部其实没什么变动,次要的变动在编译器局部。
指标
实现 Vue2 编译器的一个简版实现,从字符串模版解析开始,到最终失去 render
函数。
编译器
在手写 Vue1 时,编译器时通过 DOM API 来遍历模版的 DOM 构造来实现的,在 Vue2 中不再应用这种形式,而是和官网一样,间接编译组件的模版字符串,生成 AST,而后从 AST 生成渲染函数。
首先将 Vue1 的 compiler 目录备份,而后新建一个 compiler 目录,作为 Vue2 的编译器目录
mv compiler compiler-vue1 && mkdir compiler
mount
/src/compiler/index.js
/**
* 编译器
*/
export default function mount(vm) {if (!vm.$options.render) { // 没有提供 render 选项,则编译生成 render 函数
// 获取模版
let template = ''
if (vm.$options.template) {
// 模版存在
template = vm.$options.template
} else if (vm.$options.el) {
// 存在挂载点
template = document.querySelector(vm.$options.el).outerHTML
// 在实例上记录挂载点,this._update 中会用到
vm.$el = document.querySelector(vm.$options.el)
}
// 生成渲染函数
const render = compileToFunction(template)
// 将渲染函数挂载到 $options 上
vm.$options.render = render
}
}
compileToFunction
/src/compiler/compileToFunction.js
/**
* 解析模版字符串,失去 AST 语法树
* 将 AST 语法树生成渲染函数
* @param {String} template 模版字符串
* @returns 渲染函数
*/
export default function compileToFunction(template) {
// 解析模版,生成 ast
const ast = parse(template)
// 将 ast 生成渲染函数
const render = generate(ast)
return render
}
parse
/src/compiler/parse.js
/**
* 解析模版字符串,生成 AST 语法树
* @param {*} template 模版字符串
* @returns {AST} root ast 语法树
*/
export default function parse(template) {
// 寄存所有的未配对的开始标签的 AST 对象
const stack = []
// 最终的 AST 语法树
let root = null
let html = template
while (html.trim()) {
// 过滤正文标签
if (html.indexOf('<!--') === 0) {
// 阐明开始地位是一个正文标签,疏忽掉
html = html.slice(html.indexOf('-->') + 3)
continue
}
// 匹配开始标签
const startIdx = html.indexOf('<')
if (startIdx === 0) {if (html.indexOf('</') === 0) {
// 阐明是闭合标签
parseEnd()} else {
// 解决开始标签
parseStartTag()}
} else if (startIdx > 0) {
// 阐明在开始标签之间有一段文本内容,在 html 中找到下一个标签的开始地位
const nextStartIdx = html.indexOf('<')
// 如果栈为空,则阐明这段文本不属于任何一个元素,间接丢掉,不做解决
if (stack.length) {
// 走到这里说阐明栈不为空,则解决这段文本,并将其放到栈顶元素的肚子里
processChars(html.slice(0, nextStartIdx))
}
html = html.slice(nextStartIdx)
} else {// 阐明没有匹配到开始标签,整个 html 就是一段文本}
}
return root
// parseStartTag 函数的申明
// ...
// processElement 函数的申明
}
// processVModel 函数的申明
// ...
// processVOn 函数的申明
parseStartTag
/src/compiler/parse.js
/**
* 解析开始标签
* 比方:<div id="app">...</div>
*/
function parseStartTag() {
// 先找到开始标签的完结地位 >
const end = html.indexOf('>')
// 解析开始标签里的内容 < 内容 >,标签名 + 属性,比方: div id="app"
const content = html.slice(1, end)
// 截断 html,将下面解析的内容从 html 字符串中删除
html = html.slice(end + 1)
// 找到 第一个空格地位
const firstSpaceIdx = content.indexOf(' ')
// 标签名和属性字符串
let tagName = '', attrsStr =''
if (firstSpaceIdx === -1) {
// 没有空格,则认为 content 就是标签名,比方 <h3></h3> 这种状况,content = h3
tagName = content
// 没有属性
attrsStr = ''
} else {tagName = content.slice(0, firstSpaceIdx)
// content 的剩下的内容就都是属性了,比方 id="app" xx=xx
attrsStr = content.slice(firstSpaceIdx + 1)
}
// 失去属性数组,[id="app", xx=xx]
const attrs = attrsStr ? attrsStr.split(' ') : []
// 进一步解析属性数组,失去一个 Map 对象
const attrMap = parseAttrs(attrs)
// 生成 AST 对象
const elementAst = generateAST(tagName, attrMap)
// 如果根节点不存在,阐明以后节点为整个模版的第一个节点
if (!root) {root = elementAst}
// 将 ast 对象 push 到栈中,当遇到完结标签的时候就将栈顶的 ast 对象 pop 进去,它两就是一对儿
stack.push(elementAst)
// 自闭合标签,则间接调用 end 办法,进入闭合标签的解决截断,就不入栈了
if (isUnaryTag(tagName)) {processElement()
}
}
parseEnd
/src/compiler/parse.js
/**
* 解决完结标签,比方: <div id="app">...</div>
*/
function parseEnd() {
// 将完结标签从 html 字符串中截掉
html = html.slice(html.indexOf('>') + 1)
// 解决栈顶元素
processElement()}
parseAttrs
/src/compiler/parse.js
/**
* 解析属性数组,失去一个属性 和 值组成的 Map 对象
* @param {*} attrs 属性数组,[id="app", xx="xx"]
*/
function parseAttrs(attrs) {const attrMap = {}
for (let i = 0, len = attrs.length; i < len; i++) {const attr = attrs[i]
const [attrName, attrValue] = attr.split('=')
attrMap[attrName] = attrValue.replace(/"/g,'')
}
return attrMap
}
generateAST
/src/compiler/parse.js
/**
* 生成 AST 对象
* @param {*} tagName 标签名
* @param {*} attrMap 标签组成的属性 map 对象
*/
function generateAST(tagName, attrMap) {
return {
// 元素节点
type: 1,
// 标签
tag: tagName,
// 原始属性 map 对象,后续还须要进一步解决
rawAttr: attrMap,
// 子节点
children: [],}
}
processChars
/src/compiler/parse.js
/**
* 解决文本
* @param {string} text
*/
function processChars(text) {
// 去除空字符或者换行符的状况
if (!text.trim()) return
// 结构文本节点的 AST 对象
const textAst = {
type: 3,
text,
}
if (text.match(/{{(.*)}}/)) {
// 阐明是表达式
textAst.expression = RegExp.$1.trim()}
// 将 ast 放到栈顶元素的肚子里
stack[stack.length - 1].children.push(textAst)
}
processElement
/src/compiler/parse.js
/**
* 解决元素的闭合标签时会调用该办法
* 进一步解决元素上的各个属性,将处理结果放到 attr 属性上
*/
function processElement() {
// 弹出栈顶元素,进一步解决该元素
const curEle = stack.pop()
const stackLen = stack.length
// 进一步解决 AST 对象中的 rawAttr 对象 {attrName: attrValue, ...}
const {tag, rawAttr} = curEle
// 处理结果都放到 attr 对象上,并删掉 rawAttr 对象中相应的属性
curEle.attr = {}
// 属性对象的 key 组成的数组
const propertyArr = Object.keys(rawAttr)
if (propertyArr.includes('v-model')) {
// 解决 v-model 指令
processVModel(curEle)
} else if (propertyArr.find(item => item.match(/^v-bind:(.*)/))) {
// 解决 v-bind 指令,比方 <span v-bind:test="xx" />
processVBind(curEle, RegExp.$1, rawAttr[`v-bind:${RegExp.$1}`])
} else if (propertyArr.find(item => item.match(/^v-on:(.*)/))) {
// 解决 v-on 指令,比方 <button v-on:click="add"> add </button>
processVOn(curEle, RegExp.$1, rawAttr[`v-on:${RegExp.$1}`])
}
// 节点解决完当前让其和父节点产生关系
if (stackLen) {stack[stackLen - 1].children.push(curEle)
curEle.parent = stack[stackLen - 1]
}
}
processVModel
/src/compiler/parse.js
/**
* 解决 v-model 指令,将处理结果间接放到 curEle 对象身上
* @param {*} curEle
*/
function processVModel(curEle) {const { tag, rawAttr, attr} = curEle
const {type, 'v-model': vModelVal} = rawAttr
if (tag === 'input') {if (/text/.test(type)) {
// <input type="text" v-model="inputVal" />
attr.vModel = {tag, type: 'text', value: vModelVal}
} else if (/checkbox/.test(type)) {
// <input type="checkbox" v-model="isChecked" />
attr.vModel = {tag, type: 'checkbox', value: vModelVal}
}
} else if (tag === 'textarea') {
// <textarea v-model="test" />
attr.vModel = {tag, value: vModelVal}
} else if (tag === 'select') {
// <select v-model="selectedValue">...</select>
attr.vModel = {tag, value: vModelVal}
}
}
processVBind
/src/compiler/parse.js
/**
* 解决 v-bind 指令
* @param {*} curEle 以后正在解决的 AST 对象
* @param {*} bindKey v-bind:key 中的 key
* @param {*} bindVal v-bind:key = val 中的 val
*/
function processVBind(curEle, bindKey, bindVal) {curEle.attr.vBind = { [bindKey]: bindVal }
}
processVOn
/src/compiler/parse.js
/**
* 解决 v-on 指令
* @param {*} curEle 以后被解决的 AST 对象
* @param {*} vOnKey v-on:key 中的 key
* @param {*} vOnVal v-on:key="val" 中的 val
*/
function processVOn(curEle, vOnKey, vOnVal) {curEle.attr.vOn = { [vOnKey]: vOnVal }
}
isUnaryTag
/src/utils.js
/**
* 是否为自闭合标签,内置一些自闭合标签,为了解决简略
*/
export function isUnaryTag(tagName) {const unaryTag = ['input']
return unaryTag.includes(tagName)
}
generate
/src/compiler/generate.js
/**
* 从 ast 生成渲染函数
* @param {*} ast ast 语法树
* @returns 渲染函数
*/
export default function generate(ast) {
// 渲染函数字符串模式
const renderStr = genElement(ast)
// 通过 new Function 将字符串模式的函数变成可执行函数,并用 with 为渲染函数扩大作用域链
return new Function(`with(this) {return ${renderStr} }`)
}
genElement
/src/compiler/generate.js
/**
* 解析 ast 生成 渲染函数
* @param {*} ast 语法树
* @returns {string} 渲染函数的字符串模式
*/
function genElement(ast) {const { tag, rawAttr, attr} = ast
// 生成属性 Map 对象,动态属性 + 动静属性
const attrs = {...rawAttr, ...attr}
// 解决子节点,失去一个所有子节点渲染函数组成的数组
const children = genChildren(ast)
// 生成 VNode 的可执行办法
return `_c('${tag}', ${JSON.stringify(attrs)}, [${children}])`
}
genChildren
/src/compiler/generate.js
/**
* 解决 ast 节点的子节点,将子节点变成渲染函数
* @param {*} ast 节点的 ast 对象
* @returns [childNodeRender1, ....]
*/
function genChildren(ast) {const ret = [], {children} = ast
// 遍历所有的子节点
for (let i = 0, len = children.length; i < len; i++) {const child = children[i]
if (child.type === 3) {
// 文本节点
ret.push(`_v(${JSON.stringify(child)})`)
} else if (child.type === 1) {
// 元素节点
ret.push(genElement(child))
}
}
return ret
}
后果
在 mount
办法中加一句 console.log(vm.$options.render)
,关上控制台,刷新页面,看到如下内容,阐明编译器就实现了
接下来就会进入正式的挂载阶段,实现页面的初始渲染。
链接
- 配套视频,微信公众号回复:” 精通 Vue 技术栈源码原理视频版 ” 获取
- 精通 Vue 技术栈源码原理 专栏
- github 仓库 liyongning/Vue 欢送 Star
- github 仓库 liyongning/Lyn-Vue-DOM 欢送 Star
- github 仓库 liyongning/Lyn-Vue-Template 欢送 Star
感激各位的:关注 、 点赞 、 珍藏 和评论,咱们下期见。
当学习成为了习惯,常识也就变成了常识。 感激各位的 关注 、 点赞 、 珍藏 和评论。
新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn
文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。