前言
文章是对自己学习探究Vue.js
的总结和分享,也是以前的笔记,整理了一下发出来与大家一同学习。
时至今日,Vue.js
已经被广泛的前端开发者使用,那么去探寻其内部的运行机制也是我们的必修课。模板和数据如何渲染成最终的DOM
的呢?
数据驱动
数据驱动是Vue.js
一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。例如在页面中的摸板语法``只要你去改变vue实例中的message
值,页面上就能渲染你期待的值。
模版
模板引擎的概念是:字符串 + 数据 => html
。这个概念,与我们如今谈的Vue
或 React
并没有什么不同。这其实就是 组件的本质。组件的本质虽然没变,但组件的产出却改变了。在模板引擎的年代,组件的产出是 html
字符串,而 Vue
或 React
,它们的组件所产出的内容并不是 html
字符串,而是大家所熟知的 Virtual DOM
Virtual DOM
Virtual DOM
建立在 DOM
之上,是基于 DOM
的一层抽象,实际可理解为用更轻量的纯 JavaScript 对象(树)描述 DOM
(树)。
操作 JavaScript 对象当然比操作 DOM 快,因为不用更新视图。我们可以随意改变 Virtual DOM ,然后找出改变再更新到 DOM 上。但要保证高效,需要解决以下问题:
- 高效的 diff 算法,即两个 Virtual DOM 的比较;
- 只更新需要更新的 DOM 节点;
- 数据变化检测,batch DOM 读写操作
new Vue
new
关键字在 Javascript 语言中用来表示一个对象的实例化,而在Vue
实际上是一个类,
// src/core/instance/index.js
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
可以看到 Vue
只能通过 new
关键字初始化,然后会调用 this._init
方法,简单的看下源码。
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag //性能追踪
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}` // 在组件初始化开始时,添加开始标志的`mark`类型实体
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.function Object() { [native code] }),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
顺带一提vue
在代码中开启了性能追踪主要是通过performance.mark()
方法在浏览器的性能缓冲区中使用给定名称添加一个timestamp(时间戳),定义的时间戳实体可以通过getEntries()
,getEntriesByName()
或getEntriesByType()
等方法获取。
Vue
初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher ,最后检测到如果有 el
属性,则调用 vm.$mount
方法挂载 vm
,挂载的目标就是把模板渲染成最终的 DOM
在经过合并选项、初始化生命周期、初始化事件、初始化事件等组件初始化操作完成后添加结束标志的mark
类型实体。最后调用measure
方法生成measure
类型的实体,该实体的名称为vue name init
,其中name
为组件名称。这样在浏览器开发工具的性能/时间线面板中的Timings
中就能看到名为vue name init
的相关信息。
Vue 实例挂载
// public mount method 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);
};
$mount
这个方法的实现是和平台、构建方式都相关的。(Runtime Only
)版本的Vue
中,调用的就是这个$mount
函数。而在完整版(Compiler
)的Vue
中的 $mount
实现则是对其进行了重写。
// src/platform/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element, //参数的类型检查表明`el`可以是字符串或`DOM`节点
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
这里先拿到了Runtime Only
版本的$mount
方法,然后进行重写,最后又调用了Runtime Only
版本的$mount
方法。具体流程如下
- 这段代码首先缓存了
$mount
方法,再重新定义该方法。 - 调用
query
方法,如果el
是一个字符串,就调用querySelector
获取节点并返回;如果节点不存在就抛出警告并创建一个div
节点。如果el
是一个节点就直接返回。 - 它对
el
做了限制,Vue
不能挂载在body、html
这样的根节点上。如果是就抛出警告并停止挂载。因为挂载实际上就是把el
节点替换为组件的模版。 - 如果没有定义
render 方法
,则会把el
或者template
字符串转换成render
方法。所有 Vue 的组件的渲染最终都需要render
方法 - 判断
template
类型做相应处理,转为了字符串模板
- 调用缓存的
$mount
方法进行挂载,主要是把el
从字符串转换成了节点然后传给了mountComponent
函数
mountComponent的调用过程
mountComponent
方法的逻辑也是非常清晰的,它会完成整个渲染工作我们看下他的源码
// src/core/instance/lifecycle.js
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
...... //省略部分代码
}
}
callHook(vm, "beforeMount");
let updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
updateComponent = () => {
const name = vm._name;
const id = vm._uid;
// 性能追踪
const startTag = `vue-perf-start:${id}`;
const endTag = `vue-perf-end:${id}`;
mark(startTag);
const vnode = vm._render();
mark(endTag);
measure(`vue ${name} render`, startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure(`vue ${name} patch`, startTag, endTag);
};
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
}
// 简单描述一下这个过程:初始化这个watcher对象,执行updateComponent方法,收集相关的依赖
// updateComponent的执行过程:
// 先执行vm._render方法,根据之前生成的render方法,生成相关的vnode,也就是virtual dom相关的内容
// 通过生成的vnode,调用vm._update,最终将vnode生成的dom插入到父节点中,完成组件的载入
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
hydrating = false
// `vm.$vnode` 表示 Vue 实例的父虚拟 Node,所以它为 `Null` 则表示当前是根 Vue 的实例
if (vm.$vnode == null) {
vm._isMounted = true
// 调用mounted钩子,在这个钩子的回调函数中可以访问到真是的dom节点,
// 因为在上述过程中已经将真实的dom节点插入到父节点
callHook(vm, 'mounted')
}
return vm
}
-
先将
el
保存到vm.$el
上,然后判断前面的template
是否被正确的转换成了render
函数。如果转换失败,将createEmptyVNode
作为render
函数,其会创建一个空的VNode
对象。 if
语句里面是我们熟悉的性能追踪
,else
语句里这里面定义了一个updateComponent
函数,涉及到两个函数:_render
: 调用vm.$options.render
函数并返回生成的虚拟节点(VNode
)_update
: 将VNode
渲染成真实DOM
- 创建了一个
Watcher
实例,Watcher
会解析表达式,收集依赖关系,并且在表达式的值发生改变时触发回调。Watcher
在这里主要有两个作用: 一个是初始化的时候会执行回调函数;另一个是当vm
实例中监测的数据发生变化的时候执行回调函数,而回调函数就是传入的updateComponent
函数。 -
函数最后判断为根节点的时候设置
vm._isMounted
为true
, 表示这个实例已经挂载,并执行mounted
钩子函数。所以其实
mountComponent
核心就是先实例化一个渲染Watcher
,并在它的回调函数中会调用updateComponent
方法,在此方法中调用vm._render
方法先生成虚拟Node
,最终调用vm._update
更新DOM