前置知识 JS中动态加载JS脚本 1 2 3 let script = document .createElement ('script' )script.src = 'xxx.js' document .head .appendChild (script)
Rest Parameters define a function like function sum(...nums){} invoke this function: sum(1, 2, 3), nums will be a list: [1, 2, 3]
spread operator1 2 3 var nums = [1, 2, 3] var newn = [...nums] //nums === newn
when invoking function, using sum(…nums) equals sum(1, 2, 3)
Default parameter value like python.
NT_1_V8 JS Engine And JS Scope
流程
Stream: 编码转换
Scanner: 词法分析
Preparse: 将不必要的函数(如Outer func内没用到的inner func)预解析。LazyParsing。
Parser模块转换为AST(抽象语法树)https://astexplorer.net/
Ignition解释器将AST解释成字节码(如果函数只执行一次的话),并收集关键信息用于TurbleFan优化器
TurbleFan编译器将字节码编译成优化后的 机器码(,并缓存下来 ?),用于多次执行相同函数。这样可以节省Ignite过程耗费的资源。当函数参数类型变化了,就会Deoptimation,逆向转换为字节码。
GO
GO,AO都是VO
代码解析(Parse)阶段,Parser会产生GlobalObject(GO)对象,存放类、函数、window(指向自己)、属性(没使用的话值将为undefined)
ECStack、GEC、FEC ECStack是上下文栈!GEC,FEC上下文按照调用顺序都存在这里。
执行上下文
全局执行上下文
函数执行上下文
eval函数执行上下文
编译阶段 遇到变量: 加入GO并赋值为undefined;
遇到函数:开辟空间,空间有parent scope、函数执行体,然后将空间地址加入GO。
执行阶段 从全局域开始自上而下执行。
创建全局执行上下文GEC,遇到调用函数就创建函数执行上下文FEC 。
执行上下文会分别创建VO,建立scope chain,以及确定this指向。
全局执行上下文GEC
执行代码:赋值到VO(GO)、执行代码
函数执行上下文FEC 2. 执行代码:赋值到VO(AO),沿着作用域链找、执行代码 3. 执行完毕:FEC从ECS中销毁
变量对象的创建 创建VO的过程发生在进入执行上下文的过程,也就是编译阶段。
建立arguments对象
检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
VO与AO 变量对象与活动对象其实都是同一个对象,只是处于执行上下文的不同生命周期。
不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。(这句话对吗??对的,只有在执行该函数的时候才会创建AO对象)
注意,函数A内的函数B在全局初次解析的时候只会做预解析,只有当函数A被执行时才会正式解析函数B(在堆中开辟空间)。
ES6新特性 ES6下,使用Variable Environment概念而不单单是VO。 也说明可以向上兼容。
作用域链 FEC中有VO(AO)之外,还有scope chain。 scope chain = 当前上下文的VO + parent scope(这个在编译阶段就被确定了。)
作用域链本质上是一个指向当前环境与上层环境的一系列变量对象的指针列表(它只引用但不实际包含变量对象),作用域链保证了当前执行环境对符合访问权限的变量和函数的有序访问。
按照scope chain来进行逐层查找
在非全局域内定义属性而不使用标识符(let\var)V8会默认将其直接加入/更新到GO中
在非全局域内定义属性使用var a=b=10,相当于var a=10;b=10
this this在代码执行的时候才被绑定。
在node中,全局下的this是{}
在浏览器中,全局下的this是GO
绑定规则
默认绑定:直接调用函数 ,函数内的this绑定为GO。**只要是xxx()**,不管在哪里定义,不管函数在哪,不管这个函数是不是被另一个函数作为返回值返回,其内部this都绑定为GO
隐式绑定:通过一个对象去调用这个函数。如obj.xxx(),那么this绑定的就是obj
显式绑定:func.apply()/func.call(),func中的this会绑定到apply的参数。apply和call的区别:当func有参数需要传参时,func(this要绑定的东西, func参数1, func参数2, …),func(this要绑定的东西, [func参数1, func参数2, …]). bind()也可以,func.bind()返回一个this为bind参数的新函数
new绑定: new func(“xxx”, “xxx”),func中的this绑定为调用这个构造器时创建出来的对象.
优先级: new > 显式 > 隐式 > 默认
bind > call
new和call\apply不允许同时使用
apply、call、bind的第一个参数为undefined或者null时,调用的函数的this为GO
箭头函数的this不进行绑定。直接找上层作用域的this。注意,只有函数给或者全局才能产生作用域,对象并不能
setTimeout等GO内的直接函数传入的非箭头函数的this为GO
call()原理 1 2 3 4 5 6 7 8 9 10 Function.prototype.hycall = function(thisArg, ...args) { var fn = this thisArg = (thisArg == undefined || thisArg == null) ? window : Object(thisArg) thisArg.fn = fn var res = thisArg.fn() delete thisArg.fn return res }
apply()原理 类似
bind()原理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Function.prototype.hybind = function(thisArg, ...argArray) { var fn = this thisArg = (thisArg == undefined || thisArg == null) ? window : Object(thisArg) var proxyFunc = function(...args) { thisArg.fn = fn var finalArgs = [...argArray, ...args] var res = thisArg.fn(...finalArgs) delete thisArg.fn return res } return proxyFunc }
箭头函数 简写: var newns = nums.filter(item => item%2 === 0)
filter: 符合条件的(true)过滤出来,不符合的过滤掉
NT_2_JS Memory Operation Basic Info 定义变量时分配内存。
基本数据类型:栈空间分配
复杂数据类型(对象、列表等):堆内存开辟,并返回这块空间的指针地址。
Normal GC Algorithm 引用计数 对象隐式Property:retain_count。
当有其他对象引用之时,retain_count++。当retain_count == 0时,回收。
弊端 :循环引用导致的Memory leak。
标记清除 设置根对象(How to set? GO?),GC会定期从根开始找对象。对于没有引用到的,会认为是不可用的对象。 很好地解决循环引用的问题。
被较广泛使用。
NT_3 JS Closure And Parameter 方法:一个对象中的函数称作这个对象的方法。(当然也可以称为函数)
数组的函数 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 var newNums = nums.filter (function (item, index, arr ){ return true ; }) var newNums1 = nums.map (function (item ){ return item; }) nums.forEach (function (item ){ console .log (item) }) var found = nums.find (function (index ){ return true ; }) var foundIndex = nums.findIndex (function (item ){ return true ; }) nums.reduce (function (prevalue, item ){ return prevalue+item }, 0 )
闭包 Define
闭包的定义,分成两个∶在计算机科学中和在JavaScript中。在计算机科学中对闭包的定义(维基百科):
闭包(英语:Closure) ,又称词法闭包(Lexical Closure) 函数闭包(function closures)
是在支持头等函数的编程语言中,实现词法绑定的一种技术;
闭包在实现上是一个结构体,它存储 了一个函数和一个关联的环境(相当于一个符号查找表);
闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定 ,这样即使脱离了捕捉时的上下文,它也能照常运行;
捆绑的东西只是持有其引用,而不是深复制一份
闭包 = 函数+一些自由变量。
在执行完foo()后,foo对应的上下文会被销毁(但是AO不会!!!),但是最后bar()还是输出了name=”foo”。
提醒:作用域链(parentScope)在词法解析的时候就已经被确定。也就是说,一个函数在此法解析的时候就已经知道了他的自由变量。
因此,闭包在函数创建时就会被确定然后被创建出来(*MDN)
Memory Leak of Closure 如上图,当执行完foo()后,foo()对应的FEC会被销毁,其对foo()的AO的引用也就没了,但是此时是AO没有被销毁的,因为GO中有fn,而此时fn得到了foo()的返回值bar(),而bar()中对应内存中的parentScope是有对foo的AO的引用的,根据JS的垃圾回收机制,foo()的AO是不会被销毁的。
所以,如果bar()此时候的函数对象不被销毁,foo()的AO是不会被销毁的。
这就造成了内存泄漏——该被GC的东西不能被GC。
怎么解决呢?
简单,fn = null(0x0)
V8变量销毁 闭包没有使用到的变量,V8会将其销毁。
参数 本质上是类数组对象。
argument是函数内的隐藏参数
获取参数长度arguments.length
获取参数arguments[0]
获取当前函数argument.callee
转array: Array.prototyope.slice.call(argument) 或者 [].slice.call(argument) 或者 Array.from(argument)
当函数的实参个数小于调用函数的形参个数的时候,可以用argument获得全部的参数
箭头函数中没有arguments
JavaScript—Pure Func And Currying And Compose Func Pure Function
此函数在相同的输入值时,需产生相同的输出。
函数的输出和输入值以外的其他隐藏信息或状态无关,也和I/O设备产生的外部输出无关。
该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等.
Currying
好处:
AutoCurrying
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function lwlcurring(fn) { return function curried(...arg) { if (arg.length >= fn.length ){ return fn.apply(this, arg) }else{ return function curried2(...arg2){ return curried.apply(this, [...arg, ...arg2]) } } } } function add1(num1, num2, num3, num4) { console.log(arguments) res = num1+num2+num3+num4 console.log(res) return res } var curryAdd = lwlcurring(add1) console.log(curryAdd(10)(20, 30, 40))
Compose Func So elegant!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //只对单返回值单参数函数有效 function lwlCompose(...fns){ var length = fns.length for (var i=0; i<length; i++) { if (typeof fns[i] != function){ throw new TypeError("Expected arguments are functions") } } function compose(...args) { var index = 0; var res = length ? fns[index].apply(this, args) : args while(++index < length){ res = fns[index].call(this, res) } return res } return compose }
JavaScript NT_5—with_eval_strict AND OOP Eval函数 var str = ‘console.log(‘OK’)’ eval(str)
不建议使用。
可读性
恶意篡改
必须经过JS解释器,且不被V8优化
Strict mode严格模式
严格模式下,默认绑定(自执行函数)this会指向undefined,可以使用window
OOP面向对象 对对象和其属性的控制 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 //加_表示是私有的属性/方法,实际上不是。是一个约定。 obj = {name: "Soulter", age: 19, _school: "USTB"} //defineProperty(目标对象, 目标属性, 数据属性描述符) //数据属性描述符 Object.defineProperty(obj, "love", { //默认undefined value: "anime", //不可删除、不可重新定义属性描述符 默认false configurable: false, //不可枚举 默认false enumerable: false, //不可写(非严格模式下写入无效,严格模式下写入报错) 默认false writable: false, }) //存取属性描述符 Object.defineProperty(obj, "school",{ configurable: true, enumerable: true, get: function() { //You can write some codes at there. return this._school }, set: function(val) { //You can write some codes at there, too. this._school = val } })
当然也可以同时定义多个。
1 2 3 4 5 6 7 8 Object.defineProperties(obj, { height: { //... }, weight: { //... } })
更简便的方法设置getter/setter
1 2 3 4 5 6 7 8 9 obj = { name: "Soulter", age: 19, _school: "USTB", set school(val){ //... //默认将writable、configurable、enumerable设为true } }
获取属性描述符
1 2 Object.getOwnPropertyDescriptors(obj, 'address') Object.getOwnPropertiesDescriptors(obj)
禁止 对象添加 新属性
1 Object.preventExtensions(obj)
禁止 对象配置/删除 属性configurable
禁止 对象修改 属性writable
创建对象 Factory Mode 工厂模式,生产对象。 用函数。
缺点:不能抽象出具体的类型,只知道他是Object
new构造函数 var p1 = new Person()
函数Person一般称为构造函数
发生了什么?
p1的类型:Person
p1.__proto__.constructor.name Person
【IMP】注意第二点,所有该构造函数构造出来的对象的隐式原型都指向为该构造函数的显式原型
Prototype原型 构造函数每次都会创建一个单独的对象出来,占用空间大。
原型是一个对象
获取 obj.__proto__ 隐式原型 , 浏览器提供
Object.getPrototypeOf(obj) 隐式原型 , ES5
func.__proto__ 隐式原型 , 浏览器提供
func.prototype 显式原型 , ECMA
函数的显式原型默认自带一个constructor属性,指向该函数。constructor属性是对象才拥有的,函数其实也是一个对象,但是函数本身还是一个函数, constructor的重点是Function(),
用途 获取对象属性时,如果目标对象没有该属性,会去prototype内找
因此,可以将类中需要共有的东西(如某个函数)都用函数显式原型去定义。
Prototype Chain原型链 获取对象属性时,如果目标对象没有该属性,会去prototype内找,prototype也是一个对象,也有prototype。会逐层查找。
原型链的顶层:Object的原型
例一
1 2 3 4 5 obj = { "name": "OK" } //obj.__proto__就是顶层原型 //obj.__proto__.__proto__为null
例二
对象继承 使用Student和Person构造函数来记录。
原型链实现继承 Student.prototype = new Person()
弊端 可能会造成相互影响。
1 2 stu1.friends = [] //这是对friends的set操作 stu1.friends.push("xx") //这是对friends的get操作,如果Student原型内没有则会去Person内去找
借用构造函数实现继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Person(name, age, friends){ this.name = name this.age = age this.friends = friends } function Student(name, age, friends, no){ Person.call(this, name, age, friends) console.log(this) } var p = new Person() Student.prototype = p //Stealing
弊端 父类会被调用两次,生成了一个无意义的p对象
原型式继承 创建一个新的对象,使得新对象的原型指向指定的对象(这样就可以实现子类继承父类)。
本质上和上面的方法相同,但是上面的方法生成的对象有杂质
1 2 3 4 5 6 7 8 9 10 obj= {name: "ok"} function createObj(o){ var newObj = {} Object.setPrototypeOf(newObj, o) return newObj } var info = createObj(obj) //或者 var info = Object.create(obj)
寄生式继承 也有弊端。不建议
⭐Better method of Inheritance——寄生组合式继承 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 function Person(name, age, friends){ this.name = name this.age = age this.friends = friends } function createObj(o){ function Fn() {} Fn.prototype = o return new Fn() } function inheritPrototype(sub, Super){ sub.prototype = createObj(Super.prototype) //让子类的prototype的construtor指向自己的构造函数 Object.defineProperty(sub.prototype, "constructor", { enumerable: false, configurable: true, writable: true, value: sub }) } function Student(name, age, friends, no){ Person.call(this, name, age, friends) console.log(this) } // Student.prototype = Object.create(Person.prototype) inheritPrototype(Student, Person)
这样就实现了继承。由Student构造函数创建的对象在直接对其增删改的时候不会影响到其父类,Student的原型也是没有杂质的(只有指向父类原型的原型)
其他补充1 obj.hasOwnProperty 只检测本对象的属性,不包括原型内
xxx in obj 检测对象的属性,包括原型内
obj1 instanceOf obj2 检测obj2(构造函数的prototype)是否在obj1(实例对象)的原型链上
类
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 class Person{ //类的构造方法: //创建对象、对象原型赋值为类显式原型 //函数this赋值为该对象 //执行代码 //返回这个对象 constructor(name, age){ this._name = name this.age = age /... } get name() { //xxxx } //静态方法 static createPerson(){ } } var p = new Person() //类也有原型,本质上是上面方法的语法糖 console.log(Person.prototype) //{} console.log(Person.__proto__) // 顶层Object console.log(p.constructor) //[class person] console.log(typeof Person) //function class Student extends Person{ constructor(name, age, school){ //子类在使用this或者返回this之前,必须调用父类的构造函数。 //调用之后父类构造函数中的this就是子类中的this。 super(name, age) this.school = "OK" /... } }
super.xxx()可以执行父类中的函数。
JavaScript—ES6-ES13 多态 当对不同的数据类型执行同一个操作时,变现出来的行为不一样,就是多态的表现
增强对象字面量(Enhanced object literals) 1 2 3 4 5 6 7 8 9 var name = "" var obj = { //Property shorthand name, //直接写变量,相当于name: name bar() {}, //可直接写函数(是function的语法糖,而不是箭头函数的语法糖) //计算属性名 [name + '123']: 'hahahahahaha' }
解构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //数组 names = [1, 2, 3] var [i1, i2, i3] = names var [, i5, i6] = names //拿到后面两个 var [i7, ...i8] = names //后两个形成一个新数组 var [i1, i2, i3, i4=4] = names //默认值 //对象 obj = { name: 'haha', age: 19, } var { name, age } = obj var { name: newName, age: newAge = -1} //新命名, 默认值 //对对象使用解构 function bar({ name, age }){ return name + age }
let/const
var有作用域提升,let和const无, 但不代表在代码执行前就不创建let和const标识的变量,只是不可以访问他们,直到被赋值 c ——作用域提升:能被提前访问
let、const不允许重复声明
const变量之后,该变量不能被赋值到其他内存地址上,可以修改内存地址上的一些数据。
为什么var用的越来越少? var会直接添加到window中,window这个对象在新ECMA中不做实现,V8也不做实现,由浏览器为了保持向下兼容而多出来的东西。
新ECMA中,GO变成了由C++的HashMap(ZoneHashMap)实现的VariableMap类型的variables_,所有(let var const)声明的变量都会放到这里面。
在用var时,会同时添加到window和variables_中(二者保持双向同步)。可能会产生一些bug。
暂时性死区 只要在一个代码块中用let或const声明过一个变量,那么在他被赋值之前都不能够被访问(无论全局作用域用没用var定义过相同名字的变量)
for…of 1 2 3 4 5 o = [1, 2, 3] for( let i of o ){ //xx } //可以用const
模板字面量 1 2 3 4 5 6 let name = "ok" let res = `hahaha ${name + "123"}` function calc(a, b, c){ } calc`Hello${name}World${name}` //调用函数,a=['Hello', 'World'], b=ok, c=ok
展开 1 2 3 4 let num = [1, 2] let name = "Soulter" let res = [...num, ...name] // 字符串也会分割
ES9 中,对对象也可以展开。在对象中,如果对数组进行展开,则会返回’0’:xxx, ‘1’:xx,如果对对象,那就直接展开
对对象用展开是浅拷贝!只会完全复制一份原对象给自己,如果修改原对象中所新建的对象,那么新对象也会改变!
Symbol 在ES6,可以使用字符串和Symbol作为对象key。(不能用对象!) Symbol创建出来的是独一无二的值(当然也可以自己定义值),因此可以解决对象内容覆盖的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let s1 = Symbol(), s2 = Symbol() let obj = { [s1]: 111 } obj[s2] = 222 获取时,Symbol定义的内容不能通过小数点来获取 obj.s1 //错误 obj[s1] //正确 ~~Object~~.keys(obj) //获取不到Symbol的,只能获取到字符串的 Object.getOwnPropertyNames(obj) //只能获取到字符串的 Object.getOwnPropertySymbols(obj) //可以 let sa = Symbol.for("aa") let sb = Symbol.for("aa") //找到了前面创建过值为aa的Symbol,因此返回sa。
Set
let set = new Set()
对对象强引用
delete、add、clear
WeakSet
强引用和弱引用
GC在回收的时候,会区分强弱引用。GC将弱引用当成没有引用。
1 2 info = new WeakRef(obj) info.deref()//得到的是obj
Map
相比对象,可以用更多数据类型来作为key
let map = new Map(), 可接收Entries作为参数
get、delete、set、clear
遍历:map.forEach((item, key) => {}) 或 for..of
WeakMap
Vue3响应式原理 每一个都会生成一个render函数
1 2 3 4 5 6 7 8 9 10 let obj = {name: "OK", age: 19} let map = new Map() let weakMap = new WeakMap() obj1map.set("name", [objNameFn1, objNameFn2]) obj1map.set("age", [objAgeFn1, objAgeFn2]) weakMap.set(obj, obj1map)
监听name的变化,当name变化后,通过weakmap得到obj1map得到其要调用的函数。
当用户销毁obj时,由于weakMap对obj相当于没有引用,所以obj内存会被垃圾回收。
Array Includes
和 arr.indexOf != -1 差不多,但是includes可以判断NaN。
平方运算
3 ** 3
Entries
将对象转换为可迭代的类型,Entries类型可以用forEach。
padding
1 2 3 let cardnum = "1234567890" let lastfournum = cardnum.slice(-4) let finalnum = lastfournum.padStart(cardnum.length, "*")
flat
对数组降维。默认depth=1。
fromEnrtries ES10
将Entries转为Obj
OptionalChaining可选链 ES11
1 2 3 4 info = {name: "lwl"} let res = info.friend?.girlfriend.name //如果friend为undefined或者null,那么后面的都不执行
Global ES11
node和浏览器获取全局方式不同。
用globalThis
FinalizationRegistry 检测对象是否被GC回收。
1 2 3 4 5 let fr = new FinalizationRegistry((val)=>{ console.log(val, "对象被销毁") }) let obj = {} fr.register(obj, "obj1")
Proxy mipad5
Reflect mipad5
Reflect.get(target, key, receiver), 改变target(也就是obj)中getter/setter的this指向。指向receiver(也就是proxObj)
在Proxy内使用Reflect有什么用? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let user = { _name: "张三", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { // receiver = admin return Reflect.get(target, prop, receiver); } }); let admin = { __proto__: userProxy, _name: "李四" }; console.log(admin.name); // => 李四
Reflect.construct
Promise 原理和使用 传统success、failcallback回调缺点:
不统一
要自己写
promise.then()可以接收两个参数,第一个是resolve的第二个是reject的。
调用了resolve后,还可以继续执行后面的代码,reject后不能。
resolve 会调用then中的第一个参数hook
如果resolve中的参数是一个新的promise,则状态会转移到新的promise上,也就是说最终结果会看新的promise
如果resolve中的参数是一个对象,对象中有then方法,那么会调用该对象的then方法(实现了thenable接口)并传resolve和reject进去,最终结果会看这个then里面的情况。
then 传入的回调函数的返回值会被一个新的Promise的Resolve包装然后作为then的返回值。
reject throw new Error和reject都会调用then的第二个参数hook
catch 传入的回调函数的返回值会被一个新的Promise的Resolve 包装然后作为then的返回值。
finally 无参数
特例! 1 2 3 4 5 promise.then(res => { return 111 }).catch(err => { console.log(err) })
这个catch捕获的是promise的异常!不是then回调函数中返回值产生的Promise
1 2 3 4 5 6 7 promise.then(res => { return new Promise((resolve, reject) => { reject() }) }).catch(err => { console.log(err) })
这个catch捕获的是then回调函数中返回值产生的Promise的异常!
all(类方法) Promise.all([p1, p2, p3, "aaa"]).then(...)
当所有promise都fulfilled之后,执行then。 当有一个rejected,那么就直接执行catch
allsettled(类方法) Promise.allsettled([p1, p2, p3]).then(...)
当所有promise都有结果后,执行then,将结果以对象数组的形式返回。
race(类方法) Promise.race([p1, p2, p3]).then(...)
竞赛,返回最先一个promise的结果(fulfilled或rejected)
any(类方法) 至少等到有一个fulfilled之后才执行then。全部rejected之后就会执行catch
Vue响应式原理 简单来说,定义了一个Depend类来收集依赖(一些函数,函数当中用到了要监听并响应的一些数据),Depend类有一个数组(承载依赖函数),和add函数和notify函数。
Vue3使用Proxy来实现响应式。Vue2使用Object.defineProperty.
下面展示Vue3的响应式 Step1:
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 class Depend{ constructor(){ this.reactFns = [] } add(fn) { this.reactFns.push(fn) } notify() { this.reactFns.forEach(fn => { fn() }) } } const depend = new Depend() let obj = { name: "lwl" } function lwlWatcher(fn){ depend.add(fn) } function lwlWatcherExecutor(){ depend.notify() } let objProxy = new Proxy(obj, { set: function(target, key, receiver){ Reflect.set(target, key, receiver) lwlWatcherExecutor() } }) function outputCurrentName(){ const currentName = obj.name console.log("currentName:", currentName) } //Simulate lwlWatcher(outputCurrentName) console.log("改名前:", obj) objProxy.name = "Soulter" console.log("改名后:", obj)
然而,实际开发中会有多个对象,每个对象有多个属性。这样就是要生成多个depend、watcher,如何对这些depend进行管理呢?
用map存。每个map是专属于一个对象的,map内key为obj的key名,value为depend。
然后用weakmap存各个map,weakmap的key为各个对象,value为map
修改后:
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 70 71 72 73 74 75 76 77 78 79 80 81 82 class Depend{ constructor(){ //用set防止多次调用重复的函数 this.reactFns = new Set() } notify() { this.reactFns.forEach(fn => { console.log(fn) fn() }) } depend() { if(activateReactiveFns){ console.log("收集依赖") this.reactFns.add(activateReactiveFns) } } } let targetMap = new WeakMap() let activateReactiveFns = null function getDepend(target, key){ objMap = targetMap.get(target) if(!objMap){ objMap = new Map() targetMap.set(target, objMap) } depend = objMap.get(key) if(!depend){ depend = new Depend() objMap.set(key, depend) } return depend } function lwlWatcher(fn){ //将函数设置到全局,以便收集依赖 activateReactiveFns = fn fn() activateReactiveFns = null } function reactive(obj) { return new Proxy(obj, { set: function(target, key, receiver){ console.log("setter触发") Reflect.set(target, key, receiver) let depend = getDepend(target, key) depend.notify() }, get: function(target, key, receiver) { //收集依赖 console.log("getter触发") let depend = getDepend(target, key) depend.depend() return Reflect.get(target, key, receiver) } }) } let objProxy = reactive({ name: "lwl" }) function renderTheObjName(){ const currentName = objProxy.name console.log("render:", currentName) } //Simulate lwlWatcher(renderTheObjName) console.log("改名前:", objProxy) objProxy.name = "Soulter" console.log("改名后:", objProxy)
Vue2响应式:
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 class Depend{ constructor(){ //用set防止多次调用重复的函数 this.reactFns = new Set() } notify() { this.reactFns.forEach(fn => { console.log(fn) fn() }) } depend() { if(activateReactiveFns){ console.log("收集依赖") this.reactFns.add(activateReactiveFns) } } } let targetMap = new WeakMap() let activateReactiveFns = null function getDepend(target, key){ objMap = targetMap.get(target) if(!objMap){ objMap = new Map() targetMap.set(target, objMap) } depend = objMap.get(key) if(!depend){ depend = new Depend() objMap.set(key, depend) } return depend } function lwlWatcher(fn){ //将函数设置到全局,以便收集依赖 activateReactiveFns = fn fn() activateReactiveFns = null } function reactive(obj) { // return new Proxy(obj, { // set: function(target, key, receiver){ // console.log("setter触发") // Reflect.set(target, key, receiver) // let depend = getDepend(target, key) // depend.notify() // }, // get: function(target, key, receiver) { // //收集依赖 // console.log("getter触发") // let depend = getDepend(target, key) // depend.depend() // return Reflect.get(target, key, receiver) // } // }) Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { depend = getDepend(obj, key) depend.depend() return value }, set: function(newVal) { value = newVal //?????????????????????????why depend = getDepend(obj, key) depend.notify() } }) }) return obj } let objProxy = reactive({ name: "lwl" }) function renderTheObjName(){ const currentName = objProxy.name console.log("render:", currentName) } //Simulate lwlWatcher(renderTheObjName) objProxy.name = "Soulter"