序
如果是用Vue
的选手的话,面试一般都会被问到响应式是如何实现的。
而如果我们直接按网上的大部分的答案(或者相近的答案):
- 1、实现一个监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者; - 2、实现一个订阅器
Dep
,用来收集订阅者,对监听器Observer
和 订阅者Watcher
进行统一管理; - 3、实现一个订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图; - 4、实现一个解析器
Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
我们很有可能会被认为是靠背
出来的,并非真正了解,就算你了解,如此答复,也有可能被认为是背
。试想,面试官并非面试过你一个人可能好几十个,如果答复都大近相同的话,很难让面试官眼前一亮。
而如果说手写过一个小demo,并且能举一反三,这b是否拿捏的很准。
而响应式无非是 数据劫持 与 发布订阅,也正如上述所描述的一样,但是我们是实现它的过程。
准备
...
<body>
<div id="app">
{{name}}
<div>
{{age}}
</div>
<input type="text" v-model="price">
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
name: 'W',
age: '18',
price: '无价'
}
})
</script>
</body>
...
数据劫持
// 先实现数据劫持
function Vue(options = {}) {
this._data = options.data || {}
this._el = options.el || '#app'
this._options = options
observe(this._data) // 劫持data所有属性
// 实现 this.name 能访问到 this._data.name(例子)
Object.keys(this._data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key]
},
set(newVal) {
this._data[key] = newVal
}
})
})
}
// Vue2使用defineProperty后没有实现的点(使用别的方法实现):
// (1)只能劫持对象已存在的属性,新增则无法劫持到
// (2)无法监听到数组内部变化,数组长度变化等
function Observe(data) {
Object.keys(data).forEach(key => {
let val = data[key]
observe(val) // 实现深度监听
Object.defineProperty(data, key, {
configurable: true,
enumerable: true, // 必须为true,否则后面无法枚举
get() {
console.log('获取', key, '值:', val)
return val // 不能使用data[key],否则会循环调用get方法
},
set(newVal) {
console.log('设置', key, '值:', newVal)
if (newVal === val) {
return
}
val = newVal
observe(val) // 判断newVal是否为对象
}
})
})
}
function observe(data) {
// console.log(data)
// console.log(typeof data)
if (!data || typeof data !== 'object') {
return
}
new Observe(data)
return
}
验证下:
Object.defineProperty
实际上是可以实现对数组的数据劫持的,那为何不实现,可能尤大大考虑到性能的问题吧。那如何改变数组内容后,触发视图更新呢,这里给出部分代码:
数组方法变异
// 不是完整代码,无法正常运行
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 类似创建了一个实例,改变实例的内容并不会影响到 Array.prototype
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
const original = arrayProto[method] // 获取原生Array方法
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
// 贴出def部分的代码
export function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
上述可能有点难懂,这里举一个简单的例子:
const push = Array.prototype.push
Array.prototype.push = function mutator(...arg) {
const result = push.apply(this, arg)
doSomething()
return result
}
function doSomething() {
console.log('do something')
}
let arr = []
arr.push(1)
arr.push(2)
解析
function Vue(options = {}) {
...
new Compile(this._el, this)
}
// 解析
function Compile(el, vm) {
vm._el = document.querySelector(el)
let fragment = document.createDocumentFragment() // 创建虚拟DOM
let child
// 将真实DOM移入到虚拟DOM中
// 遍历完毕后,真实dom为空
while (child = vm._el.firstChild) {
fragment.appendChild(child)
}
const replace = function (frag) {
Array.from(frag.childNodes).forEach((node) => {
let reg = /\{\{(.*?)\}\}/g
let text = node.textContent
// console.log('text', text)
// console.log('node.nodeType', node.nodeType)
// console.log('reg.test(text)', reg.test(text))
// 文本节点并且存在{{}}字符
if (node.nodeType === 3 && reg.test(text)) {
let arr = RegExp.$1.split('.')
let val = vm
// 如果dom存在 {{a.b}} 则获取到a对象的b属性值
arr.forEach(key => {
val = val[key]
})
// 将文本内容 替换
node.textContent = text.replace(reg, val).trim()
}
// 元素节点
if (node.nodeType === 1) {
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(att => {
let name = att.name
let exp = att.value
// console.log('att.name', att.name)
// console.log('att.value', att.value)
if (name.includes('v-model')) {
node.value = vm[exp]
}
})
}
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
// 将虚拟DOM重新导入到真实DOM中
vm._el.appendChild(fragment)
}
观察者 发布订阅
function Observe(data) {
Object.keys(data).forEach(key => {
...
+ let dep = new Dep()
get() {
...
+ Dep.target && dep.add(Dep.target)
},
set(newVal) {
...
+ dep.notify(newVal)
}
})
function Observe(data) {
return
}
val = newVal
dep.notify(newVal)
observe(val) // 判断newVal是否为对象
}
})
function Compile(el, vm) {
...
// 文本节点并且存在{{}}字符
if (node.nodeType === 3 && reg.test(text)) {
+ new Watcher(vm, RegExp.$1, function (newVal) {
+ node.textContent = text.replace(reg, newVal).trim()
+ })
}
if (node.nodeType === 1) {
if (name.includes('v-model')) {
...
+ new Watcher(vm, RegExp.$1, function (newVal) {
+ node.value = text.replace(reg, newVal).trim()
+ })
}
}
}
// 添加发布订阅
function Dep() {
this.subs = []
}
// 存Watcher实例
Dep.prototype.add = function (sub) {
this.subs.push(sub)
}
Dep.prototype.notify = function () {
this.subs.forEach(sub => {
sub.update()
})
}
// 观察者
function Watcher(vm, exp, fn) {
this.vm = vm
this.exp = exp
this.fn = fn // 回调
Dep.target = this
let val = vm
let arr = exp.split('.')
arr.forEach(exp => {
val = val[exp]
})
Dep.target = null
}
Watcher.prototype.update = function () {
let val = this.vm
let arr = this.exp.split('.')
arr.forEach(key => {
val = val[key]
})
this.fn(val)
}
验证下: 完整的git地址
分享不易额,喜欢的话一定别忘了点💖!!!
只关注不点💖的都是耍流氓,只收藏也不点💖的也一样是耍流氓。