概念:vue 的核心概念就是数据驱动;所谓数据驱动指,视图是由数据驱动生成的,我们对视图的修改不会直接操作DOM,而是通过修改数据进行视图的更新。相较于传统的操作DOM进行开发,大大简化了代码量,只关心数据的修改让代码的逻辑变得非常清晰,DOM变成了数据的映射,所有的逻辑都是对数据的操作。
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
app.$mount('#app')
首先要铭记一点,vue本身是一个函数,是通过给原型 prototype
和自身添加方法
// 申明一个 vue 函数
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 初始化
}
new Vue() // new vue 后发生的事件如下
初始化this.init()
如下:
export function initMixin(Vue: typeof Component) {
// 此处就是在 vue 函数中调用的 this._init()
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
.......
if (options && options._isComponent) {
initInternalComponent(vm, options as any)
} else {
// 这里在合并事件
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
// expose real self 核心的事件在这里,初始化生命周期、初始化事件中心等
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) { // 在这里判断,如果有 el 属性的话,就进行挂载vm,把模板渲染成最终的 DOM
vm.$mount(vm.$options.el)
}
}
}
核心步骤有三步:
1、通过mergeOptions()
方法合并事件(例子:在进行mixin混入的时候,就会把各钩子函数进行合并到parent上面
2、初始化生命周期、初始化事件中心等等,在这里可以看到before Create
和created
是在initState前后,initState是初始化props、data、watch、computed、methods等,也可以得到这些数据是在创建后才可以使用。
3、在这里判断,如果有 el 属性的话,就进行挂载vm,把模板渲染成最终的 DOM
vue的实例挂载是通过调用$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 | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) { // 判断是否有 render 方法
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode
if (__DEV__) {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (__DEV__ && 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() // 生成虚拟 node
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating) // 更新 dom
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher( // 核心方法,进行监听
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
// 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
方法中主要执行了以下四步:
1、执行 render
方法,生成虚拟 vnode
2、执行update
方法,更新dom
3、通过 new watcher()
进行监听,在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。
4、函数最后判断为根节点的时候设置 vm._isMounted
为 true
, 表示这个实例已经挂载了,同时执行 mounted
钩子函数。 这里注意 vm.$vnode
表示 Vue 实例的父虚拟 Node,所以它为 Null
则表示当前是根 Vue 的实例。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
let vnode
try {
setCurrentInstance(vm)
currentRenderingInstance = vm
// 核心代码调用了 render() 方法
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
}
vnode.parent = _parentVnode
return vnode
}
为方便阅读,只展示核心代码,在render
方法的执行还要调用vm.$createElement
方法,在vm.$createElement
方法中又调用了vnode
方法进一步生成虚拟节点(vnode)。在这里就不一一罗列代码,只要记住render方法最终通过vm.$createElement
方法方法进行生成vnode。
vm.$createElement
方法会调用vnode
方法,来生成虚拟dom。vue的vnode方法是借鉴了三方库snabbdom(链接:[https://github.com/snabbdom/snabbdom/blob/master/README-zh_CN.md]),这个三方库可以更加简便的将数据转换为虚拟dom。
export default class VNode {
tag?: string
data: VNodeData | undefined
children?: Array<VNode> | null
text?: string
elm: Node | undefined
ns?: string
context?: Component // rendered in this component's scope
key: string | number | undefined
componentOptions?: VNodeComponentOptions
componentInstance?: Component // component instance
parent: VNode | undefined | null // 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 // async component factory function
asyncMeta: Object | void
isAsyncPlaceholder: boolean
ssrContext?: Object | void
fnContext: Component | void // real context vm for functional nodes
fnOptions?: ComponentOptions | null // for SSR caching
devtoolsMeta?: Object | null // used to store functional render context for devtools
fnScopeId?: string | null // functional scope id support
isComponentRootElement?: boolean | null // for SSR directives
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode> | null,
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并返回。
首先写一个例子:
var app = new Vue({
el: '#app',
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
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 */) // 核心方法 __patch__
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode) // 核心方法 __patch__
}
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
let wrapper: Component | undefined = vm
while (
wrapper &&
wrapper.$vnode &&
wrapper.$parent &&
wrapper.$vnode === wrapper.$parent._vnode
) {
wrapper.$parent.$el = wrapper.$el
wrapper = wrapper.$parent
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
update
方法的核心是调用了__patch__
方法。
patch
方法的逻辑比较复杂,而且代码数百行,在 src/core/vdom/patch.js
地址下的 createPatchFunction
方法定义。
patch
函数的执行过程,看几个关键步骤:
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
', or missing
. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
在这个方法中的核心调用是createElm
方法。
由于我们传入的 oldVnode
实际上是一个 DOM container,所以 isRealElement
为 true,接下来又通过 emptyNodeAt
方法把 oldVnode
转换成 VNode
对象,然后再调用 createElm
方法,这个方法在这里非常重要,来看一下它的实现:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
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)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
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)
}
}
createElm
的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。 我们来看一下它的一些关键逻辑,createComponent
方法目的是尝试创建子组件,这个逻辑在之后组件的章节会详细介绍,在当前这个 case 下它的返回值为 false;接下来判断 vnode
是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台 DOM 的操作去创建一个占位符元素。
接下来调用 createChildren
方法去创建子元素:
createChildren(vnode, children, insertedVnodeQueue)
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(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)))
}
}
createChildren
的逻辑很简单,实际上是遍历子虚拟节点,递归调用 createElm
,这是一种常用的深度优先的遍历算法,这里要注意的一点是在遍历过程中会把 vnode.elm
作为父容器的 DOM 节点占位符传入。
接着再调用 invokeCreateHooks
方法执行所有的 create 的钩子并把 vnode
push 到 insertedVnodeQueue
中。
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
最后调用 insert
方法把 DOM
插入到父节点中,因为是递归调用,子元素会优先调用 insert
,所以整个 vnode
树节点的插入顺序是先子后父,看一下他的方法实现。
insert(parentElm, vnode.elm, refElm)
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
insert
逻辑很简单,调用一些 nodeOps
把子节点插入到父节点中,下面是ndoeOps
里的核心方法。
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
其实就是调用原生 DOM 的 API 进行 DOM 操作,动态创建 DOM。
再回到 patch
方法,首次渲染我们调用了 createElm
方法,这里传入的 parentElm
是 oldVnode.elm
的父元素,在我们的例子是 id 为 #app
div 的父元素,也就是 Body;实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。
最后,我们根据之前递归 createElm
生成的 vnode
插入顺序队列,执行相关的 insert
钩子函数
六、总结,整个事件的执行过程:
1、new Vue().$mount
2、首先进行实例化通过 this.init() 函数,是在 vue 函数中执行
3、实例化的时候进行挂载,因为 vue(position) 中会接收参数,调用的是 vue 上 prototype 的方法 m o u n t ( ) , 最终返 v m . mount(), 最终返vm.mount(),最终返vm.mount(vm.$options.el)
4、在 vm.$mount 中的 $mount 是调用 vue 上最初的 mount() 方法 mount.call(this, el, hydrating)
5、mount() 方法中调用 mountComponent() 方法,在这里会调用 vm._render() 生成虚拟 node,也会调用 vm._update() 进行更新dom
6、_render() 方法中调用 VNode() 方法生成虚拟 DOM
7、在 _render() 中调用 vm.$createElement,通过此方法进行规范化子节点 children 生成 vnode 的数组
8、之后调用 _update(),在此方法中再次调用一个核心方法 patch(preVnode, Vnode)
9、pathch 最后会返回一个 patch(oldVnode, vnode, hydrating, removeOnly),通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,然后再调用 createElm 方法
10、ceateElm 方法中调用 createChildren 方法,遍历子虚拟节点,递归调用 createElm,在遍历过程中会把 vnode.elm 作为父容器的 DOM 节点占位符传入
11、再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中
12、最后调用 insert() 方法,把子节点插入到父节点中,实际就是在操作 dom 的 api,动态生成 dom,渲染到页面
你可能感兴趣的:(vue.js,javascript,前端,前端框架)