上节已经搭建完开发环境,并且实现了数据响应式,这一节来实现vue页面的渲染
核心思想就是将模板变成js语法,然后通过js语法生成虚拟dom,当数据改变后比较虚拟dom差异的部分,组后更新需要更新的地方
模板编译
获取标签和内容
首先要找到需要解析的标签 init.js
import { initState } from "./state";
export function initMixin(Vue) { // 就是给Vue增加init方法的
Vue.prototype._init = function (options) { // 用于初始化操作
// vue vm.$options 就是获取用户的配置
const vm = this;
vm.$options = options; // 将用户的选项挂载到实例上
// 初始化状态
initState(vm);
console.log(options)
if (options.el) {
console.log(options.el)
vm.$mount(options.el); // 实现数据的挂载
}
}
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el);
let ops = vm.$options
if (!ops.render) { // 先进行查找有没有render函数
let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
if (!ops.template && el) { // 没有写模板 但是写了el
template = el.outerHTML
}else{
if(el){
template = ops.template // 如果有el 则采用模板的内容
}
}
// 写了temlate 就用 写了的template
if(template && el){
console.log('templatetemplate')
console.log(template)
}
}
}
}
index.html
<div id="app" style="color:red;background:yellow">
<div style="color:green" key="123">
{{ name }} hello {{age}} hello
</div>
<li> world </li>
</div>
<script src="vue.js"></script>
<script>
const el = new Vue({
data(){
return {
name: '张三',
age: 12,
obj:{
a:1
}
}
},
el: '#app'
})
</script>
这样就取到了需要解析的标签内容
解析标签和内容,生成ast树
获取到标签内容后需要将其变为js语法ast树,用正则来匹配标开始标签-入栈,结束标签-出栈,新建
parse.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 他匹配到的分组是一个 标签名 <xxx 匹配到的是开始 标签的名字
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配的是</xxxx> 最终匹配到的分组就是结束标签的名字
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性
// 第一个分组就是属性的key value 就是 分组3/分组4/分组五
const startTagClose = /^\s*(\/?)>/; // <div> <br/>
// 对模板进行编译处理
export function parseHTML(html) { // html最开始肯定是一个 </div>
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
const stack = []; // 用于存放元素的
let currentParent; // 指向的是栈中的最后一个
let root;
// 最终需要转化成一颗抽象语法树
function createASTElement(tag, attrs) {
return {
tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
// 利用栈型结构 来构造一颗树
function start(tag, attrs) {
let node = createASTElement(tag,attrs); // 创造一个ast节点
if(!root){ // 看一下是否是空树
root = node; // 如果为空则当前是树的根节点
}
if(currentParent){
node.parent = currentParent; // 只赋予了parent属性
currentParent.children.push(node); // 还需要让父亲记住自己
}
stack.push(node);
currentParent = node; // currentParent为栈中的最后一个
}
function chars(text) { // 文本直接放到当前指向的节点中
text = text.replace(/\s/g,' '); // 如果空格超过2就删除2个以上的
text && currentParent.children.push({
type:TEXT_TYPE,
text,
parent:currentParent
});
}
function end(tag) {
let node = stack.pop(); // 弹出最后一个, 校验标签是否合法
currentParent = stack[stack.length - 1];
}
function advance(n) {
html = html.substring(n);
}
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1], // 标签名
attrs: []
}
advance(start[0].length);
// 如果不是开始标签的结束 就一直匹配下去
let attr, end
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length);
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
}
if (end) {
advance(end[0].length)
}
return match;
}
return false; // 不是开始标签
}
while (html) {
// 如果textEnd 为0 说明是一个开始标签或者结束标签
// 如果textEnd > 0说明就是文本的结束位置
let textEnd = html.indexOf('<'); // 如果indexOf中的索引是0 则说明是个标签
if (textEnd == 0) {
const startTagMatch = parseStartTag(); // 开始标签的匹配结果
if (startTagMatch) { // 解析到的开始标签
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1])
continue;
}
}
if (textEnd > 0) {
let text = html.substring(0, textEnd); // 文本内容
if (text) {
chars(text)
advance(text.length); // 解析到的文本
}
}
}
return root;
}
import { parseHTML } from "./parse";
export function compileToFunction(template) {
将template 转化成ast语法树
let ast = parseHTML(template);
return ast;
}
init.js中$mount获取模板后进行转换
import { compileToFunction } from "./compiler";
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el);
let ops = vm.$options
if (!ops.render) { // 先进行查找有没有render函数
let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
if (!ops.template && el) { // 没有写模板 但是写了el
template = el.outerHTML
}else{
if(el){
template = ops.template // 如果有el 则采用模板的内容
}
}
// 写了temlate 就用 写了的template
if(template && el){
const ast = compileToFunction(template);
console.log('astast--')
console.log(ast)
}
}
}
这样就获取了ast树,将模板变为了js语法
将ast树生成render函数
<div style="color:red">hello {{name}} <span></span></div>
render(){
return _c('div',{style:{color:'red'}},_v('hello'+_s(name)),_c('span',undefined,''))
}
标签_c(tag,attrs,...children), 值_v(content+_s([key])),内容_c(tag,content)
import { parseHTML } from "./parse";
function genProps(attrs) {
let str = ''// {name,value}
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') {
// color:red;background:red => {color:'red'}
let obj = {};
attr.value.split(';').forEach(item => { // qs 库
let [key, value] = item.split(':');
obj[key] = value;
});
attr.value = obj
}
str += `${attr.name}:${JSON.stringify(attr.value)},` // a:b,c:d,
}
return `{${str.slice(0, -1)}}`
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ asdsadsa }} 匹配到的内容就是我们表达式的变量
function gen(node) {
if (node.type === 1) {
return codegen(node);
} else {
// 文本
let text = node.text
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`
} else {
//_v( _s(name)+'hello' + _s(name))
let tokens = [];
let match;
defaultTagRE.lastIndex = 0;
let lastIndex = 0;
// split
while (match = defaultTagRE.exec(text)) {
let index = match.index; // 匹配的位置 {{name}} hello {{name}} hello
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})`
}
}
}
function genChildren(children) {
return children.map(child => gen(child)).join(',')
}
function codegen(ast) {
let children = genChildren(ast.children);
let code = (`_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'
}${ast.children.length ? `,${children}` : ''
})`)
return code;
}
export function compileToFunction(template) {
// 1.就是将template 转化成ast语法树
let ast = parseHTML(template);
// 2.生成render方法 (render方法执行后的返回的结果就是 虚拟DOM)
let code = codegen(ast);
code = `with(this){return ${code}}`; // 函数里是向当前的vm取参数,使用with来去指定实例
let render = new Function(code); // 根据代码生成render函数
// _c('div',{id:'app'},_c('div',{style:{color:'red'}}, _v(_s(vm.name)+'hello'),_c('span',undefined, _v(_s(age))))
return render;
}
index.html
<div id="app" style="color:red;background:yellow"><div style="color:green" key="123"></div><li> {{name}}world{{age}} </li></div>
<script src="vue.js"></script>
<script>
const el = new Vue({
data(){
return {
name: '张三',
age: 12,
obj:{
a:1
}
}
},
el: '#app'
})
</script>
得到render函数
执行render函数,生成虚拟dom,patch函数渲染页面
上一步得到render函数后,下面就要执行render函数,渲染出页面来了,新建lifecycle/index.js index.js
import { initMixin } from "./init";
import { initLifeCycle } from "./lifecycle";
function Vue(options){ // options就是用户的选项
this._init(options); // 默认就调用了init
}
initMixin(Vue); // 扩展了init方法
initLifeCycle(Vue);
export default Vue
init.js
import { mountComponent } from "./lifecycle";
export function initMixin(Vue) { // 就是给Vue增加init方法的
//...
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el);
let ops = vm.$options
if (!ops.render) { // 先进行查找有没有render函数
let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
if (!ops.template && el) { // 没有写模板 但是写了el
template = el.outerHTML
}else{
if(el){
template = ops.template // 如果有el 则采用模板的内容
}
}
// 写了temlate 就用 写了的template
if(template && el){
// 这里需要对模板进行编译
const render = compileToFunction(template);
ops.render = render;
}
}
mountComponent(vm,el); // 组件的挂载 时候不能使用template
}
}
lifecycle/index.js
export function initLifeCycle(Vue){
Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
const vm = this;
const el = vm.$el;
// patch既有初始化的功能 又有更新
vm.$el = patch(el,vnode);
}
// _c('div',{},...children)
Vue.prototype._c = function(){
return createElementVNode(this,...arguments)
}
// _v(text)
Vue.prototype._v = function(){
return createTextVNode(this,...arguments)
}
Vue.prototype._s = function(value){
if(typeof value !== 'object') return value
return JSON.stringify(value)
}
Vue.prototype._render = function(){
// 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
return this.$options.render.call(this); // 通过ast语法转义后生成的render方法
}
}
export function mountComponent(vm,el){ // 这里的el 是通过querySelector处理过的
vm.$el = el;
// 1.调用render方法产生虚拟节点 虚拟DOM
vm._update(vm._render()); // vm.$options.render() 虚拟节点
// 2.根据虚拟DOM产生真实DOM
// 3.插入到el元素中
}
这里执行了vm.$options.render()_c,_v等会执行,生成虚拟dom,新建vnode/index.js
vnode/index.js
// h() _c()
export function createElementVNode(vm, tag, data, ...children) {
if (data == null) {
data = {}
}
let key = data.key;
if (key) {
delete data.key
}
return vnode(vm, tag, key, data, children);
}
// _v();
export function createTextVNode(vm, text) {
return vnode(vm, undefined, undefined, undefined, undefined, text);
}
// ast一样吗? ast做的是语法层面的转化 他描述的是语法本身 (可以描述js css html)
// 我们的虚拟dom 是描述的dom元素,可以增加一些自定义属性 (描述dom的)
function vnode(vm, tag, key, data, children, text) {
return {
vm,
tag,
key,
data,
children,
text
// ....
}
}
这样就执行render生成了虚拟的dom,然后通过_update()生成/更新dom lifecycle/index.js
import { createElementVNode, createTextVNode } from "./vdom"
function createElm(vnode){
let {tag,data,children,text} = vnode;
if(typeof tag === 'string'){ // 标签
vnode.el = document.createElement(tag); // 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
patchProps(vnode.el,data);
children.forEach(child => {
vnode.el.appendChild( createElm(child))
});
}else{
vnode.el = document.createTextNode(text)
}
return vnode.el
}
function patchProps(el,props){
for(let key in props){
if(key === 'style'){ // style{color:'red'}
for(let styleName in props.style){
el.style[styleName] = props.style[styleName];
}
}else{
el.setAttribute(key,props[key]);
}
}
}
function patch(oldVNode,vnode){
// 写的是初渲染流程
const isRealElement = oldVNode.nodeType;
if(isRealElement){
const elm = oldVNode; // 获取真实元素
const parentElm = elm.parentNode; // 拿到父元素
let newElm = createElm(vnode);
parentElm.insertBefore(newElm,elm.nextSibling);
parentElm.removeChild(elm); // 删除老节点
return newElm
}else{
// diff算法
}
}
export function initLifeCycle(Vue){
Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
const vm = this;
const el = vm.$el;
// patch既有初始化的功能 又有更新
vm.$el = patch(el,vnode);
}
// _c('div',{},...children)
Vue.prototype._c = function(){
return createElementVNode(this,...arguments)
}
// _v(text)
Vue.prototype._v = function(){
return createTextVNode(this,...arguments)
}
Vue.prototype._s = function(value){
if(typeof value !== 'object') return value
return JSON.stringify(value)
}
Vue.prototype._render = function(){
// 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
return this.$options.render.call(this); // 通过ast语法转义后生成的render方法
}
}
export function mountComponent(vm,el){ // 这里的el 是通过querySelector处理过的
vm.$el = el;
// 1.调用render方法产生虚拟节点 虚拟DOM
vm._update(vm._render()); // vm.$options.render() 虚拟节点
// 2.根据虚拟DOM产生真实DOM
// 3.插入到el元素中
}
// vue核心流程 1) 创造了响应式数据 2) 模板转换成ast语法树
// 3) 将ast语法树转换了render函数 4) 后续每次数据更新可以只执行render函数 (无需再次执行ast转化的过程)
// render函数会去产生虚拟节点(使用响应式数据)
// 根据生成的虚拟节点创造真实的DOM
至此页面渲染完成。