主题
初次渲染
mountComponent
前面已经有的render
函数, 现在要做的就是执行render
函数, 渲染页面.
Vue中渲染页面是由mountComponent
方法完成的
js
// initMixin内
// 挂载
Vue.prototype.$mount = function (el) {
const vm = this;
const opts = vm.$options;
// 取到el元素
el = typeof el === "string" ? document.querySelector(el) : el;
// 挂载优先级 render函数 > temlate > el
if (!opts.render) {
// 取到Html模板
if (!opts.template && el) {
opts.template = el.outerHTML;
}
// 开始编译模板得到render函数opts.render
opts.render = compileTemplate(opts.template);
}
// 组件挂载, 将vm上的数据挂载到el上面
mountComponent(vm, el);
};
mountComponent
方法就做了一件事, 就是new Watcher
js
// mountComponent
/**
* 组件挂载, 就是渲染的过程
* @param {*} vm 实例
* @param {*} el 挂载节点
*
*/
export function mountComponent(vm, el) {
vm.$el = el;
/**
* 默认Vue是通过Watcher进行渲染的
*/
// 组件更新的方法, 后续数据变更, 重新执行这个方法即可
function updateComponent() {
// vm._render方法的作用是将render函数渲染为虚拟节点
// vm._update方法是将虚拟节点渲染为真实节点
vm._update(vm._render()); // 两个方法均mixin中扩展
}
new Watcher(vm, updateComponent, () => {}, true); // 默认是渲染watcher
}
扩展vm._render
和vm._update
new Watcher
的时候, 会传入4个的参数, 依次为vm
, updateComponent
, 回调函数
, 以及一个标志(是否为渲染watcher) 第二个参数updateComponent
会在new Watcher
的时候被执行,里面也仅是执行的vm._update(vm._render())
,那么Vue原型上需要扩展_render
和_update
方法
js
import { initMixin } from "./init";
import { lifecyleMixin } from "./lifecycle";
import { renderMixin } from "./render";
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 添加初始化原型方法
lifecyleMixin(Vue); // 添加vm._update方法 -> 将虚拟节点转为展示DOM
renderMixin(Vue); // 添加vm._render方法 -> 将render函数转为虚拟DOM
export default Vue;
_render
方法
js
// render.js
export function renderMixin(Vue) {
Vue.prototype._render = function() {
const vm = this;
const render = vm.$options.render; // 模板解析得到的render函数或者为用户传入的render函数
return render.call(vm); // 执行render函数, this为vm
};
}
_update
方法
js
export function lifecyleMixin(Vue) {
// 将虚拟Dom变成真实Dom,
Vue.prototype._update = function(vnode) {};
}
Watcher (渲染watcher)
这里Watcher
做的事情就是执行fn
函数, 其实最终就是执行了vm._update(vm._render());
js
let uid = 0;
class Watcher {
constructor(vm, fn, cb, isRenderWatcher) {
this.vm = vm;
this.fn = fn; // 组件真实的渲染函数
this.cb = cb; // 回调函数
this.isRenderWatcher = isRenderWatcher || false; // 是否为渲染watcher
this.uid = uid++; // 每一个组件都有一个渲染watcher, 这是唯一标识
this.fn(); // 首先就让组件渲染函数执行
}
}
export default Watcher;
_c
_v
_s
...等 创建虚拟DOM
当Watcher执行了vm._update(vm._render());
, 会先执行_render函数, _render中会将真正的render函数执行, 从模板解析步骤知道, render函数中有一些_c
_v
_s
等方法, 且render函数
中的this
为vm
, 因此这些方法都为原型上的方法, 需要在原型上进行扩展
js
// render.js
import VNode from "./vdom/index";
export function renderMixin(Vue) {
// 创建虚拟DOM
Vue.prototype._c = function(...args) {
const [tag, data = {}, ...children] = args || [];
const key = data.key;
if (key) {
delete data.key;
}
return new VNode(tag, data, key, children, undefined);
};
// 创建虚拟Text
Vue.prototype._v = function(text) {
return new VNode(undefined, undefined, undefined, undefined, text);
};
// 序列化
Vue.prototype._s = function(val) {
if (val == null) return "";
if (typeof val === "object") return JSON.stringify(val);
return val;
};
// 得到虚拟dom
Vue.prototype._render = function() {
const vm = this;
const render = vm.$options.render;
return render.call(vm);
};
}
js
// VNode
// 虚拟DOM
export default class VNode {
constructor(tag, data, key, children, text) {
this.tag = tag;
this.data = data;
this.key = key;
this.children = children;
this.text = text;
this.el = null;
}
}
创建真实DOM -> 渲染页面
上一步_render
得到虚拟dom后, 需要用vm.update
将虚拟dom转成真实dom,再用真实dom渲染页面
js
import { patch } from "./vdom/patch";
export function lifecyleMixin(Vue) {
// 将虚拟Dom变成真实Dom,
Vue.prototype._update = function(vnode) {
const vm = this;
// 用vnode生成真实dom, 渲染页面, 并将$el挂到vm上, 后续可以通过$el访问到根节点
vm.$el = patch(vm.$el, vnode);
};
}
patch渲染页面
js
import { createEle } from "./create-element";
// 通过虚拟DOM创建真实DOM,并且更新到页面上
export function patch(oldvnode, vnode) {
// 这里需要区分是初始渲染还是二次更新,
// 初次渲染的时候oldvnode是真是DOM元素,
// 二次跟新oldvnode是虚拟Dom, 会涉及到diff
if (oldvnode.nodeType) {
// 初次渲染
const el = createEle(vnode); // 通过虚拟dom创建真实dom
const parentNode = oldvnode.parentNode; // 拿到oldDom节点的父级节点
parentNode.insertBefore(el, oldvnode) // 在旧节点前面插入新节点
parentNode.removeChild(oldvnode) // 删除旧节点
return el
} else {
// TODO:二次更新
}
}
createEle用虚拟dom生成真实DOM元素
js
import { singleTag } from "../compile/parseHtml"; // 定义的单标签tags
// 用虚拟dom创建真实DOM
export function createEle(vnode) {
const { tag, data = {}, text, children } = vnode;
// 是元素节点
if (tag) {
// TODO: 区分是组件
// 创建元素
vnode.el = document.createElement(tag); // 将真实元素与虚拟DOM进行关联(挂到vnode.el上), 方便后续diff更新
// 添加属性
for (const key in data) {
let value = data[key];
if (value && typeof value === "object") {
value = Object.keys(value).map(k => `${k}:${value[k]}`).join(";");
}
vnode.el.setAttribute(key, value);
}
// 递归创建
if (children && !singleTag.includes(tag) /* 单标签没有children */) {
children.forEach(child => {
vnode.el.appendChild(createEle(child));
});
}
} else {
// 是文本节点, 则创建文本元素
vnode.el = document.createTextNode(text);
}
// 递归创建的时候需要有返回值, 上面有 vnode.el.appendChild
// 因此返回值只能是一个element
return vnode.el;
}
小结
- 首次渲染挂载过程就是将
render
函数执行得到虚拟dom
, 接着用虚拟DOM生成真实DOM, 再用真实DOM替换页面模板 render
函数只会生成一次,用cli的时候是通过vue-loader
在构建阶段生成, 否则就是在挂载阶段生成, 后续数据变更只会重新运行render函数以及后续步骤- 渲染过程用到渲染
Watcher
, 渲染watcher
现在的作用仅仅只是用于自动执行updateComponent
,其余功能且看后续, 其实updateComponent
就是更新页面的核心方法