前端框架核心学习(一)
框架在现代的前端开发中占据举足轻重的地位。
目前流行的前端框架:Angular、React 和 Vue,它们各有特点和受众,都值得开发者认真思考和学习。
那么我们在精力有限的情况下,如何做到「触类旁通」、如何提取框架共性、提高学习和应用效率呢?
我们将用三篇文章来带大家一块进行前端框架核心的学习。
相关核心知识:
核心关键词:
-
双向绑定 -
依赖收集 -
发布订阅模式 -
MVVM/MVC -
虚拟DOM -
虚拟DOM diff -
模版编译
响应式框架的基本原理
关于响应式原理和双向绑定的基本概念,网上有很多。我们从直观的行为上来看:
当页面中数据发生变化时,不用在代码层面重新开发相关逻辑去更新视图变化,视图会自己更新。
这一过程中,需要了解下几个点:
-
视图变化依赖哪些数据 => 依赖收集 -
如何感知到数据变化 => 数据劫持 / 数据代理 -
数据变化时,视图是怎么知道,并且更新的 => 发布订阅模式
数据劫持以及代理
感知数据变的方式很简单:数据劫持或者数据代理。通常的方式是通过 Object.defineProperty 实现,其定义了两个方法:getter 和 setter。
let data = {
stage: 'GitChat',
course: {
title: '前端面试宝典',
author: '小助手',
publishTime: '2022/04/20'
}
}
Object.keys(data).forEach(key => {
let currentValue = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log(`getting ${key} value now, getting value is:`, currentValue)
return currentValue
},
set(newValue) {
currentValue = newValue
console.log(`setting ${key} value now, setting value is`, currentValue)
}
})
})
data.course
// getting course value now, getting value is:
// {title: "前端面试宝典", author: "小助手", publishTime: "2022/04/20"}
data.course = '前端面试宝典 2'
// setting course value now, setting value is 前端面试宝典 2
但是这种实现有一个问题,例如:
data.course.title = '前端面试宝典 2'
// getting course value now, getting value is:
// {title: "前端面试宝典", author: "小助手", publishTime: "2022/04/20"}
只会有 getting course value now, getting value is: {title: "前端面试宝典", author: "小助手", publishTime: "2022/04/20" } 的输出,这是因为我们尝试读取了 data.course 信息。但是修改 data.course.title 的信息并没有打印出来。
出现这个问题的原因是因为我们的实现代码只进行了一层 Object.defineProperty,或者说只对 data 的第一层属性进行了 Object.defineProperty,对于嵌套的引用类型数据结构:data.course,我们同样应该进行拦截。
为了达到深层拦截的目的,将 Object.defineProperty 的逻辑抽象为 observe 函数,并改用递归实现:
let data = {
stage: 'GitChat',
course: {
title: '前端面试宝典',
author: '小助手',
publishTime: '2022/04/20'
}
}
const observe = data => {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
let currentValue = data[key]
observe(currentValue)
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log(`getting ${key} value now, getting value is:`, currentValue)
return currentValue
},
set(newValue) {
currentValue = newValue
console.log(`setting ${key} value now, setting value is`, currentValue)
}
})
})
}
observe(data)
这样一来,就实现了深层数据拦截:
data.course.title = '前端面试宝典 2'
// getting course value now, getting value is: {// ...} // setting title value now, setting value is 前端面试宝典 2
请注意,我们在 set 代理中,并没有对 newValue 再次递归进行
observe(newValue)。也就是说,如果赋值是一个引用类型:
data.course.title = {
title: '前端面试宝典 2'
}
无法实现对 data.course.title 数据的观察。这里为了简化学习成本,默认修改的数值符合语义,都是基本类型。
在尝试对 data.course.title 赋值时,首先会读取 data.course,因此输出:getting course value now, getting value is: {// ...},赋值后,触发 data.course.title 的 setter,输出:setting title value now, setting value is 前端面试宝典 2。
因此我们总结出:对数据进行拦截并不复杂,这也是很多框架实现的第一步。
监听数组变化
如果上述数据中某一项变为数组
let data = {
stage: 'GitChat',
course: {
title: '前端面试宝典',
author: ['小助手', '小助手2'],
publishTime: '2022/04/20'
}
}
const observe = data => {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
let currentValue = data[key]
observe(currentValue)
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log(`getting ${key} value now, getting value is:`, currentValue)
return currentValue
},
set(newValue) {
currentValue = newValue
console.log(`setting ${key} value now, setting value is`, currentValue)
}
})
})
}
observe(data)
data.course.author.push('小助手3')
// getting course value now, getting value is: {//...}
// getting author value now, getting value is: (2) [(...), (...)]
我们只监听到了 data.course 以及 data.course.author 的读取,而数组 push 行为并没有被拦截。这是因为 Array.prototype 上挂载的方法并不能触发 data.course.author 属性值的 setter,由于这并不属于做赋值操作,而是 push API 调用操作。然而对于框架实现来说,这显然是不满足要求的,当数组变化时我们应该也有所感知。
Vue 同样存在这样的问题,它的解决方法是:将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。
实现逻辑如下:
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
arrMethods.forEach(method => {
const oldMethod = Array.prototype[method]
const newMethod = function(...args) {
oldMethod.apply(this, args)
console.log(`${method} 方法被执行了`)
}
arrExtend[method] = newMethod
})
示例代码中加入了一行 console.log。使用时:
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
arrMethods.forEach(method => {
const oldMethod = Array.prototype[method]
const newMethod = function(...args) {
oldMethod.apply(this, args)
console.log(`${method} 方法被执行了`)
}
arrExtend[method] = newMethod
})
Array.prototype = Object.assign(Array.prototype, arrExtend)
let data = {
stage: 'GitChat',
course: {
title: '前端开发进阶',
author: ['Lucas', 'Ronaldo'],
publishTime: '2018 年 5 月'
}
}
const observe = data => {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
let currentValue = data[key]
observe(currentValue)
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log(`getting ${key} value now, getting value is:`, currentValue)
return currentValue
},
set(newValue) {
currentValue = newValue
console.log(`setting ${key} value now, setting value is`, currentValue)
}
})
})
}
observe(data)
data.course.author.push('Messi')
// getting course value now, getting value is: {//...}
// getting author value now, getting value is: (2) [(...), (...)]
// push 方法被执行了
对应代码:
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
arrMethods.forEach(method => {
const oldMethod = Array.prototype[method]
const newMethod = function(...args) {
oldMethod.apply(this, args)
console.log(`${method} 方法被执行了`)
}
arrExtend[method] = newMethod
})
Array.prototype = Object.assign(Array.prototype, arrExtend)
let data = {
stage: 'GitChat',
course: {
title: '前端面试宝典',
author: ['小助手1', '小助手2'],
publishTime: '2020/04/20'
}
}
const observe = data => {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
let currentValue = data[key]
observe(currentValue)
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log(`getting ${key} value now, getting value is:`, currentValue)
return currentValue
},
set(newValue) {
currentValue = newValue
console.log(`setting ${key} value now, setting value is`, currentValue)
}
})
})
}
observe(data)
data.course.author.push('小助手3')
// getting course value now, getting value is: {//...}
// getting author value now, getting value is: (2) [(...), (...)]
// push 方法被执行了
Object.defineProperty VS Proxy
我们首先尝试使用 Proxy 来完成代码重构:
let data = {
stage: 'GitChat',
course: {
title: '前端面试宝典',
author: ['小助手1', '小助手2'],
publishTime: '2020/04/20'
},
};
const observe = (data) => {
if (
!data ||
Object.prototype.toString.call(data) !== '[object Object]'
) {
return;
}
Object.keys(data).forEach((key) => {
let currentValue = data[key];
// 事实上 proxy 也可以对函数类型进行代理。
// 这里只对承载数据类型的 object 进行处理,读者了解即可。
if (typeof currentValue === 'object') {
observe(currentValue);
data[key] = new Proxy(currentValue, {
set(target, property, value, receiver) {
// 因为数组的 push 会引起 length 属性的变化,
// 所以 push 之后会触发两次 set 操作,
// 我们只需要保留一次即可,property 为 length 时,忽略
if (property !== 'length') {
console.log(
`setting ${key} value now, setting value is`,
currentValue
);
}
return Reflect.set(target, property, value, receiver);
},
});
} else {
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log(
`getting ${key} value now, getting value is:`,
currentValue
);
return currentValue;
},
set(newValue) {
currentValue = newValue;
console.log(
`setting ${key} value now, setting value is`,
currentValue
);
},
});
}
});
};
observe(data);
已经符合我们的需求了。注意这里在使用 Proxy 进行代理时,并没有对 getter 进行代理,因此上述代码的输出结果并不像之前使用 Object.defineProperty 那样也会有 getting value 输出。
整体实现并不难理解,需要了解最基本的 Proxy 知识。简单总结一下,对于数据键值为基本类型的情况,我们使用 Object.defineProperty;对于键值为对象类型的情况,继续递归调用 observe 方法,并通过 Proxy 返回的新对象对 data[key] 重新赋值,这个新值的 getter 和 setter 已经被添加了代理。
了解了 Proxy 实现之后,我们对 Proxy 实现数据代理和 Object.defineProperty 实现数据拦截进行对比,会发现:
-
Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写 -
Object.defineProperty 必须遍历对象的每个属性,且对于嵌套结构需要深层遍历 -
Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的 -
Proxy 支持代理数组的变化 -
Proxy 的第二个参数除了 set 和 get 以外,可以有 13 种拦截方法,比起 Object.defineProperty() 更加强大,这里不再一一列举 -
Proxy 性能将会被底层持续优化,而 Object.defineProperty 已经不再是优化重点
最后
《前端面试题宝典》经过近一年的迭代,现已推出 和 电脑版刷题网站 (
https://fe.ecool.fun/
),欢迎大家使用~