烂大街的问题,但却不是每个人都能回答到位。因为如果你只是看看别人写的网文,通常没什么底气,也经不住面试官推敲,但像我们这样即看过源码还造过轮子的,回答这个问题就会比较有底气。
vue2响应式:
https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/index.js#L135-L136
vue3响应式:
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L89-L90
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/ref.ts#L67-L68
题目分析:vue基于虚拟DOM做更新,diff又是其核心部分,因此常被问道,此题考查面试者深度。
必问题目,涉及vue更新原理,比较考查理解深度。
1.Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。
2.最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。
3.vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。
4.patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例:
双向绑定是vue的特色之一,开发中必然会用到的知识点,然而此题还问了实现原理,升级为深度考查。
知其所以然:测试代码
<div id="demo"> <input type="text" v-model="foo"> <input type="checkbox" v-model="bar"> <select v-model="baz"> <option value="vue">vue</option> <option value="react">react</option> </select> </div> <script src="../dist/vue.js"></script> <script> // 创建实例 const app = new Vue({ el: '#demo', data: { foo: 'foo', bar: true, baz: 'react' } }); console.log(app.$options.render); </script>
观察输出的渲染函数:
// <input type="text" v-model="foo"> _c('input', { directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], attrs: { "type": "text" }, domProps: { "value": (foo) }, on: { "input": function ($event) { if ($event.target.composing) return; foo = $event.target.value } } })
// <input type="checkbox" v-model="bar"> _c('input', { directives: [{ name: "model", rawName: "v-model", value: (bar), expression: "bar" }], attrs: { "type": "checkbox" }, domProps: { "checked": Array.isArray(bar) ? _i(bar, null) > -1 : (bar) }, on: { "change": function ($event) { var $$a = bar, $$el = $event.target, $$c = $$el.checked ? (true) : (false); if (Array.isArray($$a)) { var $$v = null, $$i = _i($$a, $$v); if ($$el.checked) { $$i < 0 && (bar = $$a.concat([$$v])) } else { $$i > -1 && (bar = $$a.slice(0, $$i).concat($$a.slice($$i + 1))) } } else { bar = $$c } } } })
// <select v-model="baz"> // <option value="vue">vue</option> // <option value="react">react</option> // </select> _c('select', { directives: [{ name: "model", rawName: "v-model", value: (baz), expression: "baz" }], on: { "change": function ($event) { var $$selectedVal = Array.prototype.filter.call( $event.target.options, function (o) { return o.selected } ).map( function (o) { var val = "_value" in o ? o._value : o.value; return val } ); baz = $event.target.multiple ? $$selectedVal : $$selectedVal[0] } } }, [ _c('option', { attrs: { "value": "vue" } }, [_v("vue")]), _v(" "), _c('option', { attrs: { "value": "react" } }, [_v("react")]) ])
这是一道特别常见的问题,主要考查大家对虚拟DOM和patch细节的掌握程度,能够反映面试者理解层次。
源码中找答案:src\core\vdom\patch.js - sameVnode()
此题考查常识,文档中曾有详细说明v2|v3;也是一个很好的实践题目,项目中经常会遇到,能够看出面试者应用能力。
在 Vue 2
中,v-for
优先于 v-if
被解析;但在 Vue 3
中,则完全相反,v-if
的优先级高于 v-for
。
我曾经做过实验,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件
实践中也不应该把它们放一起,因为哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表。
通常有两种情况下导致我们这样做:
为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive"
)。此时定义一个计算属性 (比如 activeUsers
),让其返回过滤后的列表即可。
为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers"
)。此时把 v-if
移动至容器元素上 (比如 ul
、ol
)即可。
文档中明确指出永远不要把 v-if
和 v-for
同时用在同一个元素上,显然这是一个重要的注意事项。
看过源码里面关于代码生成的部分,
在 Vue 2
中做个测试,
两者同级时,渲染函数如下:
ƒ anonymous( ) { with(this){return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return (item.isActive)?_c('div',{key:item.id},[_v("\n "+_s(item.name)+"\n ")]):_e()}),0)} }
在 Vue 3
中做个测试,
两者同级时,渲染函数如下:
(function anonymous( ) { const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, toDisplayString: _toDisplayString, createCommentVNode: _createCommentVNode } = _Vue return shouldShowUsers ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(items, (item) => { return (_openBlock(), _createElementBlock("div", { key: item.id }, _toDisplayString(item.name), 1 /* TEXT */)) }), 128 /* KEYED_FRAGMENT */)) : _createCommentVNode("v-if", true) } } })
源码中找答案:
Vue 2
:compiler/codegen/index.js
Vue 3
:compiler-core/src/codegen.ts