vue源码学习之旅-day5

new Vue({}) 过程梳理

part 3

1
2
3
4
5
6
7
8
9
1. new Vue 说明Vue是一个构造函数。
2. 搜索export default Vue,找到core/instance/index.js
3. 发现执行了this._init方法
4. 依次执行了initMixin、stateMixin、renderMixin、eventsMixin、lifecycleMixin、warn函数为入参Vue构造函数添加了相关原型方法
5. 先看initMixin;添加了_init方法
6. 再看stateMixin; 添加了$data和$props两个只读属性到实例上;添加了$del,$set,$watch方法
7. renderMixin; 添加了$nextTick , _render方法
8. eventMixin;添加了$on $once $emit $off 方法
9. lifecycleMixin;添加了 _update,$foreceUpdate,$destory
1
2
3
4
5
6
7
8
9
const vm = new Vue({
data:function (){
return {
a:1
}
}
})

vm.a = 2

分析: new Vue(options) 时;执行下面动作

    1. 执行了Vue.prototyoe._init方法
    1. meargeOptions 处理传入的options后赋值给vm.$options
    1. vm.$options:
    • {
      // Vue.options静态属性合并过来的
      • components,
      • filters,
      • directives,
      • _base,
        // 传入的
      • data(){},
      • el,
        // 暂时不确定
      • render,
      • staticRenderFns,
        // 暂时不确定
      • _parentElm,
      • _refElm
        }
    1. 执行initProxy添加 vm._render = vm // 开发环境里可能是一个代理对象
    1. 添加vm._self = vm ;然后执行initLifecycle,添加$parent,$children(数组),$refs({})
    1. 执行initEvents; 添加_events,_parentListeners(创建子组件实例才有这个文件)
    1. 执行initRender方法;添加$slots,$vnode,$scopeSlots三个属性
    1. 使用callHook调用beforeCreate钩子
    1. 依次实例化inject,porps,methods,data,computed,watch,provide
    1. 使用callHook调用created钩子

数组对象相关方法拦截的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	1.Array.proptotype为原型创建一个对象
2. 缓存原有数组方法
3. 通过Object.defineProperty拦截数组相关方法,并且定义重名方法来改写他
4. def(arrayMethods, method, function mutator (...args) {
// 省略...
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args // push unshift 参数就是新增的参数
break
case 'splice':
inserted = args.slice(2) // splice第2个参数后面都是新增变量。所以直接截取
break
}
if (inserted) ob.observeArray(inserted)
// 省略...
})

microtask(微任务) 与 macrotask (宏任务)

javascript 主线程有一个 执行栈 和 任务队列

    1. 按照顺序依次执行js代码,当遇到函数时,会先将函数入栈,等函数执行完将该函数出栈。
    1. 这个过程遇到setTimeout、ajax等异步任务时,该函数会立即返回一个值以免阻塞线程。同时将该任务的回调依次推入 任务队列,后续让浏览器执行。
    1. 执行完 执行栈的任务后,会按照先入先出原则,依次执行 任务队列里的回调函数

microtask 和 macrotask 都属于异步任务的一种

    1. microtask种类: process.nextTick、Promise、MutationObserver
    1. macrotask种类:setTimeout、setInterval、setImmediate,I/O,UI rendering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');

// script start
// script end
// promise1
// promise2
// setTimeout
1
2
3
4
原因分析:
1. 异步任务分为 macrotask 和 microtask.
2. 每一次循环只取一个macrotask宏任务和当前栈中的所有微任务
3. 微任务先清空完才去执行宏任务

vue源码学习之旅-day4

  1. Watcher 类

目的: 订阅者的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
 /**
* vm: vdom
* render: 源代码叫expOrFn,其实是optiosn入参的render方法
* cb: 执行后的回调函数
*/
class Wathcer{
constructor(vm,render,cb){
this.vm = vm
/**
* 省略 expOrFn 判断expression(字符串表达式) 和 function 的相关逻辑。简化处理
*/
this.expOrFn = render
this.ob = ob

this.value = this.get()
}
get(){
// 简化处理,源码中使用了数组作为栈来存储
Dep.target = this // 打上标记,告知Observer类,是Wathcer触发的get!,让它收集我作为订阅者!
let value = this.vm._data[this.expOrFn] // 触发Observer类的Object.defineProperty.get 句柄
Dep.target = null // 清除标记
return value
}
updata(){
/**
* 省略:懒加载。同步。watcher队列处理等相关判断逻辑
*/
this.run()
}
run(){
/**
* 省略相关判断
* 本方法作用主要是获取旧值和新值,然后对比两者。不同则执行回调
*
*/
// 关键代码:
// 1. 源码debugger一步步看,可以发现此处get触发Observer类的Object.defineProperty.get 拦截句柄;
// 2. 继而触发dep.depend()
// 3. 接着触发Wathcer类的addDep方法(所以需要定义),把自身作为订阅者推入订阅者队列
let value = this.get()
// 新旧值比较
if(value !== this.value){
let oldV = value
// 赋值
this.value = value
// 执行回调
this.cb.call(this.vm,value,oldV)
}
}
addDep(dep){
// 省略相关代码

// 添加到订阅者队列,后续notify分发通知时,自然可以执行对应的render方法,从而更新数据
dep.addSub(this)
}
}

  1. Observer类,Dep类,Watcher类的关系梳理

初步通读这三个类的相关定义,暂时明确了它们的一些基本作用,记录下,方便后续查阅

  • Observer类,观察者。

      1. 主要通过递归遍历入参options,对每个key执行defineReactive添加__ob__属性
      1. 对每个key通过Object.defineProperty设置get/set方法
  • Dep 类, 发布订阅器。处理一对多,多对多的关系

      1. 内部实现addSub,removeSub 来添加/移除 订阅者
      1. 内部实现 notify 来分发通知
      1. 内部实现 depend 方法,用于收集依赖 Watcher
  • Watcher 类,订阅者

      1. 内部实现updata 方法,用于更新数据
      1. 内部实现addDep 方法,用于将自身作为 “订阅者” 推入 “订阅者队列”
      1. 内部实现 run 方法,主要是判断新旧值,并执行后续回调
  1. 小练习,实现vm.$watch
1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
data:{
a:1,
b:2
}
})
vm.$watch('a',()=>{
console.log("观测a成功")
})

setTimeOut(()=>{ vm.a = 233 },2000)

具体实现分 Dep.js, Watcher.js, Observer.js 及 index.html

  • (文件名不区分大小写,为了便于阅读,还是取首字母大写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Dep.js
let uid = 0;// 全局变量
class Dep {
static target = null;
constructor() {
this.id = uid++

this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
// 略
}
notify() {
const subs = this.subs.slice()
subs.forEach(watcher => {
watcher.updata()
})
}
depend() {
// 此处Dep.target是个Watcher实例
if(Dep.target){
// 传入dep
Dep.target.addDep(this)
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Observer.js

// 属性劫持
function defineReactive(
obj,// 劫持的对象
key,// 劫持的key
val,// key对应的value
customSetter,// 暂时不知道作用的入参
shallow // 暂时不知道作用的入参
){
let dep = new Dep()
// 简易处理val
arguments.length === 2? val = obj[key] : ''
// 递归处理子属性
let childOb = observe(val)
// get/set的实现
Object.defineProperty(obj,key,{
enumerable:true,// 可枚举?
configurable:true,// 描述属性可删改?
get(){
// 看是否Watcher实例的get调用
if(Dep.target){
// 递归收集依赖
dep.depend()
if(childOb){
childOb.dep.depend()
}
}
return val
},
set(newV){
val = newV
// 分发通知
dep.notify()
}
})
}
// 判断入参是否有__ob__属性,返回Observer实例
function observe(value,asRootData){
// 简易处理
let ob = value.__ob__ || new Observer(value)
if(asRootData && ob){
ob.vmCount ++ // 统计组件实例被使用次数
}
return ob
}
// 观察者类
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0 // 统计组件实例被使用次数

// 简易判断是否对象或数组
const type = Object.prototype.toString.call(value).slice(8, -1)
if (type === 'Object') {
this.walk(value)
}
else if (type === 'Array') {
// 数组处理,暂时省略
}
}
walk(value) {
const keys = Object.keys(value)
keys.forEach(key => {
defineReactive(value, key)
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Watcher.js

class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.expOrFn = expOrFn
this.cb = cb

// 关键代码
this.value = this.get()
}
get() {
// 打上标记
Dep.target =this
// vm 是标准的vue.options。其中data会被赋值给_data;
// 此处会触发Observer的get拦截句柄,并最终将自身作为订阅者加入队列
let value = this.vm._data[this.expOrFn]
// 移除标记
Dep.target = null
return value
}
updata() {
// 省略相关代码
this.run()
}
run() {
let value = this.get()
if (value !== this.value) {
const oldV = value
// 赋予新值
this.value = value
// 执行回调
this.cb.call(this.vm, value, oldV)
}
}
addDep(dep){
// 省略相关代码
dep.addSub(this)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<script src="./js/Dep.js"></script>
<script src="./js/Observer.js"></script>
<script src="./js/Watcher.js"></script>

<script>
class Vue {
constructor(options = {}) {
// 简易处理
this.$options = options
let data = this._data = this.$options.data
// 代理vm._data.a -> vm.a
Object.keys(data).forEach(key=>{
this.myProxy(key)
})
// 观测data
observe(data, this)
}
$watch(expOrFn, cb, options) {
new Watcher(this, expOrFn, cb)
}
myProxy(key){
var obj = this
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
return obj._data[key]
},
set(newV){
obj._data[key] = newV
}
})
}
}

// 示例
const vm = new Vue({
data:{
a:1,
b:2
}
})

vm.$watch('a',()=>{
console.log("正在观测a")
})

setTimeout(()=>{
vm.a = 233
},2000)
</script>
</body>

</html>

vue源码学习之旅-day3

part2 响应式原理

  1. Dep构造函数的实现

目的:

    1. 调用Dep.target(一个后续的Watcher)的addDep添加依赖方法。从而收集依赖
    1. 根据依赖精准的添加订阅者
    1. 分发通知给每个订阅者
  • Dep 发布-订阅器的作用:

      1. 提供add,remove等方法添加和取消订阅
      1. 提供notify方法,通知订阅者执行相关方法
      1. 提供depend 方法,收集依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let uid = 0;
class Dep {
static target = null;// 当前正在被观察的对象,空/Watcher
constructor() {
// 每个Dep实例都有一个唯一id
this.id = uid++
// 订阅者队列
this.subs = []
}
// 添加订阅者
addSub(sub) {// sub:Watcher
this.subs.push(sub)
}
// 移除订阅者
removeSub(sub) {
this.subs = this.subs.filter(item => {
return item.id !== sub.id
})
}
// 分发订阅
notify() {
const subs = this.subs.slice()
// 通知每个订阅者更新状态
subs.forEach(item => {
item.update()
})
}
// 收集依赖 ;不理解可以暂时跳过,后续看完Observer 和 Watcher再回来看
depend() {
// 这里Dep.target为当前正在被观察的对象
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
// 示例
const dep = new Dep()
// 添加订阅者
dep.addSub({
id: "001",
update() {
console.log("通知001,更新状态")
}
})
dep.addSub({
id: "002",
update() {
console.log("通知002,更新状态")
}
})
dep.addSub({
id: "003",
update() {
console.log("通知003,更新状态")
}
})
dep.notify() // 打印3个输出日志
dep.removeSub({
id: "002",
update() {
console.log("通知002,更新状态")
}
})
dep.notify() // 打印1,3两个输出日志
  1. Observer 构造函数的实现-简化版,暂时只考虑对象

目的:处理被观察者对象,通过往它身上附加get/set来收集依赖和分发状态

  • Observer 观察者类的作用:
      1. 实例化一个Dep,记录入参value,定义一个vmCount用于统计被使用次数
      1. 向入参添加一个__ob__属性,同时赋值dep,value,vmCount
      1. 遍历入参的键,调用defineReactive 设置反应性属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Observer {
constructor(value) {
this.value = value
this.vmCount = 0 // 统计当前实例被使用的次数
this.dep = new Dep() // 实例化一个发表订阅器实例
// 往被观察对象身上添加一个__ob__属性,同时赋值dep,vmCount,value自身给它
def(value, __ob__, this)
// 遍历value所有“对象类型”的key,调用defineReactive方法
this.walk(value)
}
walk(obj) {
let keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}

  1. defineReactive 定义反应性属性方法的实现
  • 难点之一,最好下载源码后,断点阅读,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<p>{{ arr }}</p>
</div>
<script src="./vue.js"></script>
<script>
new Vue({
el:"#app",
data:function (){
return {
arr:[1,2,3],
info:{
name:'tom',
type:{
sort:"俄罗斯蓝猫",
color:"blue"
}
}
}
},
})
</script>
  • defineReactive 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function defineReactive(
obj,// 当前操作的对象
key,// 对象的key
val,// 监听的数据
customSetter,// 暂时不知道作用的形参
shallow,// 暂时不知道作用的形参
){
var dep = new Dep()

/**
* 省略一系列边界判断,以及obj自身是否定义了getter/setter,有就执行它
* **/

// 对被观察对象执行定义的observer 方法。注意不是前面定义的Observer构造函数;
// 主要作用是返回 一个Observer构造函数中定义的__ob__属性
var childOb = !shallow && obsevrer(val)
// debugger // 在源码此处断点可以看到,val是被观察者,如vue.options 的data

// 在这里对obj 的属性进行依赖收集和分发通知
Object.defineProperty({
obj,
key,
get(){
/**
* getter之类的特殊处理,略
*/

// 查看当前是否有正在被观察的对象,有的话执行它的 addDep 方法
// 正在被观察的对象 -> 一个Watcher实例,后续定义
// 核心代码,通过这个Dep.target全局变量,实现往Dep实例的subs中添加订阅者。
// 即调用 new Dep().addSub()
if(Dep.target){
dep.depend() // 等价于 Dep.target.addDep(Dep实例)
// 递归这个对象,收集全部依赖
if(childOb){
childOb.dep.depend()
}
}
return value
},
set(newValue){
/**
* setter之类的特殊处理,略
*/

// 核心实现
value = newValue
dep.notify() // 通知订阅者更新状态
}
})
}


vue源码学习之旅-day2

  1. 让函数只执行一次的闭包函数 once
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function once(f){
var called = false
return function (){
if(!called){
called = true
f.apply(this,arguments)
}
}
}

// 使用示例
var handler = once(function (){
console.log("hello")
})

handler() // hello
handler() // 无效
handler() // 无效
  1. Object.defineProperty() 的封装函数 def。 源码中经常用到,有个印象即可
1
2
3
4
5
6
7
8
function def(obj,key,val,enum){
Object.defineProperty(obj,key,{
value:val,
enumerable:Boolean(enum),// 能否被for...in 和 Object.keys() 枚举?
writable:true,// 可改写不?
configurable:true // 属性能否被删除?除writeable外的配置属性能改不?
})
}
  1. 是否原生支持的方法
1
2
3
4
5
6
function isNative(f){
return typeof f === 'function' && (/native code/.test(f.toString()))
}
// 示例
Math.max // ƒ max() { [native code] }
isNative(Math.max) // true
  1. 使用位运算实现字符串的重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function repeat(str, n) {
var res = '';
while (n) {
if (n % 2 === 1) {
res += str;
}
if (n > 1) {
str += str;
}
// 相当于 Math.floor(n/2),这里用了移位运算。实用性不大,红宝书有相关说明
n >>= 1;
}
return res
}
// 示例
repeat("abc",3) // "abcabcabc"

vue源码学习之旅-day1

前言

已经用vue框架一段时间了,一直想去拜读下源码。但是每次看到那一万多行的代码都退缩了。最近开发需求正好比较少,争取每天抽出一部分时间,看看源码,学习一下相关技巧。

vue源码版本:v2.5.16

part1: vue工具函数的学习

  1. 获取复杂数据类型的方式
1
2
Object.prototype.toString.call(target).slice(8,-1)
// slice(8,-1) 亮眼,想想自己的项目里的各种切割处理方式,真是一堆辣鸡代码
  1. makeMap 工具函数
    目的: 定义一系列注册值缓存起来,方便后续使用,返回true/undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function makeMap(str,expectsLowerCase){
var map = Object.create(null), // 创建一个没有原型的空对象
list = str.split(","); // 约定,按照逗号间隔传入注册值。或者传入数组join转化一下
for(var i=0;i<list.length;i++){
// 注册值作为key添加到map对象中,且对应的value为true。
map[list[i]] = true
}
// 是否要转小写
return expectsLowerCase?function (val){return map[val.toLowerCase()]}
: function (val){ return map[val] }
}

// 使用示例
var isZS = makeMap("zs,16,boy,175");
console.log(isZS("17")) // undefined
console.log(isZS("boy")) // true
  • 知识点记录
1
2
3
4
5
6
7
8
1. Object.create(null):传入null时,返回一个没有原型(_proto_)的对象!
控制台执行下面代码,自行体会区别
var obj1 = Object.create(null),
obj2 = {};
console.dir(obj1)
console.dir(obj1)
2.策略者模式
3.闭包及函数柯里化
  1. JSON.stringify(target,null,2) 的三个参数
1
2
3
4
5
// 才知道JSON.stringify有三个参数,一直都只会用一个参数。。。惭愧
1.2个参数接受一个函数或者数组;
传入函数时,按照函数处理每一项,返回指定的值
传入数组时,只处理数组中定义的key
2. 第三个参数,指定缩进
  1. hasOwnProperty 方法的封装
  • 目的: 判断对象是否包含指定的“自身属性”,非继承
1
2
3
4
5
function hasOwn(obj,key){
return ({}).hasOwnProperty.call(obj,key)
}

hasOwn({"zs":14},"zs") // true
  1. cached 方法
  • 目的: 创建纯函数的缓存对象。我们知道纯函数的返回结果只和输入有关,因此如果用对象的key来描述输入,value来描述输出。那么这个对象就是一个完美的缓存,因为对象的key是唯一的。
  • 第一次调用时,缓存结果,第二次及后续调用,则读取缓存,避免重复计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function cached(fn){
var cache = Object.create(null); // 既然要缓存结果,那肯定要创建对象来存储
return (function (val){// 柯里化返回函数,每次传入不同的纯函数时,都能有一个相对应的缓存对象
// 看对象里是否有存储过对应的值,第一次则创建缓存,后续直接读取缓存
var hit = cache[val]
console.log("查看当前缓存:",cache)
return hit || (cache[val] = fn(val))
})
}

// 使用示例
function sayHello(a){
return 'hello '+a
}

function sayHi(b){
return 'hi '+b
}

var hello = cached(sayHello),
hi = cached(sayHi);

hello("zhangsan")
hello("lisi")
hello("zhangsan")
hello("lisi")

hi("tom")
hi("jerry")
hi("tom")
hi("jerry")