前言
通过手写Vue2源码,更深入了解Vue; 在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅; 另外我会编写一些开发文档,阐述编码细节及实现思路; 源码地址:手写Vue2源码
流程分析
在$mount
中最后需要将生成的render函数转化成真实DOM渲染到页面:
// src/init.js
export function initMixin(Vue) {
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
const render = compileToFunctions(options.template);
options.render = render;
return mountComponent(vm, el);
}
}
看一下简化版的mountComponent(vm, el)
:
// src/lifesycle.js
export function mountComponent(vm, el) {
vm.$el = el;
// 执行beforeMount生命周期钩子
callHook(vm, "beforeMount");
let updateComponent = () => {
vm._update(vm._render());
};
updateComponent();
// 创建一个Watcher,后续在响应式时再实现
// new Watcher(
// vm,
// updateComponent,
// () => {
// callHook(vm, "beforeUpdate");
// },
// true
// );
callHook(vm, "mounted");
}
主要执行了两个方法:vm._render()
和 vm._update()
;
vm._render()
执行了render函数,生成VNode;
vm._update()
有两个过程:
- 初次渲染时直接Vnode挂载到页面上
- 更新时,比较新旧VNode,经过Diff算法,渲染真实DOM
render函数如何生成VNode
首先在Vue原型上定义一下_render()
方法,以及render函数中调用的几个创建节点的方法(_c
、_v
、_s
):
// src/render.js
import { createElement, createTextNode } from "./vdom/index";
export function renderMixin(Vue) {
Vue.prototype._render = function () {
const vm = this;
// 获取模板编译生成的render方法
const { render } = vm.$options;
// 生成vnode--虚拟dom
const vnode = render.call(vm);
return vnode;
};
Vue.prototype._c = function (...args) {
// 创建虚拟dom元素
return createElement(this,...args);
};
Vue.prototype._v = function (text) {
// 创建虚拟dom文本
return createTextNode(this,text);
};
Vue.prototype._s = function (val) {
// 如果模板里面的是一个对象,需要JSON.stringify
return val == null
? ""
: typeof val === "object"
? JSON.stringify(val)
: val;
};
}
小结一下它们的实现思路:
_render()
就是执行vm.$options.render.call(vm)
(在$mount
中生成render函数,并赋值到options.render)_s(val)
:如果val基础类型,直接展示;如果是对象,调用JSON.stringify(val)
转化成字符串_v
:调用createTextNode(this,text)
方法创建文本Vnode_c
:调用createElement(this,...args)
方法创建元素Vnode
思考一下如何创建元素VNode及文本Vnode:
- VNode就是用来描述元素的js对象
- 文本VNode和元素VNode的区别就是js对象的一些属性不同
- 在
createTextNode
和createElement
返回不同的VNode实例,传入不同的参数即可。
具体实现如下:
// src/vdom/index.js
export default class Vnode {
/**
* @param {标签名} tag
* @param {属性} data
* @param {标签唯一的key} key
* @param {子节点} children
* @param {文本节点} text
* @param {组件节点的其他属性} componentOptions
*/
constructor(tag, data, key, children, text, componentOptions) {
this.tag = tag;
this.data = data;
this.key = key;
this.children = children;
this.text = text;
this.componentOptions = componentOptions;
}
}
// 创建文本vnode
export function createTextNode(vm, text) {
return new Vnode(undefined, undefined, undefined, undefined, text);
}
// 创建元素vnode
export function createElement(vm, tag, data = {}, ...children) {
let key = data.key;
// 如果是普通标签
if (isReservedTag(tag)) {
return new Vnode(tag, data, key, children);
} else {
// 否则就是组件
// TODO...........后续章节再处理组件元素
// let Ctor = vm.$options.components[tag]; //获取组件的构造函数
// return createComponent(vm, tag, data, key, children, Ctor);
}
}
_update(Vnode)如何生成真实DOM
_update()
方法是通过实例调用的,可以将该方法定义在vue原型上;
思考一下_update()
怎么实现:
- 需要实现两个功能:初次渲染和组件更新
- 通过是否能获取到上一次的oldVnode判断是否是初次渲染
具体实现:
// src/lifecycle.js
import { patch } from './vdom/patch'
export function lifecycleMixin(Vue) {
// 初始挂载及后续更新
// 更新的时候,不会重新进行模板编译,因为更新只是数据发生变化,render函数没有改变
Vue.prototype._update = function (vnode) {
const vm = this
const preVnode = vm._vnode // 获取上一次的vnode
vm._vnode = vnode // 在组件实例上增加一个_vnode属性,将本次的vnode存到vm._vnode中
// 通过 vm._vnode 判断是否是初次渲染,patch方法既用于初次渲染,也用于后续更新
// 如果是初次渲染
if (!preVnode) {
// 存储创建的真实DOM到vm.$el
// patch中第一个参数vm.$el可能是真实dom(options中定义过el时)或空(options中没定义过el)
vm.$el = patch(vm.$el, vnode)
} else {
// 如果是视图更新
vm.$el = patch(preVnode, vnode, vm)
}
}
}
其中核心方法就是 patch(oldVnode, vnode)
,该方法既可用于初次渲染,也可用于后续更新。
思考一下如何实现patch方法?
- 根据oldVnode,分情况处理
- 如果没有oldVnode,则直接创建一个真实dom,赋值为vnode.el
- 如果oldVnode为真实DOM,则将vnode转化成真实dom,替换掉老的DOM
- 如果oldVnode为虚拟DOM,则说明是更新,后续章节再分析
具体实现:
// src/vdom/patch.js
export function patch(oldVnode, vnode, vm) {
/**
* 情况1:如果options中没有el,也没有oldVnode,直接创建真实dom
*/
if (!oldVnode) {
return createElm(vnode)
} else {
// Vnode没有设置nodeType,真实节点可以获取到nodeType
const isRealElement = oldVnode.nodeType
/**
* 情况2:如果oldVnode为真实DOM(即初次渲染,且options.el存在),则将vnode转化成真实dom,替换掉老的DOM
*/
if (isRealElement) {
const oldElm = oldVnode
const parentElm = oldElm.parentNode
// 创建新节点的真实DOM
const el = createElm(vnode)
// 插入新节点
parentElm.insertBefore(el, oldElm.nextSibling)
// 移除老节点
parentElm.removeChild(oldVnode)
return el
} else {
/**
* 情况3:如果oldVnode为虚拟DOM,则说明是更新视图
* 涉及到diff算法,后续再补充
*/
console.log(oldVnode, vnode)
console.log('diff更新视图')
}
}
}
// 虚拟Vnode转化成真实dom
function createElm(vnode) {
const { tag, data, key, children, text } = vnode
// 1. 如果是元素节点/自定义组件
if (typeof tag === 'string') {
// 1.1 如果是组件Vnode,返回组件渲染的真实DOM
if (createComponent(vnode)) {
return vnode.componentInstance.$el
}
// 1.2 否则是元素Vnode
vnode.el = document.createElement(tag)
// 解析vnode中的data属性
updateProperties(vnode)
// 遍历子节点Vnode,递归调用createElm生成真实DOM后,插入到父节点里
children.forEach((child) => {
return vnode.el.appendChild(createElm(child))
})
} else {
// 2. 如果是文本节点
vnode.el = document.createTextNode(text)
}
return vnode.el
}
// 创建组件Vnode的真实DOM
function createComponent(vnode) {
let i = vnode.data
/**
* 如果i.hook存在,赋值i=i.hook;如果i.init存在,赋值i=i.init
* 相当于执行:vnode.data.hook.init(vnode)
*/
if ((i = i.hook) && (i = i.init)) {
i(vnode)
}
// 如果组件实例化完毕,有componentInstance属性,那证明是组件
if (vnode.componentInstance) {
return true
}
}
// 解析vnode中的data属性
function updateProperties(vnode, oldProps = {}) {
const newProps = vnode.data || {}
const el = vnode.el
// 如果新的节点没有该属性,需要把老的节点属性移除
for (const k in oldProps) {
if (!newProps[k]) {
el.removeAttribute(k)
}
}
// 对style属性进行处理:如果新的style中没有,需要把老的style值置空
const newStyle = newProps.style || {}
const oldStyle = oldProps.style || {}
for (const key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = ''
}
}
// 遍历新属性,设置相关属性
for (const key in newProps) {
if (key === 'style') {
for (const styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
el.setAttribute(key, newProps[key])
}
}
}
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析