前言
接着上篇文章的内容(08 | 【阅读Vue2源码】Template被Vue编译成了什么?),由于篇幅有限,并且实现过程也确实挺复杂的,所以分两篇文章来记录。
上篇文章已经研究了Template转成render函数的过程,那么,本篇文章就接着研究render函数是如何生成标准的DOM,并且Vue是如何更新视图的。
在了解了实现原理之后,尝试自己动手实现。
在阅读源码和自己动手实现的过程中,发现这个过程确实很难很痛苦
- 要写好文章也很难,要记录自己对源码理解、也要关注写的文章其他人能否读懂,真的挺难。挺耗时间的。
- 但是又总会想到雷总的一句话,“你所经历的所有挫折、失败,甚至那些看似毫无意义消磨时间的事情,都将成为你最宝贵的财富。”继续坚持,砥砺前行。
示例Demo代码
沿用上篇文章(08 | 【阅读Vue2源码】Template被Vue编译成了什么?)中的demo示例代码继续分析
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count }}</div>
</section>
const app = new Vue({
data() {
return {
count: 0
}
},
methods: {
plus() {
this.count += 1;
}
}
})
app.$mount('#app')
借用上篇文章中生成的render函数
function render() {
return with(this) {_c('section', {"attrs":{"id":"app"}}, [_c('button', {"on":{"click":{"value":"plus"}},"attrs":{}}, [_v('+1')]), _c('div', {"attrs":{"class":"count-cls count-text"}}, [_v('count:' + _s('count'))]), count % 2 === 0 ? _c('div', {"attrs":{}}, [_v('doubleCount:' + _s('count'))]) : _e()])}
}
源码分析
思维导图
简单回顾template生成render函数的过程
- Vue组件初始化完,调用
$mount()挂载元素 $mount获取template字符串,底层调用baseCompile函数处理baseCompile的逻辑为:调用parse解析template生成ast- 调用
genrate函数根据ast生成render函数的函数体字符串 - 使用
new Function,以前面步骤构建的函数体字符串作为参数生成render函数 - 生成完
render函数,回到$mount的函数体,继续调用mount函数 - 接下来,本文将继续分析
mount函数之后的逻辑
生成render函数之后
执行mount
在生成render函数之后,回到$mount的函数体,继续调用mount函数
mount函数是之前的$mount,当前这个入口文件为entry-runtime-with-compiler.js,带编译器的入口文件,需要增加一些额外的逻辑,所以把原来的$mount函数缓存了一份为mount,执行完额外的逻辑后再执行原来的$mount,即mount
// src/platforms/web/entry-runtime-with-compiler.js
// 先缓存一份原来的$mount方法
const mount = Vue.prototype.$mount
// 重新赋值$mount方法,主要是要加入编译函数compileToFunctions生成render方法
Vue.prototype.$mount = function () {
// ...
return mount.call(this, el, hydrating)
}
继续debugger,进入mount的函数体
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
可以看到,这里主要是调用mountComponent,进入mountComponent看看做了什么
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent // 声明updateComponent,等待赋值
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
const vnode = vm._render()
vm._update(vnode, hydrating)
// ...
}
} else {
// 这里是核心逻辑,把vm._render()执行结果VNode作为参数传递给_update执行
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 这里是最核心的逻辑:把updateComponent更新函数放到Watcher的回调中进行监听,如果vm的数据有更新,则执行updateComponent函数,更新视图。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent的逻辑也很清晰,主要逻辑为
- 声明
updateComponent,等待赋值一个函数 updateComponent的函数体为vm._update(vm._render(), hydrating)
- 执行
vm._render()函数得到vnode - 执行
vm._update更新视图
new Watcher作为渲染的watcher,以updateComponent作为回调函数,当vm发生变化时,执行updateComponent,更新视图
到这里,可以了解到Vue更新视图的大概的流程和原理了。
触发updateComponent
在之前分析watch的实现原理的文章(05 | 【阅读Vue2源码】watch实现原理)中,我们知道了,new Watcher时(非computedWatcher)会先执行getter函数取一下值,也就会执行cb,这里的cb就是updateComponent
也就是说,new Watcher时,里面会执行updateComponent,然后进入updateComponent的逻辑
再跟着debugger,进入updateComponent
_update执行更新
里面执行vm._update,它接收的第一个参数为_render()的返回值,那么我们看看_render函数
// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// ...
let vnode
try {
// 核心逻辑,执行render,得到vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
} finally {
// ...
}
// ...
return vnode
}
核心逻辑,就是调用之前生成的render函数,生成vnode,再返回vnode,简单来说就是包装了一下render函数,就成了现在_render函数
生成VNode
从代码中可以看到,执行render函数生成的VNode,render函数接收两个参数,第一个是vue实例,用于绑定作用域,第二个参数是$createElement,用于创建VNode。
$createElement的函数是在Vue初始化的创建的,调用链路为new Vue() -> this._init() -> initRender()
// src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
可以看到vm.$createElement和vm._c都是createElement,支持最后一个参数不同,那么看看createElement的实现。
createElement
createElement的源码位置在:src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// ...
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
return createEmptyVNode()
}
// ...
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 核心逻辑:new VNode创建VNode 和创建组件
if (config.isReservedTag(tag)) {
// 标准的标签名
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建组件
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 其他
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
// 返回VNode
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
createElement内部调用_createElement,看看_createElement的实现,
核心逻辑为:
- 判断标签名是否是标准的html标签名,如果是直接
new VNode - 如果标签名是自定义的组件,则调用
createComponent - 其他标签名,也是
new VNode - 返回
vnode实例
createElement这个函数并没有创建元素,只是返回了vnode,但是作者为什么要取这个函数名呢?我觉得还不如取createVNode呢,哈哈,这只是我自己的一个臆想。
那么VNode是什么样子的呢,看看VNode源码的定义
VNode
其实也很简单,就是一堆属性
// src/core/vdom/vnode.js
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
vnode实例对象就是这个样子的
其中主要关注tag、data、children、elm、text,context、后面要根据这些属性来生成真实的DOM,赋值给elm属性保存着。
准备更新视图
执行完_render(),得到vnode作为参数传给_update,那么看看_update做了啥呢。
_update源码
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) { // 核心逻辑
// initial render 初始化时走这个
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新视图时走这个
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
这里的核心逻辑主要是执行vm.__patch__。
分两种情况
- 初始化时:
vm.__patch__(vm.$el, vnode, hydrating, false),以$el作为第一个参数 - 更新时:
vm.__patch__(prevVnode, vnode),以prevVnode作为第一个参数
那么__patch__又是什么呢?它就是patch函数
// src/platforms/web/runtime/index.js
import { patch } from './patch'
// ...
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
实现视图的更新
看看patch的实现
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 初始化时是真正的元素,$el
if (isRealElement) {
// ...
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 核心逻辑1,创建真实的DOM,挂载到vnode.elm中
// create new node
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// ...
// 核心逻辑2,移除旧元素
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
这里最核心的逻辑就是
createElm,创建真实的DOM,挂载到vnode.elm中,当新元素挂上去时,会出现新元素与旧元素并存的时刻(后面会移除旧元素)
- 移除旧元素,
removeVnodes([oldVnode], 0, 0) - 完成视图的更新,更新完成后就只剩最新的元素了
createElm
看看createElm具体实现
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// ...
// 核心逻辑:根据vnode中的tag,调用document.createElement创建真实的元素,挂载到vnode.elm
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 设置scope,即style的scoped的实现
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
// 创建子元素
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 将创建的元素挂载到父元素下面
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
这里的核心逻辑:
- 根据
vnode中的tag,调用document.createElement创建真实的元素,挂载到vnode.elm - 创建子元素,调用
createChildren - 将创建的元素挂载到父元素下面,
insert(parentElm, vnode.elm, refElm) - 接着回到
patch函数
patch函数中移除旧节点,完成视图更新,这里已经完成了视图的首次更新,接着代码回调new Watcher之后的代码执行,我们要分析的内容也就完成了。
思维导图
其实看代码还是很枯燥的,可能看得迷迷糊糊,这里我画一张图,描述主要的过程
自己实现render转化DOM的过程
编写大致框架代码
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count * 2 }}</div>
</section>
const renderHelpers = miniRender();
const { MiniVue } = createMiniVue(renderHelpers);
const app = new MiniVue({
el: document.getElementById("app"),
data: {
count: 0
},
methods: {
plus() {
this.count += 1;
}
}
})
app.$mount('#app');
创建MiniVue
因为我这个示例是在html的script标签中编写的,为了方便阅读和组织代码结构,我把MiniVue的实现代码都放到一个createMiniVue函数中,MiniVue的代码借用我之前的文章(05 | 【阅读Vue2源码】watch实现原理)中的代码。
做了一点改造:
- 挂载
mount和updateComponent,因为需要挂载mount方法和updateComponent方法,所以通过参数的方式传递进来。 vm增加_renderWatcher属性,用于保存渲染用的watcherMiniWatcher的run方法增加_renderWatcher的cb执行,用于更新视图
function createMiniVue(config) {
const {mount, updateComponent} = config;
MiniVue.prototype.$mount = mount;
class MiniWatcher {
vm = null; // 当前vue/vue组件实例
cb = () => { }; // 回调函数
getter = () => { }; // 取值函数
expression = ''; // watch的键名
user = false; // 是否是用户定义的watch
value; // 当前观察的属性的值
constructor(vm, expOrFn, cb, options = {}) {
this.vm = vm;
this.cb = cb;
this.expression = expOrFn;
// 对getter做区分处理
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parseExpression(this.expression, vm, this);
}
this.user = options.user;
// 初始化lazy
this.lazy = !!options.lazy;
// 增加对computed的处理
this.value = this.lazy ? undefined : this.get();
}
get() {
const value = this.getter.call(this.vm);
return value;
}
update() {
nextTick(() => {
this.run();
})
}
run() {
// 获取新值和旧值
const newValue = this.get();
const oldValue = this.value;
this.value = newValue;
this.cb.call(this.vm, newValue, oldValue);
if (this.vm._renderWatcher) {
this.vm._renderWatcher.cb();
}
}
// 新增computed用的计算值的函数
evaluate() {
this.value = this.get();
}
}
class MiniDep {
static target = null;
subs = [];
depend(sub) {
if (sub && !this.subs.includes(sub)) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach(sub => {
sub && sub.update();
})
MiniDep.target = null;
}
}
// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
return () => {
MiniDep.target = watcher;
// 取值,触发getter,取值前先把watcher实例放到target中
const value = vm.data[key];
// 取完值后,清空Dep.target
MiniDep.target = null;
return value;
}
}
function nextTick(cb) {
return Promise.resolve().then(cb);
}
function MiniVue(options = {}) {
const vm = this;
this.$el = options.el;
this.vm = this;
this.data = options.data;
this.watch = options.watch;
this.deps = new Set();
initData(vm, this.data); // 初始化data
initWatch(this.watch); // 初始化watch
initMethods(options.methods); // 初始化methods
function observe(data) {
for (const key in data) {
defineReactive(data, key);
}
}
function defineReactive(data, key) {
const dep = new MiniDep();
vm.deps.add(dep);
const clonedData = JSON.parse(JSON.stringify(data));
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend(MiniDep.target || vm._renderWatcher);
return clonedData[key];
},
set: function reactiveSetter(value) {
dep.notify();
clonedData[key] = value;
return value;
}
});
}
function initData(vm, data = {}) {
for (const key in data) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get() {
return vm['data'][key];
},
set(val) {
vm['data'][key] = val;
}
})
observe(vm.data);
}
}
function initWatch(watch = {}) {
for (const key in watch) {
new MiniWatcher(vm, key, watch[key], { user: true }); // user = true,标记这是用户定义的watch
}
}
// 把methods的属性平铺到vm中
function initMethods(methods = {}) {
for (const key in methods) {
if (Object.hasOwnProperty.call(methods, key)) {
const method = methods[key];
Object.defineProperty(vm, key, {
value: method.bind(vm)
})
}
}
}
vm._renderWatcher = new MiniWatcher(this, () => { }, () => {
updateComponent(vm, vm.$el)
})
}
return {
MiniVue
};
}
创建miniCompiler
miniCompiler是上篇文章(08 | 【阅读Vue2源码】Template被Vue编译成了什么?)中写的代码,这里也是把代码放进miniCompiler函数中
function miniCompiler(el) {
function compile(template = '') {
const ast = parse(template);
// console.log('alan->ast', ast);
const code = generate(ast);
const render = createFunction(code);
return render;
}
function parse(template = '') {
// 获取元素所有属性
function getAttrs(el) {
const attributes = el.attributes;
const attrs = []; // 收集属性
const attrMap = {}; // 收集属性的map
const events = {}; // 收集事件@xxx
let ifStatment = {}; // 收集v-if
for (const key in attributes) {
if (Object.hasOwnProperty.call(attributes, key)) {
const item = attributes[key];
attrMap[item.name] = item.value;
attrs.push({
name: item.name,
value: item.value,
});
if (item.name.startsWith('@')) { // 处理事件
events[item.name.replace('@', '')] = { value: item.value }
}
if (item.name === 'v-if') { // 处理v-if
ifStatment = { exp: item.value }
}
}
}
return { attrs, attrMap, events, ifStatment };
}
// 解析插值
function parseExpressionVar(str = "") {
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
const matchs = [...str.matchAll(reg)] || [];
const res = [];
if (matchs.length) {
matchs.forEach((item) => {
res.push({
raw: item[0],
name: String(item[1]).trim(),
index: item.index,
});
});
}
return res;
}
// 遍历元素
function walkElement(el, parent) {
const ast = createASTElement();
ast.parent = parent;
ast.tag = el.tagName.toLowerCase();
// 获取当前元素的所有属性
const { attrs, attrMap, events, ifStatment } = getAttrs(el);
ast.attrs = attrs;
ast.attrMap = attrMap;
ast.events = events;
if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
ast.if = ifStatment
}
const children = Array.from(el.children);
if (children.length) { // 如果有子元素,递归遍历收集所有子元素
children.forEach((child) => {
const childAST = walkElement(child, ast);
ast.children.push(childAST);
});
} else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
const childVNodes = [...el.childNodes];
if (childVNodes.length) {
const text = childVNodes[0].nodeValue
.trim()
.replace(" ", "")
.replace("\n", " ")
.trim(); // 去除空格和换行
// 创建空的ast,文本节点增加text属性
const textAst = createASTElement();
textAst.text = text;
textAst.expression = {
values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
};
ast.children.push(textAst);
}
}
return ast;
}
const tempDOM = document.createElement("div");
tempDOM.innerHTML = template;
const templateDOM = tempDOM.children[0];
const ast = walkElement(templateDOM, null);
return ast;
}
// 将ast转化成render函数的函数体的字符串
function generate(ast = {}) {
// 构建子元素
const genElmChildren = (children = []) => {
let str = "[";
children.forEach((child, i) => {
str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
});
return str + "]";
};
// 构建data
const genData = (ast = {}) => {
const data = {}
// 处理事件
if (ast.events && Object.keys(ast.events).length) {
data.on = ast.events;
}
// 处理属性
if (ast.attrs && ast.attrs.length) {
data.attrs = {}
ast.attrs.forEach(item => {
const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
let key;
let value;
if (!skip) {
if (item.name.startsWith(':')) { // parse :class
key = item.name.replace(':', '');
if (data.attrs[key]) {
const oldVal = data.attrs[key]
const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
value = `${oldVal} ${valList.join(' ')}`
}
} else {
key = item.name;
value = item.value;
}
}
data.attrs[key] = value;
})
}
return data;
};
// 构建_c()
const genElm = (ast) => {
let str = "";
if (ast['if'] && ast['if'].exp) { // 处理v-if
let elStr = ''
if (ast.tag) {
elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
}
// v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
str += `${ast['if'].exp} ? ${elStr} : _e()`
} else if (ast.tag) {
// 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
} else if (ast.text) { // 处理文本节点
// 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
if (ast.expression && ast.expression.values.length) {
// 解析插值语法
const replaceVarWithFn = (name, target = "") => {
const toReplace = `' + _s(${name})`;
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
let newStr = "";
newStr = target.replaceAll(reg, (item) => {
const matchs = [...item.matchAll(reg)] || [];
let tempStr = "";
if (matchs.length) {
matchs.forEach((matItem) => {
const mated = matItem[1];
if (mated && mated.trim() === name) {
tempStr = item.replaceAll(reg, toReplace);
}
});
}
return tempStr;
});
return newStr;
};
let varName = "";
ast.expression.values.forEach((item) => {
varName += replaceVarWithFn(item.name, ast.text);
});
str += `_v('${varName})`;
} else {
// 静态文本
str += `_v('${ast.text}')`;
}
}
return str;
};
let code = genElm(ast);
return code;
}
function createASTElement(tag, attrs, parent) {
return {
tag,
attrsMap: {},
parent,
children: []
}
}
function createFunction(code = '') {
return new Function(`
with(this) {
return ${code};
}
`)
}
// 获取元素和模板字符串
el = el || document.getElementById('app');
const template = el.outerHTML;
// 执行编译
const compiled = compile(template);
return {
render: compiled
}
}
创建miniRender
因为更新视图的函数需要在mount中实现,所以我这里把mount和updateComponent函数都放进miniRender函数,并返回
function miniRender() {
function mount(el) {
if (!(el instanceof HTMLElement)) {
el = document.querySelector(el);
}
updateComponent(this, el);
}
function updateComponent(vm, el) {
function update(vm, el) {
// 获取render函数
const { render } = miniCompiler(el);
class VNode {
constructor(tag, data, children) {
this.tag = tag;
this.data = data;
this.children = children;
this.elm = undefined;
this.context = undefined;
this.text = undefined;
}
}
// 处理视图更新
function patch(vm, oldVNode, vnode, parentElm) {
if (!parentElm) {
parentElm = document.body;
}
if (!oldVNode) {
oldVNode = vm.$el;
const emptyVNode = new VNode(oldVNode.tagName.toLowerCase(), {}, []);
oldVNode = emptyVNode;
oldVNode.elm = vm.$el;
}
// 根据vnode创建真实的dom,处理属性、事件、静态文本、创建子元素等
const createElm = (vnode, parentElm) => {
const { tag, data = {}, children = [] } = vnode || {};
if (tag) {
const elm = document.createElement(tag);
const { attrs = {}, on = {} } = data;
// 处理属性
for (const key in attrs) {
if (Object.hasOwnProperty.call(attrs, key)) {
const value = attrs[key];
elm.setAttribute(key, value);
}
}
// 处理监听事件
for (const key in on) {
if (Object.hasOwnProperty.call(on, key)) {
if (on[key].value) {
const event = vm[on[key].value]
event && elm.addEventListener(key, event)
}
}
}
// 处理子元素
if (children && children.length) {
children.forEach((childVNode) => {
createElm(childVNode, elm);
});
}
vnode.elm = elm;
// 移除文档上的旧节点
parentElm.appendChild(elm);
} else if (vnode.text) {
// 处理静态文本
textNode = document.createTextNode(vnode.text);
parentElm.innerHTML = textNode.nodeValue;
}
};
createElm(vnode, parentElm);
parentElm.removeChild(oldVNode.elm);
}
// 创建vnode
function createElement(tag = "div", data = {}, children = []) {
const createVNode = (tag = "div", data = {}, children = []) => {
const vnodeChildren = [];
if (children && children.length) {
children.forEach((child) => {
vnodeChildren.push(child);
});
}
const vnode = new VNode(tag, data, vnodeChildren);
return vnode;
};
// render函数中执行_c,接收参数,创建vnode
const vnode = createVNode(tag, data, children);
return vnode;
}
function _c(tag = "div", data = {}, children = []) {
return createElement(tag, data, children)
}
function _v(str) {
const vnode = new VNode();
vnode.text = str;
return vnode;
}
function _s(val) {
return String(val);
}
function _e() {
return new VNode();
}
// 挂载render函数中需要使用的_c、_v、_s、_e
vm._c = _c;
vm._v = _v;
vm._s = _s;
vm._e = _e;
// 执行渲染函数生成vnode
const vnode = render.call(vm);
// 将vnode转成真实的DOM元素
patch(vm, vm._vnode, vnode, null);
// 保存旧的vnode
vm._vnode = vnode;
}
update(vm, el);
}
return {
mount,
updateComponent
}
}
解析代码逻辑:
挂载元素时执行mount,其实也是执行updateComponent函数,然后Vue初始化完成后更新视图时也是执行updateComponent函数,updateComponent调用update方法
update函数,里面主要包括了
render,调用miniCompiler生成render函数- 定义了
VNode类 - 定义
patch函数,处理视图更新的主要逻辑 - 定义了
createElement,由render函数调用 - 定义了render函数需要使用的
_c、_s、_v、_e函数 - 执行
render生成vnode - 执行
patch,接收vm,旧vnode,新vnode,父元素作为参数,更新视图 - 保存旧的
vnode到vm._vnode中,用于下次更新时用
patch函数,处理视图更新,主要逻辑为:
- 执行
createElm,根据vnode创建真实的dom,处理属性、事件、静态文本、创建子元素等,生成真实的DOM,挂载到vnode.elm中 - 移除文档中旧的元素节点
- 视图更新完成
实现效果
总结
- 实现的过程还是很复杂的,我利用工作之余的时间加周末,阅读源码加自己动手实现,也花了半个月左右的时间
- 要写好文章也很难,要记录自己对源码理解、也要关注写的文章其他人能否读懂,真的挺难。挺耗时间的。
- 但是又总会想到雷总的一句话
加油!
附录
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Compiler</title>
</head>
<body>
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count * 2 }}</div>
</section>
<script>
function miniCompiler(el) {
function compile(template = '') {
const ast = parse(template);
// console.log('alan->ast', ast);
const code = generate(ast);
const render = createFunction(code);
return render;
}
function parse(template = '') {
// 获取元素所有属性
function getAttrs(el) {
const attributes = el.attributes;
const attrs = []; // 收集属性
const attrMap = {}; // 收集属性的map
const events = {}; // 收集事件@xxx
let ifStatment = {}; // 收集v-if
for (const key in attributes) {
if (Object.hasOwnProperty.call(attributes, key)) {
const item = attributes[key];
attrMap[item.name] = item.value;
attrs.push({
name: item.name,
value: item.value,
});
if (item.name.startsWith('@')) { // 处理事件
events[item.name.replace('@', '')] = { value: item.value }
}
if (item.name === 'v-if') { // 处理v-if
ifStatment = { exp: item.value }
}
}
}
return { attrs, attrMap, events, ifStatment };
}
// 解析插值
function parseExpressionVar(str = "") {
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
const matchs = [...str.matchAll(reg)] || [];
const res = [];
if (matchs.length) {
matchs.forEach((item) => {
res.push({
raw: item[0],
name: String(item[1]).trim(),
index: item.index,
});
});
}
return res;
}
// 遍历元素
function walkElement(el, parent) {
const ast = createASTElement();
ast.parent = parent;
ast.tag = el.tagName.toLowerCase();
// 获取当前元素的所有属性
const { attrs, attrMap, events, ifStatment } = getAttrs(el);
ast.attrs = attrs;
ast.attrMap = attrMap;
ast.events = events;
if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
ast.if = ifStatment
}
const children = Array.from(el.children);
if (children.length) { // 如果有子元素,递归遍历收集所有子元素
children.forEach((child) => {
const childAST = walkElement(child, ast);
ast.children.push(childAST);
});
} else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
const childVNodes = [...el.childNodes];
if (childVNodes.length) {
const text = childVNodes[0].nodeValue
.trim()
.replace(" ", "")
.replace("\n", " ")
.trim(); // 去除空格和换行
// 创建空的ast,文本节点增加text属性
const textAst = createASTElement();
textAst.text = text;
textAst.expression = {
values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
};
ast.children.push(textAst);
}
}
return ast;
}
const tempDOM = document.createElement("div");
tempDOM.innerHTML = template;
const templateDOM = tempDOM.children[0];
const ast = walkElement(templateDOM, null);
return ast;
}
// 将ast转化成render函数的函数体的字符串
function generate(ast = {}) {
// 构建子元素
const genElmChildren = (children = []) => {
let str = "[";
children.forEach((child, i) => {
str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
});
return str + "]";
};
// 构建data
const genData = (ast = {}) => {
const data = {}
// 处理事件
if (ast.events && Object.keys(ast.events).length) {
data.on = ast.events;
}
// 处理属性
if (ast.attrs && ast.attrs.length) {
data.attrs = {}
ast.attrs.forEach(item => {
const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
let key;
let value;
if (!skip) {
if (item.name.startsWith(':')) { // parse :class
key = item.name.replace(':', '');
if (data.attrs[key]) {
const oldVal = data.attrs[key]
const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
value = `${oldVal} ${valList.join(' ')}`
}
} else {
key = item.name;
value = item.value;
}
}
data.attrs[key] = value;
})
}
return data;
};
// 构建_c()
const genElm = (ast) => {
let str = "";
if (ast['if'] && ast['if'].exp) { // 处理v-if
let elStr = ''
if (ast.tag) {
elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
}
// v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
str += `${ast['if'].exp} ? ${elStr} : _e()`
} else if (ast.tag) {
// 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
} else if (ast.text) { // 处理文本节点
// 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
if (ast.expression && ast.expression.values.length) {
// 解析插值语法
const replaceVarWithFn = (name, target = "") => {
const toReplace = `' + _s(${name})`;
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
let newStr = "";
newStr = target.replaceAll(reg, (item) => {
const matchs = [...item.matchAll(reg)] || [];
let tempStr = "";
if (matchs.length) {
matchs.forEach((matItem) => {
const mated = matItem[1];
if (mated && mated.trim() === name) {
tempStr = item.replaceAll(reg, toReplace);
}
});
}
return tempStr;
});
return newStr;
};
let varName = "";
ast.expression.values.forEach((item) => {
varName += replaceVarWithFn(item.name, ast.text);
});
str += `_v('${varName})`;
} else {
// 静态文本
str += `_v('${ast.text}')`;
}
}
return str;
};
let code = genElm(ast);
return code;
}
function createASTElement(tag, attrs, parent) {
return {
tag,
attrsMap: {},
parent,
children: []
}
}
function createFunction(code = '') {
return new Function(`
with(this) {
return ${code};
}
`)
}
// 获取元素和模板字符串
el = el || document.getElementById('app');
const template = el.outerHTML;
// 执行编译
const compiled = compile(template);
// console.log('alan->compiled', compiled);
return {
render: compiled
}
}
function createMiniVue(config) {
const {mount, updateComponent} = config;
MiniVue.prototype.$mount = mount;
class MiniWatcher {
vm = null; // 当前vue/vue组件实例
cb = () => { }; // 回调函数
getter = () => { }; // 取值函数
expression = ''; // watch的键名
user = false; // 是否是用户定义的watch
value; // 当前观察的属性的值
constructor(vm, expOrFn, cb, options = {}) {
this.vm = vm;
this.cb = cb;
this.expression = expOrFn;
// 对getter做区分处理
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parseExpression(this.expression, vm, this);
}
this.user = options.user;
// 初始化lazy
this.lazy = !!options.lazy;
// 增加对computed的处理
this.value = this.lazy ? undefined : this.get();
}
get() {
const value = this.getter.call(this.vm);
return value;
}
update() {
nextTick(() => {
this.run();
})
}
run() {
// 获取新值和旧值
const newValue = this.get();
const oldValue = this.value;
this.value = newValue;
this.cb.call(this.vm, newValue, oldValue);
if (this.vm._renderWatcher) {
this.vm._renderWatcher.cb();
}
}
// 新增computed用的计算值的函数
evaluate() {
this.value = this.get();
}
}
class MiniDep {
static target = null;
subs = [];
depend(sub) {
if (sub && !this.subs.includes(sub)) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach(sub => {
sub && sub.update();
})
MiniDep.target = null;
}
}
// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
return () => {
MiniDep.target = watcher;
// 取值,触发getter,取值前先把watcher实例放到target中
const value = vm.data[key];
// 取完值后,清空Dep.target
MiniDep.target = null;
return value;
}
}
function nextTick(cb) {
return Promise.resolve().then(cb);
}
function MiniVue(options = {}) {
const vm = this;
this.$el = options.el;
this.vm = this;
this.data = options.data;
this.watch = options.watch;
this.deps = new Set();
initData(vm, this.data); // 初始化data
initWatch(this.watch); // 初始化watch
initMethods(options.methods); // 初始化methods
function observe(data) {
for (const key in data) {
defineReactive(data, key);
}
}
function defineReactive(data, key) {
const dep = new MiniDep();
vm.deps.add(dep);
const clonedData = JSON.parse(JSON.stringify(data));
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend(MiniDep.target || vm._renderWatcher);
return clonedData[key];
},
set: function reactiveSetter(value) {
dep.notify();
clonedData[key] = value;
return value;
}
});
}
function initData(vm, data = {}) {
for (const key in data) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get() {
return vm['data'][key];
},
set(val) {
vm['data'][key] = val;
}
})
observe(vm.data);
}
}
function initWatch(watch = {}) {
for (const key in watch) {
new MiniWatcher(vm, key, watch[key], { user: true }); // user = true,标记这是用户定义的watch
}
}
function initMethods(methods = {}) {
for (const key in methods) {
if (Object.hasOwnProperty.call(methods, key)) {
const method = methods[key];
Object.defineProperty(vm, key, {
value: method.bind(vm)
})
}
}
}
vm._renderWatcher = new MiniWatcher(this, () => { }, () => {
updateComponent(vm, vm.$el)
})
}
return {
MiniVue
};
}
function miniRender() {
function mount(el) {
if (!(el instanceof HTMLElement)) {
el = document.querySelector(el);
}
updateComponent(this, el);
}
function updateComponent(vm, el) {
function update(vm, el) {
const { render } = miniCompiler(el);
class VNode {
constructor(tag, data, children) {
this.tag = tag;
this.data = data;
this.children = children;
this.elm = undefined;
this.context = undefined;
this.text = undefined;
}
}
function patch(vm, oldVNode, vnode, parentElm) {
if (!parentElm) {
parentElm = document.body;
}
if (!oldVNode) {
oldVNode = vm.$el;
const emptyVNode = new VNode(oldVNode.tagName.toLowerCase(), {}, []);
oldVNode = emptyVNode;
oldVNode.elm = vm.$el;
}
const createElm = (vnode, parentElm) => {
const { tag, data = {}, children = [] } = vnode || {};
if (tag) {
const elm = document.createElement(tag);
const { attrs = {}, on = {} } = data;
for (const key in attrs) {
if (Object.hasOwnProperty.call(attrs, key)) {
const value = attrs[key];
elm.setAttribute(key, value);
}
}
for (const key in on) {
if (Object.hasOwnProperty.call(on, key)) {
if (on[key].value) {
const event = vm[on[key].value]
event && elm.addEventListener(key, event)
}
}
}
if (children && children.length) {
children.forEach((childVNode) => {
createElm(childVNode, elm);
});
}
vnode.elm = elm;
parentElm.appendChild(elm);
} else if (vnode.text) {
textNode = document.createTextNode(vnode.text);
parentElm.innerHTML = textNode.nodeValue;
}
};
createElm(vnode, parentElm);
parentElm.removeChild(oldVNode.elm);
}
function createElement(tag = "div", data = {}, children = []) {
const createVNode = (tag = "div", data = {}, children = []) => {
const vnodeChildren = [];
if (children && children.length) {
children.forEach((child) => {
vnodeChildren.push(child);
});
}
const vnode = new VNode(tag, data, vnodeChildren);
return vnode;
};
const vnode = createVNode(tag, data, children);
return vnode;
}
function _c(tag = "div", data = {}, children = []) {
return createElement(tag, data, children)
}
function _v(str) {
const vnode = new VNode();
vnode.text = str;
return vnode;
}
function _s(val) {
return String(val);
}
function _e() {
return new VNode();
}
// 挂载render函数中需要使用的_c、_v、_s、_e
vm._c = _c;
vm._v = _v;
vm._s = _s;
vm._e = _e;
// 执行渲染函数生成vnode
const vnode = render.call(vm);
// 将vnode转成真实的DOM元素
patch(vm, vm._vnode, vnode, null);
vm._vnode = vnode;
}
update(vm, el);
}
return {
mount,
updateComponent
}
}
const renderHelpers = miniRender();
const { MiniVue } = createMiniVue(renderHelpers);
const app = new MiniVue({
el: document.getElementById("app"),
data: {
count: 0
},
methods: {
plus() {
this.count += 1;
}
}
})
app.$mount('#app');
</script>
</body>
</html>