最新的Vue面试题大全含源码级回答,吊打面试官系列

const original = arrayProto[method]

def(arrayMethods, method, function mutator (…args) {

const result = original.apply(this, args)

notify()

return result

})

})

这是数据劫持的部分,接下来说下视图更新的机制:

  1. 由于 Vue 执行一个组件的 render 函数是由 Watcher 去代理执行的,Watcher 在执行前会把 Watcher 自身先赋值给 Dep.target 这个全局变量,等待响应式属性去收集它。

  2. 在组件执行render函数时访问了响应式属性,响应式属性就会精确的收集到当前全局存在的 Dep.target 作为自身的依赖。

  3. 在响应式属性发生更新时通知 Watcher 去重新调用vm._update(vm._render())进行组件的视图更新,视图更新的时候会通过diff算法对比新老vnode差异,通过patch即时更新DOM。

2.v-if和v-for哪个优先级高


答案是v-for解析的优先级高,可以在源码的compiler/codegen/index.js 里的genElement函数找到答案

function genElement (el: ASTElement, state: CodegenState): string {

if (el.parent) {

el.pre = el.pre || el.parent.pre

}

if (el.staticRoot && !el.staticProcessed) {

return genStatic(el, state)

} else if (el.once && !el.onceProcessed) {

return genOnce(el, state)

} else if (el.for && !el.forProcessed) {

return genFor(el, state)

} else if (el.if && !el.ifProcessed) {

return genIf(el, state)

} else if (el.tag === ‘template’ && !el.slotTarget && !state.pre) {

return genChildren(el, state) || ‘void 0’

} else if (el.tag === ‘slot’) {

return genSlot(el, state)

} else {

// component or element

let code

if (el.component) {

code = genComponent(el.component, el, state)

} else {

let data

if (!el.plain || (el.pre && state.maybeComponent(el))) {

data = genData(el, state)

}

const children = el.inlineTemplate ? null : genChildren(el, state, true)

code = `_c(' e l . t a g ′ {el.tag}' el.tag{

data ? ,${data} : ‘’ // data

}${

children ? ,${children} : ‘’ // children

})`

}

// module transforms

for (let i = 0; i < state.transforms.length; i++) {

code = state.transforms[i](el, code)

}

return code

}

}

vue中的内置指令都有相应的解析函数,执行顺序是通过简单的if else-if语法来确定的。在genFor的函数里,最后会return一个自运行函数,再次调用genElement。

虽然v-for和v-if可以放一起,但我们要避免这种写法,在官网中也有明确指出,这会造成性能浪费。

3.key的作用


作用:用来判断虚拟DOM的某个节点是否为相同节点,用于优化patch性能,patch就是计算diff的函数。

先看下patch函数:

只提取了本次要分析的关键代码

function patch (oldVnode, vnode) {

if (isUndef(vnode)) {

if (isDef(oldVnode)) invokeDestroyHook(oldVnode)

return

}

let isInitialPatch = false

const insertedVnodeQueue = []

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 {

// some code

}

}

return vnode

}

patch函数接收oldVnode和vnode,也就是要比较的新旧节点对象。

首先会用isUndef函数判断传入的两个vnode是否为空对象再做相应处理。当两个都为节点对象时,再用sameVnode来判断是否为同一节点,再判断本次操作是新增、修改、还是移除。

function sameVnode (a, b) {

return (

a.key === b.key // key值

&&

(

a.tag === b.tag && // 标签名

a.isComment === b.isComment && // 是否为注释节点

isDef(a.data) === isDef(b.data) && // 是否都定义了data,data包含一些具体信息,例如onclick , style

sameInputType(a, b) // 当标签是的时候,type必须相同

)

)

}

sameVnode通过判断key、标签名、是否为注释、data等是否相等,来判断是否需要进行比较。

值得比较则执行patchVnode,不值得比较则用Vnode替换oldVnode,再渲染真实dom。

patchVnode会对oldVnode和vnode进行对比,然后进行DOM更新。这个会在diff算法里再进行说明。

v-for通常都是生成一样的标签,所以key会是patch判断是否相同节点的唯一标识,如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,就会去做pathVnode pdateChildren的更新操作,这造成了大量的dom更新操作,所以设置唯一的key是必要的。

4.双向绑定原理


vue中双向绑定是一个指令v-model,可以绑定一个动态值到视图,同时视图中变化能改变该值。v-model是语法糖,默认情况下相当于:value和@input。

通常在表单元素可以直接使用v-model,这是vue解析的时候对这些表单元素进行了处理,根据控件类型自动选取正确的方法来更新元素。

v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

如果是自定义组件的话要使用它需要在组件内绑定props value并在数据更新数据的时候用$emit(‘input’),也可以在组件里定义modal属性来自定义绑定的属性名和事件名称。

model: {

prop: ‘checked’,

event: ‘change’

}

5.nextTick原理


先看下官方文档的说明:

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

nextTick就是将回调函数放到队列里去,保证在异步更新DOM的watcher后面,从而获取到更新后的DOM。

结合src/core/util/next-tick源码再进行分析。

首先是定义执行任务队列方法

function flushCallbacks () {

pending = false

const copies = callbacks.slice(0)

callbacks.length = 0

for (let i = 0; i < copies.length; i++) {

copiesi

}

}

按照推入callbacks队列的顺序执行回调函数。

然后定义timerFunc函数,根据当前环境支持什么方法来确定调用哪个异步方法

判断的顺序是: Promise > MutationObserver > setImmediate > setTimeout

最后是定义nextTick方法:

export function nextTick (cb?: Function, ctx?: Object) {

let _resolve

callbacks.push(() => {

if (cb) {

try {

cb.call(ctx)

} catch (e) {

handleError(e, ctx, ‘nextTick’)

}

} else if (_resolve) {

_resolve(ctx)

}

})

if (!pending) {

pending = true

timerFunc()

}

if (!cb && typeof Promise !== ‘undefined’) {

return new Promise(resolve => {

_resolve = resolve

})

}

}

其实nextTick就是一个把回调函数推入任务队列的方法。

6.data为什么是函数


如果组件里 data 直接写了一个对象的话,那么在模板中多次声明这个组件,组件中的 data 会指向同一个引用。

此时对 data 进行修改,会导致其他组件里的 data 也被修改。使用函数每次都重新声明一个对象,这样每个组件的data都有自己的引用,就不会出现相互污染的情况了。

7.组件通信方式


1. props和$on$emit

适合父子组件的通信,通过props传递响应式数据,父组件通过$on监听事件、子组件通过$emit发送事件。

on和emit是在组件实例初始化的时候通过initEvents初始化事件,在组件实例vm._events赋值一个空的事件对象,通过这个对象实现事件的发布订阅。下面是事件注册的几个关键函数:

// 组件初始化event对象,收集要监听的事件和对应的回调函数

function initEvents (vm: Component) {

vm._events = Object.create(null)

vm._hasHookEvent = false

// init parent attached events

const listeners = vm.$options._parentListeners

if (listeners) {

updateComponentListeners(vm, listeners)

}

}

// 注册组件监听的事件

function updateComponentListeners (

vm: Component,

listeners: Object,

oldListeners: ?Object

) {

target = vm

updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)

target = undefined

}

2. ref$parent$children,还有$root

  • ref: 在普通DOM元素上声明就是DOM元素的引用,组件就是指向组件实例。

  • $parent:访问组件的父组件实例

  • $children:访问所有的子组件集合(数组)

  • $root: 指向root实例

3. Event Bus

通常是创建一个空的Vue实例作为事件总线(事件中心),实现任何组件在这个实例上的事件触发与监听。原理就是一个发布订阅的模式,跟$on``$emit一样,在实例化一个组件的事件通过initEvents初始化一个空的event对象,再通过实例化后的这个bus(vue实例)手动的$on$emit添加监听和触发的事件,代码在src/core/instance/events:

Vue.prototype.$on = function (event: string | Array, fn: Function): Component {

const vm: Component = this

// 传入的事件如果是数组,就循环监听每个事件

if (Array.isArray(event)) {

for (let i = 0, l = event.length; i < l; i++) {

vm.$on(event[i], fn)

}

} else {

// 如果已经有这个事件,就push新的回调函数进去,没有则先赋值空数组再push

(vm._events[event] || (vm._events[event] = [])).push(fn)

// instead of a hash lookup

if (hookRE.test(event)) {

vm._hasHookEvent = true

}

}

return vm

}

Vue.prototype.$emit = function (event: string): Component {

const vm: Component = this

let cbs = vm._events[event]

// 循环调用要触发的事件的回调函数数组

if (cbs) {

cbs = cbs.length > 1 ? toArray(cbs) : cbs

const args = toArray(arguments, 1)

const info = event handler for "${event}"

for (let i = 0, l = cbs.length; i < l; i++) {

invokeWithErrorHandling(cbs[i], vm, args, vm, info)

}

}

return vm

}

4. attrs、listeners

  • $attrs: 包含了父作用域没被props声明绑定的数据,组件可以通过v-bind="$attrs"继续传给子组件

  • $listernes: 包含了父作用域中的v-on(不含 .native 修饰器的) 监听事件,可以通过v-on="$listeners"传入内部组件

5. provide、inject

父组件通过provide注入一个依赖,其所有的子孙组件可以通过inject来接收。要注意的是官网有这一段话:

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

所以Vue不会对provide中的变量进行响应式处理。要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的。实际上在很多高级组件中都可以看到组件会将this通过provide传递给子孙组件,包括element-ui、ant-design-vue等。

6. vuex 状态管理实现通信

vuex是专为vue设计的状态管理模式。每个组件实例都有共同的store实例,并且store.state是响应式的,改变state唯一的办法就是通过在这个store实例上commit一个mutation,方便跟踪每一个状态的变化,实现原理在下面的vuex原理里有讲。

8.computed、watch、method有什么区别


computed:有缓存,有对应的watcher,watcher有个lazy为true的属性,表示只有在模板里去读取它的值后才会计算,并且这watcher在初始化的时候会赋值dirty为true,watcher只有dirty为true的时候才会重新求值,重新求值后会将dirty置为false,false会直接返回watcher的value,只有下次watcher的响应式依赖有更新的时候,会将watcher的dirty再置为false,这时候才会重新求值,这样就实现了computed的缓存。

watch:watcher的对象每次更新都会执行函数。watch 更适用于数据变化时的异步操作。如果需要在某个数据变化时做一些事情,使用watch。

method: 将方法在模板里使用,每次视图有更新都会重新执行函数,性能消耗较大。

9.生命周期


官网对生命周期的说明:

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

生命周期就是每个Vue实例完成初始化、运行、销毁的一系列动作的钩子。

基本上可以说8 个阶段创建前/后,载入前/后,更新前/后,销毁前/后。

  • 创建前/后: 在 beforeCreate 阶段,vue 实例的挂载元素 el 还没有。

  • 载入前/后:在 beforeMount 阶段,vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换。在 mounted 阶段,vue 实例挂载完成,data.message 成功渲染。

  • 更新前/后:当 data 变化时,会触发 beforeUpdate 和 updated 方法。

  • 销毁前/后:在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 vue 实例已经解除了事件监听以及和 dom 的绑定,但是 dom 结构依然存在

结合源码再理解,在源码中生命周期钩子是用callHook函数调用的。看下callHook函数:

function callHook (vm: Component, hook: string) {

pushTarget()

const handlers = vm.$options[hook]

const info = ${hook} hook

if (handlers) {

for (let i = 0, j = handlers.length; i < j; i++) {

invokeWithErrorHandling(handlers[i], vm, null, vm, info)

}

}

if (vm._hasHookEvent) {

vm.$emit(‘hook:’ + hook)

}

popTarget()

}

接收一个vm组件实例的参数和hook,取组件实例的$options传入的hook属性值,有的话会循环调用这个钩子的回调函数。在调用生命钩子的回调函数之前会临时pushTarget一个null值,也就是将Dep.target置为空来禁止在执行生命钩子的时候进行依赖收集。

vm.$emit(‘hook:’ + hook)则是用来给父组件监听该组件的回调事件。

接下来看每个生命钩子具体调用的时机。

1. beforeCreate、created:

Vue.prototype._init = function (options?: Object) {

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’)

if (vm.$options.el) {

vm. m o u n t ( v m . mount(vm. mount(vm.options.el)

}

}

在执行beforeCreate之前调用了 initLifecycle、initEvents、initRender函数,所以beforeCreate是在初始化生命周期、事件、渲染函数之后的生命周期。

在执行created之前调用了initInjections、initState、initProvide,这时候created初始化了data、props、watcher、provide、inject等,所以这时候就可以访问到data、props等属性。

2. beforeMount、mounted

3. beforeUpdate、updated

这两个钩子函数是在数据更新的时候进行回调的函数。在src/core/instance/lifecycle.js找到beforeUpdate调用的代码:

new Watcher(vm, updateComponent, noop, {

before () {

if (vm._isMounted && !vm._isDestroyed) {

callHook(vm, ‘beforeUpdate’)

}

}

}, true /* isRenderWatcher */)

_isMounted为ture的话(DOM已经被挂载)会调用callHook(vm, ‘beforeUpdate’)方法,然后会对虚拟DOM进行重新渲染。然后在/src/core/observer/scheduler.js下的flushSchedulerQueue()函数中渲染DOM,flushSchedulerQueue会刷新watcher队列并执行,执行完所有watcher的run方法之后(run方法就是watcher进行dom diff并更新DOM的方法),再调用callHook(vm, ‘updated’),代码如下:

/**

  • Flush both queues and run the watchers.

*/

function flushSchedulerQueue () {

for (index = 0; index < queue.length; index++) {

watcher = queue[index]

if (watcher.before) {

watcher.before()

}

watcher.run()

}

callUpdatedHooks(updatedQueue)

}

function callUpdatedHooks (queue) {

let i = queue.length

while (i–) {

const watcher = queue[i]

const vm = watcher.vm

if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {

callHook(vm, ‘updated’)

}

}

}

4. beforeDestroy、destroyed

这两个钩子是vue实例销毁的钩子,定义在Vue.prototype.$destroy中:

Vue.prototype.$destroy = function () {

const vm: Component = this

if (vm._isBeingDestroyed) {

return

}

callHook(vm, ‘beforeDestroy’)

vm._isBeingDestroyed = true

// remove self from parent

const parent = vm.$parent

if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {

remove(parent.$children, vm)

}

// teardown watchers

if (vm._watcher) {

vm._watcher.teardown()

}

let i = vm._watchers.length

while (i–) {

vm._watchers[i].teardown()

}

// remove reference from data ob

// frozen object may not have observer.

if (vm._data.ob) {

vm._data.ob.vmCount–

}

// call the last hook…

vm._isDestroyed = true

// invoke destroy hooks on current rendered tree

vm.patch(vm._vnode, null)

// fire destroyed hook

callHook(vm, ‘destroyed’)

// turn off all instance listeners.

vm.$off()

// remove vue reference

if (vm.$el) {

vm.$el.vue = null

}

if (vm.$vnode) {

vm.$vnode.parent = null

}

}

}

在销毁之前执行callHook(vm, ‘beforeDestroy’),然后销毁的时候做了几件事:

  • 如果有父元素,将父元素的$children中把该组件实例移除。

  • 移除watchers,并在依赖订阅者中移除自己。

  • 删除数据引用

5. activated、deactivated

剩下的还有activated、deactivated、errorCaptured三个钩子函数。

activated、deactivated这两个钩子函数分别是在keep-alive 组件激活和停用之后的回调。

errorCaptured捕获到当子孙组件错误时会被调用,在源码中可以经常看到try catch中catch会调用handleError函数,handleError会向组件所有的父级组件抛出异常,

function handleError (err: Error, vm: any, info: string) {

pushTarget()

try {

if (vm) {

let cur = vm

while ((cur = cur.$parent)) {

const hooks = cur.$options.errorCaptured

if (hooks) {

for (let i = 0; i < hooks.length; i++) {

try {

const capture = hooks[i].call(cur, err, vm, info) === false

if (capture) return

} catch (e) {

globalHandleError(e, cur, ‘errorCaptured hook’)

}

}

}

}

}

globalHandleError(err, vm, info)

} finally {

popTarget()

}

}

分析完源码再一下官网图示,会更清楚:

10.keep-aliva原理


keep-alive是Vue.js的一个内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

include与exclude两个属性,允许组件有条件地进行缓存,max属性确定最多缓存多少组件实例。

keep-alive是一个组件,跟其他组件一样有生命周期和render函数,keep-alive包裹的分析keep-alive就是分析一个组件。

源码再src/core/components/keep-alive,created声明了要缓存的组件对象,和存储的组件keys,keep-alive销毁的时候会用pruneCacheEntry将缓存的所有组件实例销毁,也就是调用组件实例的destroy方法。在挂载完成后监听include和exclude,动态地销毁已经不满足include的组件和满足exclude的组件实例:

created () {

this.cache = Object.create(null) // 存储需要缓存的组件

this.keys = [] // 存储每个需要缓存的组件的key,即对应this.cache对象中的键值

},

// 销毁keep-alive组件的时候,对缓存中的每个组件执行销毁

destroyed () {

for (const key in this.cache) {

pruneCacheEntry(this.cache, key, this.keys)

}

},

mounted () {

this.$watch(‘include’, val => {

pruneCache(this, name => matches(val, name))

})

this.$watch(‘exclude’, val => {

pruneCache(this, name => !matches(val, name))

})

},

接下来是render函数:

render () {

const slot = this.$slots.default

const vnode: VNode = getFirstComponentChild(slot)

// 如果vnode存在就取vnode的选项

const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions

if (componentOptions) {

// check pattern

//获取第一个有效组件的name

const name: ?string = getComponentName(componentOptions)

const { include, exclude } = this

if (

// not included

(include && (!name || !matches(include, name))) ||

// excluded

(exclude && name && matches(exclude, name))

) {

return vnode// 说明不用缓存,直接返回这个组件进行渲染

}

// 匹配到了,开始缓存操作

const { cache, keys } = this // keep-alive组件的缓存组件和缓存组件对应的key

// 获取第一个有效组件的key

const key: ?string = vnode.key == null

// same constructor may get registered as different local components

// so cid alone is not enough (#3269)

? componentOptions.Ctor.cid + (componentOptions.tag ? ::${componentOptions.tag} : ‘’)
vnode.key

if (cache[key]) {

// 这个组件的实例用缓存中的组件实例替换

vnode.componentInstance = cache[key].componentInstance

// make current key freshest

// 更新当前key在keys中的位置

remove(keys, key)

keys.push(key)

} else {

cache[key] = vnode

keys.push(key)

// prune oldest entry

// 如果缓存中的组件个数超过传入的max,销毁缓存中的LRU组件

// LRU: least recently used 最近最少用,缓存淘汰策略

if (this.max && keys.length > parseInt(this.max)) {

pruneCacheEntry(cache, keys[0], keys, this._vnode)

}

}

vnode.data.keepAlive = true

}

// 若第一个有效的组件存在,但其componentOptions不存在,就返回这个组件进行渲染

// 或若也不存在有效的第一个组件,但keep-alive组件的默认插槽存在,就返回默认插槽的第一个组件进行渲染

return vnode || (slot && slot[0])

}

代码做了详细的注释,这里再分析下render做了什么。

通过this.$slots.default拿到插槽组件,也就是keep-alive包裹的组件,getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode(vnode节点描述对象,vue通过vnode创建真实的DOM)

匹配到了就开始缓存,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。并且通过remove(keys, key),将当前的key从keys中删除再重新keys.push(key),这样就改变了当前key在keys中的位置。这个是为了实现max的功能,并且遵循缓存淘汰策略。

如果没匹配到,说明没缓存过,这时候需要进行缓存,并且判断当前缓存的个数是否超过max指定的个数,如果超过,则销毁keys里的最后一个组件,并从keys中移除,这个就是LRU(Least Recently Used :最近最少使用 )缓存淘汰算法。

最后返回vnode或者默认插槽的第一个组件进行DOM渲染。

12.虚拟dom和diff算法


虚拟DOM是对DOM的描述,用对象属性来描述节点,本质上是JavaScript对象。它有几个意义:

  1. 具备跨平台的优势

由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器、小程序、Node、原生应用、服务端渲染等等。

  1. 提升渲染性能

频繁变动DOM会造成浏览器的回流或者重回,而通过将大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,可以减少真实DOM的操作次数,从而提高性能。

  1. 代码可维护性更高

通过虚拟 DOM 的抽象能力,可以用声明式写 UI 的方式,大大提高了我们的工作效率。

在vue中template最终会转成render函数,而render函数最终是执行的createElement,生成vnode,vnode正是 vue中用来表示虚拟DOM的类,看下vnode:

class VNode {

tag: string | void;

data: VNodeData | void;

children: ?Array;

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,

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

}

}

看下其中关键的几个属性:

  • tag: 当前节点的标签名

  • data: 表示节点上的class,attribute,style以及绑定的事件

  • children: 当前节点的子节点,是一个数组

  • text: 当前节点的文本

  • elm: 当前虚拟节点对应的真实dom节点

  • key: 节点的key属性,被当作节点的标志,用以优化

  • componentOptions: 组件的option选项

  • componentInstance: 当前节点对应的组件的实例

  • parent: 当前节点的父节点

  • isStatic: 是否为静态节点

children和parent是指当前的vnode的子节点和父节点,这样一个个vnode就形成了DOM树。

diff算法发生在视图更新的时候,也就是数据更新的时候,diff算法会将新旧虚拟DOM作对比,将变化的地方转换为DOM

当某个数据被修改的时候,依赖对应的watcher会通知更新,执行渲染函数会生成新的vnode,vnode再去与旧的vnode进行对比更新,这就是vue中的虚拟dom diff算法触发的流程。

看下组件更新的_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.KaTeX parse error: Expected group after '_' at position 9: el = vm._̲_patch__(vm.el, vnode, hydrating, false /* removeOnly */)

} else {

// updates

vm.$el = vm.patch(prevVnode, vnode)

}

}

vm.$el = vm._patch(),这个就是最终渲染的DOM元素,patch就是vue中diff算法的函数,在key的作用章节有提过。patch将新旧虚拟DOM节点比较后,最终返回真实的DOM节点。

patch

看下patch代码(部分):

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {

/vnode不存在则直接调用销毁钩子/

if (isUndef(vnode)) {

if (isDef(oldVnode)) invokeDestroyHook(oldVnode)

return

}

let isInitialPatch = false

const insertedVnodeQueue = []

if (isUndef(oldVnode)) {

// empty mount (likely as component), create new root element

isInitialPatch = true

createElm(vnode, insertedVnodeQueue, parentElm, refElm)

} else {

/标记旧的VNode是否有nodeType/

/Github:https://github.com/answershuto/

const isRealElement = isDef(oldVnode.nodeType)

if (!isRealElement && sameVnode(oldVnode, vnode)) {

// patch existing root node

/是同一个节点的时候直接修改现有的节点/

patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)

return vnode.elm

首先是判断是否有新的vnode,没有代表是要销毁旧的vnode,调用销毁组件的钩子。

然后判断是否有旧的vnode,没有代表是新增,也就是新建root节点。

接下来判断旧的vnode是否是真实的元素,而不是组件,如果是组件并且用someVnode判断新旧节点是否是相同的节点(sameVnode在key的作用章节有做解析),是进行patchVnode,这时候进行真正的新老节点的diff。只有相同的节点才会进行diff算法!!!

patchVnode

function patchVnode (

oldVnode,

vnode,

insertedVnodeQueue,

ownerArray,

index,

removeOnly

) {

// 两个vnode相同,说明不需要diff,直接返回

if (oldVnode === vnode) {

return

}

// 如果传入了ownerArray和index,可以进行重用vnode,updateChildren里用来替换位置

if (isDef(vnode.elm) && isDef(ownerArray)) {

// clone reused vnode

vnode = ownerArray[index] = cloneVNode(vnode)

}

const elm = vnode.elm = oldVnode.elm

// 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return

if (isTrue(oldVnode.isAsyncPlaceholder)) {

if (isDef(vnode.asyncFactory.resolved)) {

hydrate(oldVnode.elm, vnode, insertedVnodeQueue)

} else {

vnode.isAsyncPlaceholder = true

}

return

}

/*

如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),

并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),

那么只需要替换elm以及componentInstance即可。

*/

if (isTrue(vnode.isStatic) &&

isTrue(oldVnode.isStatic) &&

vnode.key === oldVnode.key &&

(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))

) {

vnode.componentInstance = oldVnode.componentInstance

return

}

let i

const data = vnode.data

if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {

i(oldVnode, vnode)

}

const oldCh = oldVnode.children

const ch = vnode.children

最后

技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。

接下来判断旧的vnode是否是真实的元素,而不是组件,如果是组件并且用someVnode判断新旧节点是否是相同的节点(sameVnode在key的作用章节有做解析),是进行patchVnode,这时候进行真正的新老节点的diff。只有相同的节点才会进行diff算法!!!

patchVnode

function patchVnode (

oldVnode,

vnode,

insertedVnodeQueue,

ownerArray,

index,

removeOnly

) {

// 两个vnode相同,说明不需要diff,直接返回

if (oldVnode === vnode) {

return

}

// 如果传入了ownerArray和index,可以进行重用vnode,updateChildren里用来替换位置

if (isDef(vnode.elm) && isDef(ownerArray)) {

// clone reused vnode

vnode = ownerArray[index] = cloneVNode(vnode)

}

const elm = vnode.elm = oldVnode.elm

// 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return

if (isTrue(oldVnode.isAsyncPlaceholder)) {

if (isDef(vnode.asyncFactory.resolved)) {

hydrate(oldVnode.elm, vnode, insertedVnodeQueue)

} else {

vnode.isAsyncPlaceholder = true

}

return

}

/*

如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),

并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),

那么只需要替换elm以及componentInstance即可。

*/

if (isTrue(vnode.isStatic) &&

isTrue(oldVnode.isStatic) &&

vnode.key === oldVnode.key &&

(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))

) {

vnode.componentInstance = oldVnode.componentInstance

return

}

let i

const data = vnode.data

if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {

i(oldVnode, vnode)

}

const oldCh = oldVnode.children

const ch = vnode.children

最后

技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。

[外链图片转存中…(img-LsfMyzoA-1714723245572)]

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值