数据驱动作为Vue.js的核心思想之一,是指视图由数据生成,想要对视图做出修改时,不同于jQuery等前端库直接操作DOM,而是通过修改数据来影响视图。这样大大简化了代码量,在开发过程中只关心数据也使得代码逻辑变得非常清晰。如下:
<div id="app">
{{message}}
</div>
const vm = new Vue({
el: '#app',
data: {
message: 'Hello World'
}
})
最终会在页面上渲染出 Hello World。接下来我们逐步分析这一过程是如何实现的。
通过上一篇的分析,当执行new Vue(options)时,会执行this._init(options),我们找到_init方法的定义:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm = this
// ...此处省略多行代码
// merge options
// ...此处省略多行代码,合并options到this.$options上
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// ...此处省略多行代码
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
其中initState(vm)与我们上面的demo相关,进入initState(vm)方法中。
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// initializing options accroding to corresponding `opts[keys]`
// ...此处省略多行代码,分别处理props、methods
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// ...处理computed、watch
}
在我们的demo中opts.data存在,调用initData(vm),进入initData(vm)方法中。
function initData (vm: Component) {
let data = vm.$options.data
// check if data is a function or object
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// ... 此处省略多行代码
// 判断methods、props、data的字段如果有冲突,抛出警告,否则执行下面逻辑
if (!isReserved(key)) {
// 对data做proxy处理,this.xxx实际上相当于this._data.xxx
proxy(vm, `_data`, key)
}
}
// observe data
// 数据响应式,在此处暂且不关注
observe(data, true /* asRootData */)
}
在initData方法中,对data做了proxy处理,这样一来,访问this.xxx时实际上就相当于访问了this._data.xxx,而initData函数内部将data赋值给了this._data。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
至此data的初始化完成,回到上面_init方法中,还有一个vm.$mount(vm.$options.el)没有执行,也就是挂载。接下来我们看挂载过程。
因为我们使用的是Runtime+Compiler版本的Vue.js,所以执行vm.$mount()时,进入src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) //转化成DOM对象
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
// 没有定义render函数,就把模板编译成render函数
let template = options.template
if (template) {
// 定义了template
// ...
} else if (el) {
// 未定义template
template = getOuterHTML(el)
}
if (template) {
// 编译相关
// ...
}
}
// 定义了render函数,直接调用
return mount.call(this, el, hydrating)
}
看到这儿会发现,Vue的prototype上已经拥有了$mount方法(定义在runtime/index.js),在此将该方法保存到变量上,之后又重新定义了$mount方法。这又是什么原因呢?
因为在 Runtime+Compiler 版本中,我们可能定义了template字段,而在 Runtime-Only 版本中,只能定义render函数。当调用$mount方法时,如果定义了render函数,直接调用原来的$mount方法;如果没有,通过重新定义的$mount方法将template转为render函数之后,再调用原来的$mount方法。
我们再来进入runtime/index.js中的$mount方法。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
很简单,内部执行了mountComponent方法。接下来进入该方法。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
// 如果没有编译出render函数,创建一个空的VNode
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
// 定义updateComponent函数
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 渲染Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) callHook(vm, 'beforeUpdate')
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
在mountComponent方法中定义了updateComponent函数(用作渲染的函数),随后执行了new Watcher(),简单看一下Watcher的定义
class Watcher{
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
)
if (typeof expOrFn === 'function') this.getter = expOrFn
this.value = this.lazy ? undefined : this.get()
}
get () {
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
在Watcher中将第二个参数赋值给了getter,随后调用get方法时执行了getter,等同于updateComponent函数被执行。也等同于vm._update(vm._render(),hydrating),那么接下来从vm._render开始探讨。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// render self
// 在生产环境下vm._renderProxy = vm,开发环境下要做简单处理,具体见initProxy()方法
let vnode = render.call(vm._renderProxy, vm.$createElement)
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
return vnode
}
可以看出vm._render会返回一个值vnode。具体过程是,首先从$options解构出render函数,并调用。传入两个参数,vm._renderProxy在生产环境下直接用vm赋值,在开发环境下要经过initProxy方法处理(代码见init.js);vm.$createElement定义在initRender函数(已在init.js执行)中,在函数内调用了createElement方法
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
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)
进入createElement方法
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
首先对参数做了重载,随后调用了_createElement方法
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 对children做normalization,最终统一形式[vnode, vnode, ...]
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// tag是字符串
if (typeof tag === 'string') {
let Ctor
if (config.isReservedTag(tag)) {
// 如果是保留标签
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
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
}
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方法中,首先对children做 normalization,最终生成统一形式[vnode, vnode, ...],随后进入typeof tag === 'string'的逻辑,div属于保留标签,创建相应的vnode,最后返回。
至此,vm._render函数就执行完毕了(_createElement -> createElement -> $createElement -> render -> _render)。回退到vm._update(vm._render(),hydrating),还剩最后一步执行vm.update。vm._update定义在lifecycle.js中,代码如下:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
我们找到vm.__patch__方法,定义如下:
Vue.prototype.__patch__ = inBrowser ? patch : noop
再找到patch方法,定义如下:
// module中定义了一些钩子函数,用于生成attrs、class、style...
const modules = platformModules.concat(baseModules)
// nodeOps定义了一些DOM操作方法
export const patch: Function = createPatchFunction({ nodeOps, modules })
再找到createPatchFunction方法,在createPatchFunction最后,我们看到返回了一个patch函数。
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend){
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ... 此处省略几百行代码,多为辅助函数
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
return vnode.elm
}
}
那Vue.js为什么要绕这么一大圈定义patch函数呢?
实际上此处是利用了函数科里化的技巧,因为Vue.js是支持多端的。在调用createPatchFunction时,把与平台相关的的参数传入,简化了真正patch函数的逻辑。
在patch函数中,首先把传入的oldVnode(上面调用时传入的DOM)通过emptyNodeAt方法转化为vnode,随后根据vnode获取到当前元素(div)和父级元素(body),调用createElm方法。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 创建DOM
vnode.elm = nodeOps.createElement(tag, vnode)
// 创建children
createChildren(vnode, children, insertedVnodeQueue)
// 插入
insert(parentElm, vnode.elm, refElm)
} 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)
}
}
在createElm方法中,先根据标签创建相应 DOM 元素,如果有children,调用createChildren(函数内部递归调用了createElm),最终调用insert将整个新生成的插入父节点,此时页面已经渲染出了最终内容。但还没有结束,打开控制台,查看element会发现有两个div节点,我们还需调用removeVnodes删除旧的节点,至此整个页面的初次渲染就宣告结束了。
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
总结一下,从new Vue()开始,经历了init -> $mount -> compile/render -> vnode -> patch -> DOM,以上就是将一个简单节点从数据初始化至渲染到页面的基本流程。下一篇我们来讨论将组件渲染至页面的情况。