Skip to content

四、初渲染原理

  1. 先初始化数据,进行观测
  2. 将模板进行编译为render函数
  3. 利用render.call(vm)进行改变this指向,最终产生虚拟DOM
  4. 调用_update()方法,执行patch函数将虚拟DOM转化为真实DOM
  5. 将真实DOM放到页面替换#app

1、在Vue的原型上混入_render和_update方法

js
// src\index.js

import { lifecycleMixin } from "./lifecycle";
import { renderMixin } from "./vdom/index";

// Vue构造函数
function Vue(options) {
    // console.log(options);
    // 入口方法,做初始化操作
    this._init(options)
}

// 混合生命周期和渲染函数
lifecycleMixin(Vue);
// 混入_render方法
renderMixin(Vue);

2、生成虚拟DOM

  1. 在Vue的原型上定义_render方法
  2. 在Vue的原型上定义很多编译方法_c、_s、_v等
  3. 执行render.call(vm)的时候,传入vue实例,改变this指向
  4. 利用with的原理,最后将包括字符串的函数转化为虚拟dom
js
// src\vdom\index.js

export function renderMixin(Vue) {
    // 创建虚拟dom-标签元素
    Vue.prototype._c = function() {
        return createElement(...arguments);
    }

    // 处理虚拟dom中双大括号{{}}。如果结果一个对象时,stringify会对这个对象取值
    Vue.prototype._s = function(val) {
        return val == null ? '' : (typeof val == 'object') ? JSON.stringify(val) : val;
    }

    // 创建虚拟dom-文本元素
    Vue.prototype._v = function(text) {
        return createTextVnode(text);
    }

    // 扩展_render方法
    Vue.prototype._render = function() { //_render = render
        const vm = this;
        const render = vm.$options.render;
        // 执行render方法,改变里面this的指向为vm,最后生成虚拟dom
        let vnode = render.call(vm);
        // 返回虚拟dom
        return vnode;
    }
}

// 生成元素节点的虚拟dom对象
function createElement(tag, data = {}, ...children) {
    return vnode(tag, data, data.key, children);
}

// 生成文本节点的虚拟dom对象
function createTextVnode(text) {
    return vnode(undefined, undefined, undefined, undefined, text);
}

// 用来产生虚拟dom的,可以自定义一些属性
function vnode(tag, data, key, children, text) {
    return {
        tag,
        data,
        key,
        children,
        text
    }
}

3、调用渲染函数

js
// src\init.js

// 挂载函数
Vue.prototype.$mount = function(el) {
	const vm = this;
	const options = vm.$options;
	el = document.querySelector(el);
	
	......

	// 调用渲染函数
	mountComponent(vm, el);
}
js
// src\lifecycle.js

import { patch } from "./vdom/patch";

// 定义_update方法
export function lifecycleMixin(Vue) {
    Vue.prototype._update = function(vnode) {
        const vm = this;
        // 将虚拟节点渲染成真实节点:用新创建的元素,替换老的vm.$el
        vm.$el = patch(vm.$el, vnode);
    }
}

// 定义渲染函数
export function mountComponent(vm, el) {
    vm.$el = el;
    // 先调用render方法创建虚拟节点,再将虚拟节点渲染到页面上
    vm._update(vm._render());
}

4、生成真实DOM

js
// src\vdom\patch.js

// 将虚拟节点转化为真实节点
export function patch(oldVnode, vnode) {
    // 产生真实的dom
    let el = createElm(vnode);
    // 获取老的app的父亲-body
    let parentElm = oldVnode.parentNode;
    // 当前真实元素插入到app的后面
    parentElm.insertBefore(el, oldVnode.nextSibling);
    // 删除老的节点
    parentElm.removeChild(oldVnode);
	// 返回新节点进行实时替换
    return el;
}

function createElm(vnode) {
    let { tag, children, key, data, text } = vnode;
    if (typeof tag == 'string') {
        // 创建元素,放到vnode.el上
        vnode.el = document.createElement(tag);

        // 只有元素才有属性
        updateProperties(vnode);

        // 遍历儿子,将儿子渲染后的结果放到父亲中
        children.forEach(child => {
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 创建文本,放到vnode.el上
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

// 处理属性
function updateProperties(vnode) {
    let el = vnode.el; // 当前的真实节点
    let newProps = vnode.data || {}; // 获取当前节点的属性 

    for (let key in newProps) {
        if (key == 'style') { // {color: red}
            // 样式需要遍历添加
            for (let styleName in newProps.style) {
                el.style[styleName] = newProps.style[styleName];
            }
        } else if (key == 'class') {
            // class直接添加
            el.className = el.class;
        } else {
            // 属性就需要利用方法添加,值就是对应的值
            el.setAttribute(key, newProps[key]);
        }
    }
}

Released under the MIT License.