前言
本文仅以记录自己的学习过程,有其他理解的同学可留言。注意 我学习原理一直保持 28 策略 所谓百分之 20 的代码实现了百分之 80 的功能 所以此系列咱们只关心核心逻辑以及功能的实现
正文
上篇文章介绍了Vue的响应式原理,Vue实例化过程中,在初始化数据之后,就进入渲染过程
入口
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount(el)大约做了哪些事?
// 一:判断如果没有$options.render,没有则生成
// 二:mountComponent(vm, el),生成、挂载真实dom
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
// 如果不存在render属性
if (!options.render) {
// 如果存在template属性
let template = options.template;
if (!template && el) {
// 如果不存在render和template 但是存在el属性 直接将模板赋值到el所在的外层html结构(就是el本身 并不是父元素)
template = el.outerHTML;
}
// 最终需要把tempalte模板转化成render函数
if (template) {
const render = compileToFunctions(template);
options.render = render;
}
}
// 将当前组件实例挂载到真实的el节点上面,这时候el为options.el查询到的真实dom,如果是更新渲染,那么el为上次的vnode
return mountComponent(vm, el);
};
compileToFunctions(template)得到渲染函数render
// 这里不做太多分析,
// parser:使用正则拆分template,得到ast对象
// optimization:优化标记静态节点
// generate:生成render函数
// 拼接字符串
// with语法
// new Function(字符串)
组件挂载核心方法 mountComponent
export function mountComponent(vm, el) {
// 真实的el选项赋值给实例的$el属性 为之后虚拟dom产生的新的dom替换老的dom做铺垫
vm.$el = el;
// _update和._render方法都是挂载在Vue原型的方法 类似_init
vm._update(vm._render());
}
分析_render()函数;生成vnode
Vue.prototype._render = function () {
const vm = this;
const { render } = vm.$options;
// 生成vnode--虚拟dom
const vnode = render.call(vm);
return vnode;
};
// 执行渲染函数,渲染函数根据最新的数据生成vnode
// 那渲染函数是什么样的? function(){return _c(标签,{属性,指令等}, 子节点)}
// 举一个最简单的例子,模板为<div id="contain"> <span>{{ name }}</span> </div>
// 编译后的渲染函数为
render() {
return _c(
'div',
{data: {id: 'contain'}},
[_c(
'span',
{},
[name])
]
)
}
// render函数里面有_c _v _t方法需要定义
// _c函数,生成元素节点
// _v函数,为生成文本节点
// _t函数,生成插槽节点,这块留到插槽那块讲解
// Vnode类
class Vnode {
constructor(tag, data, key, children, text) {
this.tag = tag;
this.data = data;
this.key = key;
this.children = children;
this.text = text;
...
}
}
// 创建元素vnode
function createElement(tag, data = {}, ...children) {
let key = data.key;
return new Vnode(tag, data, key, children);
}
// 创建文本vnode
function createTextNode(text) {
return new Vnode(undefined, undefined, undefined, undefined, text);
}
Vue.prototype._c = createElement;
Vue.prototype._v = createTextNode;
核心方法 _update();本文只介绍初次渲染过程
// 1 虚拟dom转换成真实dom
// 2 保存vnode
Vue.prototype._update = function (vnode) {
const vm = this;
// patch是渲染vnode为真实dom核心
patch(vm.$el, vnode);
};
function patch(oldVnode, vnode) {
// 初次渲染$el为options.el查询到的dom节点
const isRealElement = oldVnode.nodeType;
if (isRealElement) {
// 这里是初次渲染的逻辑
const oldElm = oldVnode;
const parentElm = oldElm.parentNode;
// 将虚拟dom转化成真实dom节点
let el = createElm(vnode);
parentElm.insertBefore(el, oldElm.nextSibling);
// 删除老的el节点
parentElm.removeChild(oldVnode);
this.$el = el;
return el;
}
}
// 虚拟dom转成真实dom 就是调用原生方法生成dom树
// 普通dom标签:
// 创建dom
// 更新属性 updateProperties(vnode)
// 有子节点则递归创建,加入到父元素中
// 组件标签例如tag为执行beforeMount,进入当前组件的子组件的初始化过程,在这个阶段当前已经执行beforeMount
// 组件初始化后会挂载dom,也就是说在父把虚拟dom转化成真实dom的过程中渲染了子组件
// 所以父子组件的生命周期也就是 父beforeCreate 父created 父beforeMount 子beforeCreate ... 子mounted 所有子都mounted 父mounted
function createElm(vnode) {
let { tag, data, key, children, text } = vnode;
if (typeof tag === "string") {
vnode.el = document.createElement(tag);
// 解析虚拟dom属性
updateProperties(vnode);
// 如果有子节点就递归插入到父节点里面
children.forEach((child) => {
return vnode.el.appendChild(createElm(child));
});
} else {
// 文本节点
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
// 解析vnode的data属性 映射到真实dom上
function updateProperties(vnode) {
let newProps = vnode.data || {};
let el = vnode.el; //真实节点
for (let key in newProps) {
// style需要特殊处理下
if (key === "style") {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === "class") {
el.className = newProps.class;
} else {
// 给这个元素添加属性 值就是对应的值
el.setAttribute(key, newProps[key]);
}
}
}
思维导图
如果觉得本文对你有帮助,记得点赞、收藏、评论,十分感谢!