Skip to content

初次渲染

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._rendervm._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函数中的thisvm, 因此这些方法都为原型上的方法, 需要在原型上进行扩展

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;
}

小结

  1. 首次渲染挂载过程就是将render函数执行得到虚拟dom, 接着用虚拟DOM生成真实DOM, 再用真实DOM替换页面模板
  2. render函数只会生成一次,用cli的时候是通过vue-loader在构建阶段生成, 否则就是在挂载阶段生成, 后续数据变更只会重新运行render函数以及后续步骤
  3. 渲染过程用到渲染Watcher, 渲染watcher现在的作用仅仅只是用于自动执行updateComponent,其余功能且看后续, 其实updateComponent就是更新页面的核心方法