小伙伴们是否在面试中因为造火箭的问题而苦恼?
我不想抱怨什么,因为我就是那种让面试者造火箭的面试官:)
本文将从底层逻辑分析引起这种现状的原因并提出诚恳建议。
视频教程
村长特地录制了配套视频,手把手带领大家撸出自己的mini vue
:
从手写Vue到面试策略分析
欢迎各位小伙伴三连+关注,您的鼓励是我坚持下去的最大动力❤️
为什么面试会造火箭
因为市场供需关系变了,以前是小甜甜,现在是牛夫人。尤其初中级前端不像6、7年前那会儿随便找工作。竞争对手多了,用人单位就更挑剔,给同样的钱,当然选更有能力、有干劲的员工。同时面试官们发现大家简历千篇一律:熟练使用vue框架和全家桶,熟练使用element-ui
、iView
等组件库,熟练使用axios
获取服务端数据等等。此时如果不提高面试难度,很难区分面试者能力高下。
遇到造火箭问题怎么办
比如面试官问:
- 为什么需要数据响应式?
vue
中是怎么实现的? - 为什么需要虚拟
dom
?diff
过程是怎样的?
小伙伴们选择去找答案,背下来,你这样解决不了问题,因为背的答案是死的,经不住推敲,稍微追问几下就露馅了。
我认为大家应该借此契机好好学习一下源码,不仅能找到问题答案,加深对API理解,还能学到很多算法、设计模式和工程化知识,对提高编程水平很有帮助。
源码学起来很困难怎么办
很多小伙伴也想通过阅读源码学习,但源码通常很庞大繁杂,很容易劝退。建议大家从一个mini
版的实现入手。先打下一个很好的基础,掌握之后再去看源码就会简单不少。
造个火箭试试
我就以Vue
为例,写一个mini
版,然后我们再考虑那些造火箭问题。
Vue的设计理念
开始之前我们先看一下Vue的设计理念,这样后面写起来会更容易理解:
易理解、友好、性能好、易维护、可测试
代码中感受一下
没有使用Vue
,01-no-vue.html:
<div id="app"></div>
<script>
// 需求:
// 1.有个title标题,想要显示在h3标签中
// 2.2秒后title会变化
const title = '我就是个标题'
const h3 = document.createElement('h3')
h3.textContent = title
app.appendChild(h3)
setTimeout(() {
h3.textContent = '我还是那个标题,但我变了'
}, 2000);
</script>
特点是:
- 用户要直接接触dom
- dom操作也是业务一部分
- 用户心智负担更重,开发效率底下
使用Vue
,02-with-vue.html:
<div id="app">
<h3>{{title}}</h3>
</div>
<script src="http://unpkg.com/vue"></script>
<script>
// 需求:
// 1.有个title标题,想要显示在h3标签中
// 2.2秒后title会变化
new Vue({
data() {
return {
title: '我就是个标题'
}
},
mounted() {
setTimeout(() {
this.title = '我还是那个标题,但我变了'
}, 2000);
},
}).$mount('#app')
</script>
重要变化是我们的app以数据驱动,能避免DOM操作,那么我们的目标就很明确了:
- 要能知道数据发生变化
- 变化之后能执行视图更新
造个轮子试试
基本结构:Vue
构造函数和$mount
方法
<div id="app"></div>
<script>
function Vue(options) {}
Vue.prototype.$mount = function() {}
</script>
利用defineProperty
实现数据响应式,监控data
中数据变化
function Vue(options) {
// 响应式
this.$options = options
this.$data = options.data()
observe(this.$data)
}
Vue.prototype.$mount = function () {}
// 遍历obj所有key做响应式处理
function observe(obj) {
Object.keys(obj).forEach(key {
defineReactive(obj, key, obj[key])
})
}
// 所谓响应式就是拦截对象属性访问
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() { return val },
set(newVal) { val = newVal }
}
})
}
挂载:准备一个更新函数,负责视图初始化和后续更新
<script>
Vue.prototype.$mount = function (sel) {
// 创建更新函数
this.update = function () {
const child = this.$options.render.call(this)
const parent = document.querySelector(sel)
if (!this.isMounted) {
// init
parent.appendChild(child)
this.isMounted = true
if (this.$options.mounted) {
this.$options.mounted.call(this)
}
} else {
// update
parent.innerHTML = ''
parent.appendChild(child)
}
}
this.update()
}
function observe(obj) {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {},
set(newVal) {
if (newVal !== val) {
// 触发更新
app.update();
}
}
})
}
</script>
<script>
const app = new Vue({
// 添加render函数负责渲染dom
render() {
const h3 = document.createElement('h3')
h3.textContent = this.$data.title
return h3
}
})
</script>
问题:每次更新都是全量更新视图
基于vnode
实现,避免全量更新
<script>
Vue.prototype.$mount = function (sel) {
this.update = function () {
// 执行render获取vnode
const vnode = this.$options.render.call(this, this.createElement)
if (!this.isMounted) {
// init patch:传入parent是dom
const parent = document.querySelector(sel)
this.patch(parent, vnode)
} else {
// update patch:传入两个vnode做diff
this.patch(this._vnode, vnode)
}
this._vnode = vnode
}
this.update()
}
// 加一个vnode生成函数
Vue.prototype.createElement = function (tag, props, children) {
return { tag, props, children }
}
// patch用于初始化或更新时转换vnode为dom
Vue.prototype.patch = function (n1, n2) {
if (n1.nodeType) {
// init
const child = this.createElm(n2)
n1.appendChild(child)
n2.$el = child
} else {
// update
}
}
// 递归创建元素
Vue.prototype.createElm = function (vnode) {
const {tag, props, children} = vnode
const el = document.createElement(tag)
// 创建children
if (Array.isArray(children)) {
// element
children.forEach(child el.appendChild(createElm(children)))
} else {
// text
el.textContent = children
}
vnode.$el = el
return el
}
</script>
<script>
const app = new Vue({
// render返回vnode
render(h) {
return h('h3', null, this.$data.title)
}
})
app.$mount('#app')
</script>
更新逻辑:主要看双方children类型,针对性做dom操作,此处仅解决了测试用例中的文本情况
Vue.prototype.patch = function (n1, n2) { if (n1.nodeType) {} else { // 获取待操作dom const el = n2.$el = n1.$el // children更新 if (n1.tag === n2.tag) { // 是否相同节点,节点复用 if (typeof n1.children === 'string') { if (typeof n2.children === 'string') { // text update if (n1.children !== n2.children) { el.textContent = n2.children } } else { // replace text with elements } } else { if (typeof n2.children === 'string') { // replace elements with text } else { // update children } } } else { // replace } }
再来思考回答策略
可使用四段体:介绍概念,说必要性,源码如何实现,实践中如何使用。例如:
- 为什么需要数据响应式?怎么执行?
介绍概念:数据响应式是MVVM这类框架中侦测数据变化的机制,三大框架中各不相同(知道就发挥一下)
必要性:MVVM最重要的任务就是实现数据驱动,要实现数据驱动就必须要知道数据何时发生变化,从而做出响应,这就需要一套数据响应式机制。
源码实现:vue 2.x中主要利用defineProperty,vue 3.x中主要利用Proxy(不知道就不提)。以vue 2.x为例通过遍历对象属性,定义get/set,做属性拦截,将来数据变化,就可以感知,并调用更新函数使视图更新。
结合实践:实践中,我们传入组件的属性props,方法methods,数据data,都会在Vue初始化的时候统一做响应式处理,因此当它们发生变化,视图就会重新渲染,得以更新。也有些特殊情况,比如有新属性添加或删除,需要使用Vue.set/delete这样的API。
当然,小伙伴还要继续学习不少细节,比如:
- 怎么通知视图更新(依赖收集,异步更新等知识点)
- 视图是怎么更新的(虚拟dom和patch)
- vue 2.x中响应式有啥问题(效率、额外api、数组处理等)
- 为什么需要Vue.set/delete这样的API
你看全是和响应式这个点引申出来的,如果回答得当,基本妥妥的。
范例源码
关注公众号「村长学前端
」自取
视频教程
村长特地录制了配套视频,手把手带领大家撸出自己的mini vue
:
从手写Vue到面试策略分析