目录
一:前言
Vue2是一种渐进式框架,从最初的响应式数据,到生成真实DOM的步骤如下:
- 创建响应式数据
- 模板转换成ats树
- 将ats树转换成render函数(虚拟DOM)
- 后续每次数据更新只需要执行render函数,而不需要执行ats转化过程
- 根据虚拟DOM生成真实DOM
在前面几节中已经完成了前两步,在本文中将为大家讲解3-5点。这里附一下之前的文章
二:ats->虚拟DOM->真实DOM的源码
1、项目目录
以下是项目的目录,其中本次新增的是vdom文件夹,以及根目录下的lifecycle.js文件,基于上一篇文章完善的是complier文件夹下的index.js文件。其中在complier文件夹下的index.js文件是将ats树转化为render函数(虚拟DOM)。而vdom起到创建节点的功能,lifecycle.js是将虚拟DOM转化为真实DOM。接下来将会详细的讲解各文件的实现逻辑代码。
2、complier文件夹下的index.js文件
该文件是将生成的ats树转换为render函数,通过在compileToFunction函数中调用codegen函数,进行字符串的处理与拼接,这里要注意,节点可能有文本节点和标签节点,这里需要进行判断,然后通过正则去匹配。
注意:render函数的模板如下,所以我们要将字符串处理成以下的格式:
_c('div',{id:'app'},_c('div',{style:{color:'red'}}, _v(_s(vm.name)+'hello'),_c('span',undefined, _v(_s(age))))
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)
// 模板引擎的实现原理 就是 with + new Function
let code = codegen(ast);
code = `with(this){return ${code}}`;
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;
}
// <xxx
// <namepsace:xxx
// color = "asdsada" c= 'asdasd' d= asdasdsa
3、vdom文件夹下的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
// ....
}
}
4、lifecycle.js文件
该文件是将虚拟DOM转换为真实DOM的主要文件,向外提供了initLifeCycle和mountComponent两个方法,前者是写在init.js文件的初始化方法,而后者是挂载节点的方法。
初始化方法比较简单,就是_c,_v 这些的初始化。这里不做太多的讲解。
而mountComponent挂载方法,是生成真实DOM进行挂载的,
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; // 这个nodeType是原生的,如果等于表示是元素
if(isRealElement){
const elm = oldVNode; // 获取真实元素
const parentElm = elm.parentNode; // 拿到父元素
let newElm = createElm(vnode);
parentElm.insertBefore(newElm,elm.nextSibling);//把当前新节点插入到老节点下面
parentElm.removeChild(elm); // 然后删除老节点
console.log(newElm)
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
三:总结
当真实DOM挂载完成后,其Vue2的基本构成已经可以实现了,但是还有需要完善的地方。这些会在下一篇文章中继续完善,希望各位小伙伴能够有所收货哦!