一,前言
上篇,根据 render 函数,生成 vnode,主要涉及一下几点:
封装 vm._render 返回虚拟节点
_s,_v,_c 的实现
本篇,根据 vnode 虚拟节点渲染真实节点
二,
1,前情回顾
mountComponent 方法:将组件挂载到 vm.$el 上
mountComponent 中,调用 vm 实例方法 _render 返回 vnode
_render 方法:在 Vue 初始化时,通过 renderMixin 被扩展到 Vue 原型上
在 _render 中执行前边生成的 render 函数(render 的生成:1,拼接 code;2,with + new Function;)
render 在执行过程中,会调用_s,_v,_c 方法,最终返回了 vnode
// src/lifecycle.js
export function mountComponent(vm) {
vm._render(); // 调用 render 函数,内部触发_s,_v,_c...
}
// src/render.js
export function renderMixin(Vue) {
Vue.prototype._render = function () {
const vm = this;
let { render } = vm.$options;
let vnode = render.call(vm); // 内部会调用_c,_v,_s方法
return vnode; // 返回虚拟节点
}
Vue.prototype._c = function () {/* 创建元素虚拟节点 */}
Vue.prototype._v = function () {/* 创建文本虚拟节点 */}
Vue.prototype._s = function () {/* JSON.stringify */}
}
复制代码
vm._render 执行完成后,就已经得到了虚拟节点 vnode
接下来,就需要将 vnode 更新到页面上了
需要 vm._update 方法:专门负责将虚拟节点更新到页面上
2,vm._update 方法
// src/lifecycle.js
export function mountComponent(vm) {
// vm._render():调用 render 方法
// vm._update:将虚拟节点更新到页面上
vm._update(vm._render());
}
复制代码
在 Vue 原型上扩展 _update 方法:
export function mountComponent(vm) {
vm._update(vm._render());
}
export function lifeCycleMixin(Vue){
Vue.prototype._update = function (vnode) {
console.log("_update-vnode", vnode)
}
}
复制代码
并在 Vue 初始化时完成 Mixin:
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";
function Vue(options){
this._init(options);
}
initMixin(Vue)
renderMixin(Vue)
lifeCycleMixin(Vue) // 在 Vue 原型上扩展 _update 方法
export default Vue;
复制代码
测试:
<body>
<div id="app">
<li>{{name}}</li>
<li>{{age}}</li>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
return { name: "Brave" , age : 123}
}
});
</script>
</body>
复制代码
vm._render 方法执行完成后,返回 vnode
继续进入 vm._update,查看打印 vnode 结果:
TODO:_update 为什么选择在 Vue 原型上进行扩展?
3,patch 方法
vnode 是一个描述了节点关系的对象
根据虚拟节点渲染出真实节点,就需要将 vnode 对象递归进行渲染(先序深度遍历创建节点)
备注:
由于 vue 的更新特性是组件级别的,因此合理进行组件化拆分,能够有效避免递归产生的性能问题
patch 方法:将虚拟节点转为真实节点后插入到元素中
patch 方法所属 vdom 模块,创建 src/vdom/patch.js
备注:
后续的 diff 算法也是在 patch 方法中进行
// src/vdom/patch.js
/**
* 将虚拟节点转为真实节点后插入到元素中
* @param {*} el 当前真实元素 id#app
* @param {*} vnode 虚拟节点
* @returns 新的真实元素
*/
export function patch(el, vnode) {
console.log(el, vnode)
// 根据虚拟节点创造真实节点,替换为真实元素并返回
}
复制代码
在 vm._update 中使用 patch 方法:
// src/lifeCycle.js#lifeCycleMixin
export function lifeCycleMixin(Vue){
Vue.prototype._update = function (vnode) {
const vm = this;
// 传入当前真实元素vm.$el,虚拟节点vnode,返回新的真实元素
vm.$el = patch(vm.$el, vnode);
}
}
复制代码
测试:
Vue 文档中的说明:
创建一个新的 $el,并且使用它来替换掉原来的 el
所以,后面继续要做的事:
根据右侧 vnode 创建真实节点
使用真实节点替换掉左侧老节点 id#app
备注:到目前为止,只有一个根节点,还没有涉及到组件级别的更新
如何替换节点?
replace 替换:需要找到父亲,并指定用谁替换谁,使用不方便
Vue 的实现:找到老节点,将新节点放到老节点的后面,删除新节点
优点:能够确保更新后节点在文档中的顺序不变
4,根据虚拟节点创建真实节点
// src/vdom/patch.js
// 将虚拟节点转为真实节点后插入到元素中
export function patch(el, vnode) {
// 1,根据虚拟节点创造真实节点
const elm = createElm(vnode);
// 2,使用真实节点替换老节点
return elm;
}
复制代码
function createElm(vnode) {
let el;
let{tag, data, children, text, vm} = vnode;
// 通过 tag 判断当前节点是元素 or 文本,判断逻辑:文本 tag 是 undefined
if(typeof tag === 'string'){
el = document.createElement(tag) // 创建元素的真实节点
// 继续处理元素的儿子:递归创建真实节点并添加到对应的父亲上
children.forEach(child => { // 若不存在儿子,children为空数组,循环终止
el.appendChild(createElm(child))
});
} else {
el = document.createTextNode(text) // 创建文本的真实节点
}
return el;
}
复制代码
创建真实节点和虚拟节点的映射关系
三,结尾
又成功水了一篇,还剩 2 篇
本篇,根据 vnode 虚拟节点渲染真实节点,主要涉及一下几点:
下一篇,根据 vnode 虚拟节点渲染真实节点
评论