最后
技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。
走到最后,就是observe(data)
,也就是开始处理vue数据的双向绑定
不同于react
的单向数据流,vue
使用的双向绑定,单向数据流可以理解为当源头的数据发生变动则触发行为,当然这个变动是主动的,即你需要setState
才能触发,而双向绑定则可以抽象为,每一个数据旁边都有一个监护人(一种处理逻辑),当数据发生变化,这个监护人就会响应行为,这个流程是被动发生的,只要该数据发生变动,就会通过监护人触发行为。
如果你之前有过了解,大概就会知道,js每个数据的变动都是通过Object
原型链中的setter
去改变值,而如果你在他改变值之前,去通知监护人,就能够实现上述的逻辑,这一点很多博客文章都写的非常清楚了。
接着第一部分的initData
知道最后observe(data)
,这里开始正式处理响应式。
2.1 前置条件
前面一直提到,通过Object
的原型链改变对象的默认行为:getter
和setter
,首先我们需要知道,在js
中,读取一个对象的值并不是直接读取,而是通过Object的原型链上的默认行为getter拿到对应的值,而改变这种行为实际上是通过Object.defineProperty
,来重新定义一个对象的getter
和setter
,在/src/core/observer/index.js
中我们可以看一个defineReactive
方法,他就是vue
用来实现这种行为的方法,也是这个响应式的核心
function defineReactive(obj, key, val, … ) {
// 此处需要保留getter、setter是因为,开发者可能自己基于defineProperty已经做过一层覆盖,
// 而响应式又会覆盖一次,所以为了保留开发者自己的行为,此处需要兼容原有的getter、setter
const getter = property && property.get // 拿到默认的getter、setter行为
const setter = property && property.set
Object.defineProperty(obj, key, {
enumerable: true, // 是否可以被枚举出来(例如Object.keys(),for in)
configurable: true, // 是否可以被配置,是否可以被删除
get: function() {
const value = getter ? getter.call(obj) : val
…
return value
}
set: function(newVal) {
…
setter.call(obj, newVal)
}
})
}
2. 2响应式
首先,我们猜想一下,双向绑定的行为,数据能够响应行为的变化,而行为又能够操作数据的改变,虽然有部分教程会让你站在数据的角度去理解这种行为,实际上,我们站在行为的角度上去理解是更加方便的。
我们将一种行为定义为一个Watcher
,他有可能是一个vue
文件的template
中的dom
节点渲染行为,也有可能是computed
的计算值行为,总之,我们从行为的角度出发,一个行为的发生,会伴随着对变量的读取(回想一下我们在vue
文件中的template
写html
标签时,总是会使用{{obj.xxx}}
来读取某个变量并渲染),我们想要实现,变量的改变也会带动这个行为的重新渲染,是不是我们只需要在首次行为发生的周期内,在读取某个变量时,在这个变量内记录这个Watcher
,这样的话,下次变量的改变时,我只要触发我之前记录过的Watcher
就行了。所以,我们只需要在一个Watcher
发生时,将其挂载到一个公共变量上,这样在读取一个值的时候,记录这个公共变量,就能够实现上述操作。
这里先不解释Dep的作用,可以将其抽象理解为一个被挂载在数据上的数组,每次这个数据被一个watch读取时,就会将这个watch记录下来
2.2.1 Watcher
既然说到将一种行为定义为一个watcher
,那么可以在/src/core/observer/watcher.js
中看到Watcher
的实体类,而我们之前一直所说的“行为”,实际上就是构造器的第二个参数expOrFn
,可以有表达式或者函数读取的两种模式
class Watcher {
constructor ( vm: Component, // vue实例
expOrFn: string | Function, // 行为
cb: Function, // 为watch服务
options?: ?Object,
isRenderWatcher?: boolean // 判断是否为渲染watcher, )
}
接着来看一种最典型的watcher行为,在/src/core/instance/lifecycle.js
中的moundComponent
方法中,可以看到一个实例化watcher
的方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, ‘beforeUpdate’)
}
}
}, true /* isRenderWatcher */)
可以看到,他将updateComponent
(可以抽象为渲染行为)传给Watcher
,而在Watcher
的实例化中,将会执行此方法,当然在执行之前,pushTarget(this)
,将这个watcher挂载到公共变量上而后开始执行渲染行为,
class Watch {
constructor(…) {
…
if (typeof expOrFn === ‘function’) {
this.getter = expOrFn
}
this.get();
}
get() {
pushTarget(this) // 挂载行为至公共Target
value = this.getter.call(vm, vm) // 开始执行行为,之所以会有返回值是为了computed服务
popTarget() // 取消挂载,避免下次读取变量时又会绑定此行为
}
}
此时,如果此行为读取了某个响应式变量,那么该变量的getter
将会存储公共变量target
,当行为完成后就会取消行为的挂载,这个时候我们再回过头来看前面的defineReactive
的逻辑
function defineReactive(obj, key) {
const dep = new Dep(); // 每个数据都有一个自己的存储列表
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 判断公共变量中是否挂载了行为(watcher)
dep.depend() // 将行为(watcher)加入dep(即此变量的存储行为列表)
…
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return // 判断变量没有变化,则直接返回(后两者判断则是因为NaN!==NaN的特性)
}
if (setter) {
setter.call(obj, newVal) // 开始
} else {
val = newVal
}
dep.notify() // 通知自己这个数据的存储列表,数据发生改变,需要重新执行行为(watcher)
}
});
}
这个时候就很清晰明了了,这就是很多博客文章所说的依赖收集,变量在get时通过公共变量Target
收集依赖(也就是本文所说的行为),在set
时,即变量数据发生改变时,触发更新notify
;
2.2.2 Computed
前文有大致介绍computed
的实现,实际上在介绍完Wacher之后就可以来详细介绍了,计算属性computed
并没有实际的变量,他通过原型链覆盖创造了一个变量指向(src/core/instance/state.js
的initComputed
),回忆一下computed的两种写法
‘fullName’: function() {
return this.firstName + this.secondeName;
}
‘fullName’: {
get: function () {…},
set: function() {…},
}
我们再来看一下initComputed
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
// 对照着computed的两种写法,就能理解为什么这里有这样的判断,
const getter = typeof userDef === ‘function’ ? userDef : userDef.get
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
defineComputed(vm, key, userDef) // 通过defineProperty来创造一个挂载在vm上key(fullName)的指向
}
}
可以看到,他将computed
的getter
方法,作为Watcher
的行为传递了进去,这样在执行getter
时,可以将此行为绑定至过程中所读取到的变量(firstName
),如此,再下次firstName发生改变时,就会触发此Watcher
,重新运行getter方法,得到一个新的fullName
的值(还记得前文class Watch
中的value = this.getter.call(vm, vm)
吗?这个返回值就是computed
的返回值),这样就实现了computed
的逻辑
2.2.3 Watch
watch
的用法,是监听某个变量,当该变量发生变化时,执行特定的逻辑,
上文提到的两种Watcher
行为都是函数行为,但是Watcher
的行为是支持函数或者表达式的(expOrFn
),所以此处的exp(expression)
这里就是可以提现到的,我们只需要在变量发生变化时,执行watch
定义的逻辑即可,
还记得前文代码defineReactive
中 set
方法通知依赖更新(dep.notify()
),虽然前文一直为了方便理解,将Dep描述为一种抽象的列表结构,仅用于依赖收集,但实际上他是一个单独的数据结构,
let uid = 0;
class Dep {
constructor() {
this.id = uid ++;
this.subs = []; // 真正用于收集依赖的数据
}
depend () { // 依赖收集
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
notify() { // 变量值发生变化,通知更新
// 遍历所有收集的依赖,注意触发更新,
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
…
}
Dep.target = null; // 这就是一直说的,用于挂载Watcher行为的公共变量
function pushTarget(target){ Dep.target = target };
function popTarget() { Dep.target = null };
实际上这里的静态变量
target
以及pushTarget、popTarget
是经过简化的,因为渲染并不是一个单一的行为,他是层层嵌套的行为,所以在绑定响应式时,也是需要区分该变量到底是要绑定至哪个行为(否则每个变量都绑定最顶层的行为,一个变量的变化,将会引发整个页面的update
),因此真正的target是还有一个stack
栈结构,用于挂载多个嵌套的行为
可以看到,每次变量更新,都会触发watcher.update
,那么对于watch
监听的回调,就可以放到在update
中调用
class Watch {
constructor(vm, expOrFn, cb, …) {
this.cb = cb // 这个cb就是watch监听的回调
}
update() {
this.run()
}
run() {
…
this.cb.call(this.vm, …)
}
}
至此,关于watch
监听的实现逻辑大致就是如此
关于依赖收集,实际上并不是在get变量时,直接将
watcher
绑定至Dep
中,可以看到Dep.depend()
,他先通知行为(watcher
),叫他先绑定自己,然后watcher绑定完dep之后,才会回过头,告知Dep
要addSub()
,这里的逻辑像是一个圈
所以现在我们回过头来看,前文说了,每个数据都有一个“监护人”,来记录此数据所绑定的行为,那么这个“监护人”到底在哪里呢? 可以看到/src/core/observer/index.js
的class Observer
中,
class Observer {
constructor(val) {
…
def(value, ‘ob’, this) // 对value定义__ob__属性,挂载此object
…
}
}
对于每一份需要响应式处理的数据,都会挂载一个
Observer
实例,其内subs就是用于记录绑定此数据的Watcher
,同时也可以看到,这份数据的get、set
方法已经是被重写过了,也就是前文的defineReactive
中的覆盖行为。
2.2.4 其他
其实对于Array
的响应式是需要特殊处理的,因为他除了set、get
之外,还会对数组进行增减操作(splice
等),而这些操作是set无法捕捉的,所以覆盖get、set
显然无法实现数组的响应式,而vue
中采用的是直接覆盖数组的原型链中会对数据本身改变的方法(push、shift、splice
等),/src/core/observer/array.js
整个文件就是对数据的特殊处理 最新的vue3
中,使用了ES6
的proxy
特性来替代这种覆盖set、get
实现响应式行为,这种模式同时也能够处理Array
。
vue
的源码当然没有如此简单,很多东西文章都没有涉及到,譬如说,通过上面的逻辑其实你可以发现,dep
和watcher
其实是互相引用的,而js
的垃圾回收是检测变量引用的机制,所以如果是简单的复制上文的逻辑,最终的这部分的内存其实是无法被回收的,需要你手动清除,当然vue
中也做了这样的处理(每个vm
下其实有一个watcherList
,用于记录这个示例中所有使用到的watcher
,再vm.destroy
时,通过遍历watcherList
,再销毁每一个watcher
,而watcher
中又会自己销毁Dep
),但是限于篇幅原因无法详细介绍了。
=============================================================
为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了**《95页前端学习笔记》**电子稿文件。
主要内容包括html,css,html5,css3,JavaScript,正则表达式,函数,BOM,DOM,jQuery,AJAX,vue 等等。
👉点击这里免费获取👈
html5/css3
-
HTML5 的优势
-
HTML5 废弃元素
-
HTML5 新增元素
-
HTML5 表单相关元素和属性
-
CSS3 新增选择器
-
CSS3 新增属性
-
新增变形动画属性
-
3D变形属性
-
CSS3 的过渡属性
-
CSS3 的动画属性
-
CSS3 新增多列属性
-
CSS3新增单位
-
弹性盒模型
JavaScript
-
JavaScript基础
-
JavaScript数据类型
-
算术运算
-
强制转换
-
赋值运算
-
关系运算
-
逻辑运算
-
三元运算
-
分支循环
-
switch
-
while
-
do-while
-
for
-
break和continue
-
数组
-
数组方法
-
二维数组
-
字符串
正则表达式
-
创建正则表达式
-
元字符
-
模式修饰符
-
正则方法
-
支持正则的 String方法
js对象
-
定义对象
-
对象的数据访问
-
JSON
-
内置对象
-
Math 方法
-
Date 方法
面向对象是一种编程思想
-
定义对象
-
原型和原型链
-
原型链
-
原型
常用的JavaScript设计模式
-
单体模式
-
工厂模式
最后
今天的文章可谓是积蓄了我这几年来的应聘和面试经历总结出来的经验,干货满满呀!如果你能够一直坚持看到这儿,那么首先我还是十分佩服你的毅力的。不过光是看完而不去付出行动,或者直接进入你的收藏夹里吃灰,那么我写这篇文章就没多大意义了。所以看完之后,还是多多行动起来吧!
可以非常负责地说,如果你能够坚持把我上面列举的内容都一个不拉地看完并且全部消化为自己的知识的话,那么你就至少已经达到了中级开发工程师以上的水平,进入大厂技术这块是基本没有什么问题的了。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
g-blog.csdnimg.cn/img_convert/8e1c7406a78bc6f3d1952444940bf6dc.png)
js对象
-
定义对象
-
对象的数据访问
-
JSON
-
内置对象
-
Math 方法
-
Date 方法
面向对象是一种编程思想
-
定义对象
-
原型和原型链
-
原型链
-
原型
常用的JavaScript设计模式
-
单体模式
-
工厂模式
最后
今天的文章可谓是积蓄了我这几年来的应聘和面试经历总结出来的经验,干货满满呀!如果你能够一直坚持看到这儿,那么首先我还是十分佩服你的毅力的。不过光是看完而不去付出行动,或者直接进入你的收藏夹里吃灰,那么我写这篇文章就没多大意义了。所以看完之后,还是多多行动起来吧!
可以非常负责地说,如果你能够坚持把我上面列举的内容都一个不拉地看完并且全部消化为自己的知识的话,那么你就至少已经达到了中级开发工程师以上的水平,进入大厂技术这块是基本没有什么问题的了。