- 记录一下每日学习的笔记,投资自己
- Daily learning
- 记录node的学习历程
- 一个函数返回一个函数
- 一个函数的参数是高阶函数
- 高阶函数可以对一个函数进行扩展,使之在执行阶段能有其它业务处理
- 形式1
function fn(){ return function(){} }
- 形式2
function(cb){ cb() }
- 例如扩展一个函数,在其执行之前,进行其它操作
function core (a,b,c){ console.log(a,b,c) } Function.prototype.before = function(cb){ return (...args)=>{ cb() this(...args) } } let newCore = core.before(()=>{console.log('before')}) newCore(1,2,3)
- 总结:通过传入函数来扩展新的操作,通过返回一个新的函数来将新操作和原函数结合
- typeof
- typeof 只能判断基本数据类型,其中typeof null = 'object'
- typeof undefine = 'undefine'
- typeof Object typeof Function typeof Array 皆为"function"
- typeof function fn(){} 为"function"
- typeof [] typeof {} 为"object"
- instanceof
- 检测构造函数的prototype属性是否出现在实例对象的原型链上
function person(){ } let p = new person() console.log(p instanceof person)
- constructor 拷贝 除了null和undefined以外,其它类型都是包装类对象
let num = 1 console.log(num.constructor) let arr = [] console.log(arr.constructor) let a = ()=>{} console.log(a.constructor) <!-- [Function: Number] --> <!-- [Function: Array] --> <!-- [Function: Function] -->
- Object.prototype.toString.call()
console.log(Object.prototype.toString.call(1)) //[object Number] console.log(Object.prototype.toString.call('1')) //[object String] console.log(Object.prototype.toString.call([])) //[object Array] console.log(Object.prototype.toString.call(function(){})) //[object Function] console.log(Object.prototype.toString.call(null)) //[object Null] console.log(Object.prototype.toString.call(undefined)) //[object Undefined]
- 方法1
- 通过传入类型来实现函数返回
function judgeType (type,val){ return Object.prototype.toString.call(val) == `[object ${type}]` } console.log(judgeType('Array',[1,2,3]))
- 这种方法不太理想,每次都要传入一个类型才行,不能简便的判断其类型
- 方法2
- 利用闭包的缓存思想,将每一种类型判断封装成一个函数返回,以后只需要单独调用即可判断改类型
- 闭包:定义函数的作用域和执行函数的作用域不一致,就会产生闭包
function judgeType(type){ return (val)=>{ return Object.prototype.toString.call(val) == `[object ${type}]` } } let util = {} let type = ['Array','Number','Function','Object','String','Null','Undefined'] type.forEach(tp =>{ util[`is${tp}`] = judgeType(tp) }) console.log(util.isNumber(1)) //true console.log(util.isString(1)) //false
- 将一个函数利用高阶函数思想改便成多个函数嵌套形式,其传参由多个变成分批传入一个(让每个函数负责的功能更具体,缩小函数的范围),上述方法就是柯里化的思想
- 如何实现一个通用柯里化函数
function Currying(fn){
//获取函数长度
let len = fn.length
//获取剩余参数
let allArg = Array.from(arguments).splice(1)
return (...newArg)=>{
allArg = [...allArg,...newArg]
if(allArg.length >= len){
return fn(...allArg)
}else {
return Currying(fn,...allArg)
}
}
}
function sum(a,b,c,d){
return a+b+c+d
}
let newSum = Currying(sum,1,2)
console.log(newSum(3)(4))
- 如何扩大函数的范围
- 通常原型链上的函数只能提供给拥有该原型的实例对象,通过call等方法可以将其扩展给其它类型使用
function unCurrying(fn){
return (...args)=>{
return (Function.prototype.call).call(fn,...args)
}
}
let toString = unCurrying(Object.prototype.toString)
console.log(toString(123))
console.log(toString('123'))
- 用node模拟一个文件的读写
const fs = require("fs")
const path = require("path")
fs.readFile(path.resolve(__dirname,'./file/1.txt'),'utf-8',function(err,data){
console.log(data)
})
fs.readFile(path.resolve(__dirname,'./file/2.txt'),'utf-8',function(err,data){
console.log(data)
})
- 针对这种异步的并行请求,我们如何同步最终的结果呢,最直接的方法就是利用定时器+回调
const fs = require("fs")
const path = require("path")
fs.readFile(path.resolve(__dirname,'./file/1.txt'),'utf-8',function(err,data){
finish('1',data)
})
fs.readFile(path.resolve(__dirname,'./file/2.txt'),'utf-8',function(err,data){
finish('2',2)
})
function after(timer,cb){
let util={}
return function(key,value){
util[key] = value
if(--timer === 0){
cb(util)
}
}
}
const finish = after(2,function(obj){
console.log(obj)
})
- 通过发布订阅模式也可以实现该效果
const fs = require('fs')
const path = require('path')
// const { emit } = require('process')
const events = {
_arr:[],
on(callback){
this._arr.push(callback)
},
emit(key,value){
this._arr.forEach(fn=>{
fn(key,value)
})
}
}
let obj = {}
//订阅
events.on((key,value)=>{
obj[key] = value
if(Object.keys(obj).length == 2){
console.log(obj)
}
})
fs.readFile(path.resolve(__dirname,'./file/1.txt'),'utf-8',function(err,data){
//发布
events.emit(1,data)
})
fs.readFile(path.resolve(__dirname,'./file/2.txt'),'utf-8',function(err,data){
events.emit(2,data)
})
- 还有一种很常见的设计模式叫观察者模式
class Subject{
constructor(name){
this.name = name,
this.state = '开心',
this.observer = []
}
attach(ob){
this.observer.push(ob)
}
setState(newState){
if(this.state != newState){
this.state = newState
this.observer.forEach(ob=>{
ob.update(this.state)
})
}
}
}
class Observer{
constructor(name){
this.name = name
}
update(state){
console.log(state)
}
}
let baby = new Subject('baby')
let mon = new Observer('mon')
let fath = new Observer('fath')
baby.attach(mon)
baby.attach(fath)
baby.setState('哭')
js通常采用四种异步解决方案:回调、promise、Generator、async/await
- 回调解决方案
通常异步的操作需要放入回调中进行执行,若要执行串行异步,则容易造成回调嵌套
- promise优势
- 通过then来实现异步的执行,解决了回调嵌套问题(本质 依然是回调)
- 解决异步并发问题(promise.all)
- 使错误处理变得简单
-
初步实现思路
- 基本状态实现
- 添加then
- 显示异步操作
const PENDING = "PENDING" const REJECTED = "REJECTED" const FULFILLED = "FULFILLED" class Promise{ constructor(exector){ this.status = PENDING this.value = null this.reason = null this.onResolveCallback = [] this.onRejectedCallback = [] const resolve = (value)=>{ if(this.status===PENDING){ this.status = FULFILLED this.value = value this.onResolveCallback.forEach(fn=>fn()) } } const reject =(reason) => { if(this.status === PENDING){ this.status = REJECTED this.reason = reason this.onRejectedCallback.forEach(fn=>fn()) } } try{ exector(resolve,reject) }catch(e){ reject(e) } } then(onFulfulled,onRejected){ if(this.status === FULFILLED){ onFulfulled(this.value) } if(this.status === REJECTED){ onRejected(this.reason) } if(this.status === PENDING){ this.onResolveCallback.push(()=>{ onFulfulled(this.value) }) this.onRejectedCallback.push(()=>{ onRejected(this.reason) }) } } } module.exports = Promise
-
实现then的流程
- then返回一个promise
- then可以实现值的穿透
const PENDING = "PENDING" const REJECTED = "REJECTED" const FULFILLED = "FULFILLED" function resolvePromise(promise2,x,resolve,reject){ if(promise2 === x){ return reject(new TypeError(`Chaining cycle detected for promise #<Promise> my`)) } if((typeof x === 'object' && x!==null) || (typeof x === 'function')){ let called = false try{ let then = x.then if(typeof then === 'function'){ then.call(x,y=>{ if(called)return called = true resolvePromise(promise2,y,resolve,reject) },e=>{ if(called)return called = true reject(e) }) } else { resolve(x) } }catch(e){ if(called)return called = true reject(e) } }else { resolve(x) } } class Promise{ constructor(exector){ this.status = PENDING this.value = null this.reason = null this.onResolveCallback = [] this.onRejectedCallback = [] const resolve = (value)=>{ if(value instanceof Promise){ return value.then(resolve,reject) } if(this.status===PENDING){ this.status = FULFILLED this.value = value this.onResolveCallback.forEach(fn=>fn()) } } const reject =(reason) => { if(this.status === PENDING){ this.status = REJECTED this.reason = reason this.onRejectedCallback.forEach(fn=>fn()) } } try{ exector(resolve,reject) }catch(e){ reject(e) } } then(onFulfulled,onRejected){ onFulfulled = typeof onFulfulled === 'function'?onFulfulled:v=>v onRejected = typeof onRejected === 'function'? onRejected:r=>{throw r} let promise2 = new Promise((resolve,reject)=>{ if(this.status === FULFILLED){ setTimeout(()=>{ try{ let x = onFulfulled(this.value) resolvePromise(promise2,x,resolve,reject) } catch(e){ reject(e) } }) } if(this.status === REJECTED){ setTimeout(() => { try{ let x = onRejected(this.reason) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } }); } if(this.status === PENDING){ this.onResolveCallback.push(()=>{ setTimeout(() => { try{ let x = onFulfulled(this.value) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } }); }) this.onRejectedCallback.push(()=>{ setTimeout(()=>{ try{ let x = onRejected(this.reason) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } }) }) } }) return promise2 } }
-
promise单元测试
- 安装测试工具
npm install promises-aplus-tests -g
- 开始测试:
promises-aplus-tests mypromise.js
Promise.deferred = function(){ let dfd = {} dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve dfd.reject = reject }) return dfd }
- 安装测试工具
-
Promise相关方法
-
实例方法
-
catch
catch(onRejected){ return this.then(null,onRejected) }
- 静态方法
- Resolve
Promise.Resolve = (value)=>{ return new Promise((resolve,reject)=>{ resolve(value) }) }
- Reject
Promise.Reject = (reason)=>{ return new Promise((resolve,reject)=>{ reject(reason) }) }
- all
Promise.all=function(value){ return new Promise((resolve, reject) => { let result = [] let times = [] function processMap (index, data) { result[index] = data if (times++ == value.length) { resolve(result) } } for (let i = 0; i < value.length; ++i) { Promise.resolve(value[i]).then(data => { processMap(i, data) }, reject) } }) }
- allSettled
Promise.allSettled = function (value) { return new Promise((resolve, reject) => { let result = [] let times = [] function processMap (index, data) { result[index] = data if (times++ == value.length) { resolve(result) } } for (let i = 0; i < value.length; ++i) { Promise.resolve(value[i]).then(data => { processMap(i, { status: 'fulfilled', data }) }).catch(reason => { processMap(i, { status: 'rejected', reason }) }) } }) }
- race
Promise.race = function(value){ return new Promise((resolve,reject)=>{ value.forEach(item=>{ Promise.resolve(item).then(resolve,reject) }) }) }
*原型链方法
Promise.prototype.finally = function(finall){ return this.then((value)=>{ return Promise.Resolve(finall()).then(()=>value) }, (finall)=>{ return Promise.Reject(finall()).then(()=>{throw reason}) }) }
-
将node函数转换成promise
function promisify(fn){ return function(...args){ return new Promise((resolve,reject)=>{ fn(...args,function(err,data){ if(err){ reject(err) } resolve(data) }) }) } } function promisifyAll(obj){ for(let key in obj){ if(typeof obj[key] == 'function'){ obj[key] = promisify(obj[key]) } } }
- 什么叫类数组
- 有索引
- 有长度
- 能遍历
let likeArray = { 0:0, 1:1, 2:2, 3:3, length:4 } let arr = [...likeArray]//报错
- 上述并不是类数组,因为无法进行解构,说明迭代不了
- 数组能遍历,是因为内部有迭代的方法,通过Symblo可以给上述类型设置迭代方法,使之成为类数组
const { values } = require("lodash") let likeArray = { 0:0, 1:1, 2:2, 3:3, length:4, get [Symbol.toStringTag](){ return 'MyArray' }, [Symbol.iterator]:function(){ let index = 0 return { next:()=>{ return { value:this[index],done:index++==this.length } } } } } let arr = [...likeArray] //不报错 console.log(arr) console.log(likeArray.toString())
- 生成器函数
- 可以看到,类数组只需要有迭代器就可以遍历,而迭代器需要我们手动实现,有一个函数可以自动生成迭代器,那就是生成器函数
function* read(){ yield 'vue'; yield 'vite'; yield 'node' } let it = read() console.log(it.next()) //{ value: 'vue', done: false }
- 生成器和普通函数的区别是,前面需要加* ,并且配合yeild使用
- 生成器返回一个迭代器,每次执行需要调用next方法,产出value,done
- done为true时,迭代结束
- 因此类数组中的迭代器可以修改为:
[Symbol.iterator]:function*(){ let index = 0 let len = this.length while(index!==len){ yield this[index++] } }
- 调用next通过传参可以给上一个yield的返回结果赋值,如果没有上一个yield,则传参无意义
function* read(){ let a = yield 'vue' console.log(a) //11 let b = yield 'vite' console.log(b) //1111 let c = yield 'node' console.log(c) //11111 } let it = read() console.log(it.next('1')) console.log(it.next('11')) console.log(it.next('1111')) console.log(it.next('11111'))
- promise和生成器函数结合
- promise通过then解决串行执行流程
- 通过promise.all解决并行执行流程
- 但是promise依然是基于回调的,其并没有完全结果嵌套问题
- 通过迭代器,可以使我们的异步串行代码更像是同步(底层依然是异步)
const fs = require("fs").promises const path = require("path") function* read(){ let name = yield fs.readFile(path.resolve(__dirname,'name.txt'),'utf-8') let age = yield fs.readFile(path.resolve(__dirname,name),'utf-8') return age }
- 但是转换成迭代器的时候依然很复杂
let it = read() let {value,done} = it.next() if(!done){ value.then(data=>{ let {value,done} = it.next(data) if(!done){ value.then(data=>{ let {value,done} = it.next(data) if(done){ console.log(value) } }) } }) }
- tj写了一个co库专门处理这个流程,我们也可以简单实现一个co库,来处理我们的生成器函数中的promise的串行调用问题
function co(it){ return new Promise((resolve,reject)=>{ function next(val){ let {value,done} = it.next(val) if(!done){ value.then((data)=>{ next(data) },(err)=>{ reject(err) }) } else { resolve(value) } } next() }) } co(read()).then(data=>console.log(data))
- 将生成器函数放入babel中进行还原,可以看到它的源码大概如下:
var _marked = /*#__PURE__*/regeneratorRuntime.mark(read); function read() { var a, b, c; return regeneratorRuntime.wrap(function read$(_context) {. while (1) { // while(true) 表示这个东西是一个状态机,根据状态的变化实现对应的逻辑, 这个逻辑会走多次 switch (_context.prev = _context.next) { case 0: _context.next = 2; return 'vue'; case 2: a = _context.sent; console.log(a, 'a'); _context.next = 6; return 'vite'; case 6: b = _context.sent; console.log(b, 'b'); _context.next = 10; return 'node'; case 10: c = _context.sent; console.log(c, 'c'); case 12: case "end": return _context.stop(); } } }, _marked); }
- 结合之前迭代器的知识可以分析这个源码流程
- 调用read函数返回一个迭代器,即regeneratorRuntime.wrap()返回一个迭代器
- 迭代器包含一个next方法,调用next方法返回value和done
- 而wrap里面的回调是一个状态机用来获取yeild后面对应的结果
- 那么wrap回调的结果是value
- 回调传入context来保证swith走哪一步,说明context里面包含对应的指针
- next可以传参给上一个yield的返回值,从而加入到switch流程中,说明context中必定会记录next函数的传参
- 通过调用next来实现switch进入哪一步,说明next中会调用wrap,wrap中的switch每走到下一步,会控制指针指向下下一个
- 因此我们可以尝试还原regeneratorRuntime类
const regeneratorRuntime = { mark(fn){ return fn }, wrap(iteratorFn){ const _context = { next:0, sent:null, done:false, stop(){ _context.done=true } } return { next(val){ _context.sent =val return { value:iteratorFn(_context), done:_context.done } } } } }
- 而async和await就是 co和生成器函数配合promise的语法糖,而async成为目前解决异步串行的最终方法
const fs = require("fs").promises const path = require("path") async function read(){ let name = await fs.readFile(path.resolve(__dirname,'name.txt'),'utf-8') let age = await fs.readFile(path.resolve(__dirname,name),'utf-8') return age } read().then(data=>{ console.log(data) })
- 计算机分配任务是以进程来分配,进程中包含着线程
- 浏览器是一个进程,而且是一个多进程模型(多进程好处就是一个进程挂掉不会影响其它进程)
- 一个tab就是一个独立的进程
- 浏览器默认有一个主进程,来调度其它进程(进程间的通信)
- 插件也有独立的进程管理
- gpu有绘图进程
- 浏览器的渲染进程
- ui 渲染线程 负责页面渲染,布局,绘制
- js引擎线程 执行js代码的
- 这两种线程是互斥的,不能同时执行,原因是js引擎可能会操作dom,而渲染进程依赖dom
- js是单线程的,但主要指的是主线程,在主线程执行的时候,同时会开启一下独立的线程,如:
- 定时器、发请求、用户事件、
- 执行异步任务时,主线程会开辟独立的线程处理
- js执行顺序是先执行同步任务,执行完当前同步任务之后,开始执行异步任务,而负责调度这些任务执行的也是一个线程,叫做事件触发线程,也就是浏览器的eventLoop事件环
- 异步任务分为宏任务和微任务
- 宏任务 macro-task:script脚本, ui渲染,定时器(setTimeout), 发请求, 用户事件,messageChannel, setImmediate(ie下有,比setTimeout性能好)
- 微任务 micro-task: Promise.then(语言本身提供的), queueMicrotashk MutationObserver (异步监控dom的变化)
- eventLoop执行流程
- 当前的主线程的同步任务可以看作一个最开始的宏任务
- 代码执行的过程的时候 会产生微任务和宏任务
- 当发生的宏任务时间到达的时候会被发入到宏任务队列中 (放入是回调) 宏任务只有一个队列
- 微任务是立刻放到队列中 (每次执行宏任务的时候会产生一个微任务队列)
- 当前宏任务执行完毕后,会清空本轮产生的微任务, 如果执行微任务的时候又产生了微任务,会放到当前微任务的尾部
- 再去扫描宏任务队列,如果有则取出第一个宏任务 , 再去执行
- 每次微任务是执行一批 , 宏任务是执行一个
-
<script> document.body.style.background = 'red'; console.log(1) Promise.resolve().then(()=>{ console.log(2) document.body.style.background = 'yellow'; }) console.log(3); </script>
- 渲染线程发生在微任务之后,所以是 1 3 2 yellow
-
<script> button.addEventListener('click',()=>{ console.log('listener1'); Promise.resolve().then(()=>console.log('micro task1')) }) button.addEventListener('click',()=>{ console.log('listener2'); Promise.resolve().then(()=>console.log('micro task2')) }) button.click(); // click1() click2() </script>
- 代码自动触发,相当于同步,会先输出listener1、listener2、其次处理微任务
-
<button id="1">1</button> <button id="2">2</button> <script> let button1 = document.getElementsByTagName("button")[0] let button2 = document.getElementsByTagName("button")[1] button1.addEventListener('click',()=>{ console.log('listener1'); Promise.resolve().then(()=>console.log('micro task1')) }) button2.addEventListener('click',()=>{ console.log('listener2'); Promise.resolve().then(()=>console.log('micro task2')) }) // button.click(); // click1() click2() </script>
- 点击执行,相当于执行两个宏任务,会先输出第一个宏任务里的同步代码listener1、在输出微任务micro task1,然后在执行第二个宏任务
-
<script> Promise.resolve().then(() => { console.log('Promise1') setTimeout(() => { console.log('setTimeout2') }, 0); }) setTimeout(() => { console.log('setTimeout1'); Promise.resolve().then(() => { console.log('Promise2') }) }, 0); </script>
- 先执行完当前同步任务遗留的微任务,输出Promise1,其次按顺序执行宏任务,输出setTimeout1,再执行当前宏任务下的微任务输出Promise2,在执行输出setTimeout2
-
console.log(1); async function async () { console.log(2); await console.log(3); console.log(4) } setTimeout(() => { console.log(5); }, 0); const promise = new Promise((resolve, reject) => { console.log(6); resolve(7) }) promise.then(res => { console.log(res) }) async (); console.log(8);
- 先执行同步 1、6、2、3、8在执行微任务7、4、在执行宏任务5
node中的模块加载规范为commonJS规范,规范中通过require来引入模块。
- 暴漏出一个text模块
let str = 'hello world' module.exports = str
- node中的源码无法通过打断点进行调试,这里借助vscode,在debug模式中,新建launch.js文件,并且将"skipFiles"内容 清空,表示不跳过node核心代码,然后在require处打断点,即可进入require内部源码中
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "pwa-node", "request": "launch", "name": "Launch Program", "skipFiles": [ // "<node_internals>/**" ], "program": "${workspaceFolder}\\node架构\\require加载\\require加载.js" } ] }
- 分析源码执行流程:
- require一个模块后,默认调用Module._load方法,传入require的文件名
- 通过Module._resolveFilename方法,解析文件名,此时会默认添加文件的后缀,返回文件路径
- 创建当前模块实例const module = new Module() => {id:文件名,exports:{}}
- 调用module.load()方法,传入解析的文件名
- 根据文件的后缀名来调用相关的读取方法 Module._extensions[extension](this, filename);
- 此时会将模块内容读出来,内容为字符串形式
- 调用module._compile方法,通过该方法将text的内容包装成一个函数字符串
function(){ let str = 'hello world' module.exports = str }
- 通过vm.compileFunction()执行该字符串变成一个真的函数并执行,执行会把数据给module.exports
- 最终返回module.exports
- 调用require,会执行Module类中的一些列方法,首先要创建Module类,并分别添加相对应方法
function Module(id){ this.id = id this.export = {} }
- 对文件的全局路径查找
- require中传入的参数本质上是一个文件名,这个文件名可以不加后缀,依次在查找的时候 需要考虑
- 若有后缀,则直接拼接路径,存在该路径下的文件,则返回
- 若无后缀,则需在第一步基础上不断拼接可能的后缀,同时判断该路径是否存在文件,若存在,则返回
- 两种情况都不符合,则报错
Module._resolveFilename = function(id){ const filePath = path.resolve(__dirname,id) //如果有后缀 if(fs.existsSync(filePath)){ return filePath } //如果无后缀 const exts = Object.keys(Module._extension); for(let i = 0;i<exts.length;++i){ let file = filePath+exts[i] if(fs.existsSync(file)){ return file } } throw Error('Cannot find module:' + id) } Module._extension = { '.js'(){}, '.json'(){} }
- require中传入的参数本质上是一个文件名,这个文件名可以不加后缀,依次在查找的时候 需要考虑
- 之后创建一个新的module实例
- 调用实例中的load方法,截取扩展名
Module.prototype.load = function(filename){ let ext = path.extname(filename) Module._extension[ext](this) }
- 调用Module._extension方法对引入的文件进行内容读取,使用函数包裹模块内容,调用函数,将内容给module.exports
Module._extension = { '.js'(module){ //读取模块内容 const content = fs.readFileSync(module.id,'utf8') //获取包裹函数 let wrapperFn = vm.compileFunction(content,['exports','require','module','__filename','__dirname']) let exports = this.exports let thisValue = exports let require = myRequire let filename = module.id let dirname = path.dirname(filename) Reflect.apply(wrapperFn,thisValue,[exports, require, module, filename, dirname]) }, '.json'(module){ //若内容是json,则直接赋值 const content = fs.readFileSync(module.id,'utf8') module.exports = JSON.parse(content) } }
- 最终myRequire函数为:
function myRequire(id){ //获取完整路径 let absPath = Module._resolveFilename(id) const module = new Module(absPath) module.load(absPath) return module.exports }
- 测试
let str = myRequire('./text') console.log(str) //hello world
node中事件环和浏览器的事件环并不相同
-
node中有6个任务队列,当同步任务完成后,异步任务会根据类型分别放入这6个任务队列中,且这六个队列的执行顺序也不相同 ┌───────────────────────────────────┐ ┌─>│timers(计时器)执行 │ │ |setTimeout以及setInterval的回调 │ │ └──────────┬────────────────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ 处理网络,流,TCP的错误 │
│ │ callback │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ │ node内部使用 │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │poll(轮询) │<─────┤ connections, │ │ │ 执行poll中的i/o队列检查 │ │data, etc. │ │ │定时器是否到时 │ └───────────────┘ │ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ check │ │ │ 存放setImmediate回调 │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ │ 关闭的回调例如 │ │ socket.on('close') │ └───────────────────────┘ -
当同步任务执行完之后,会从timer任务开始执行,然后这里的回调执行完,依次向下找下一个队列
-
timer里存储的是定时器回调,当定时器时间到达,循环机制跳回到这个队列,执行里面的回调,否则走下面的队列
setTimeout(function(){
console.log('setTimeout')
},0)
setImmediate(function(){
console.log('setImmediate')
})
- 根据之前的解释,如果定时器到时间,那么timer里面关于该定时器的回调会执行,然后,再执行后面的队列,而check队列,明显再timer队列之后
- 但是上述代码的输出却不确定
- 因为在同步运行的时候,虽然定时器是0,但是代码可能因为性能或者其它原因,开始计时的时间不确定,因此同步执行完之后,可能计时器还没开始工作,因此timer队列可能不会先执行,而是执行后面的队列,后面定时器到时间了,反过来执行timer
- 因此上述代码谁先输出不确定
require('fs').readFile('./node事件环.md',(err,data)=>{
setTimeout(function(){
console.log('setTimeout')
},0)
setImmediate(function(){
console.log('setImmediate')
})
})
- 上述结果输出setImmediate、setTimeout
- 在第一轮循环机制中,check没有内容,因此循环会停留(堵塞)在poll轮询队列中,等待其出现内容,或者timer中时间到,调用其回调
- 在等到poll中有了回调后,开始执行回调,
- 然后进入下一个队列执行内容
- 下一个队列为check队列,所以执行setImmediate
node中events模块是一个发布订阅类
- 简单的发布订阅
const EventEmitter = require("events")
const { grep } = require("jquery")
const girl = new EventEmitter()
//订阅
girl.on('失恋了',function(boy){
console.log('哭',boy)
})
girl.on('失恋了',function(boy){
console.log("吃",boy)
})
function sleep(boy){
console.log('sleep')
}
girl.on('失恋了',sleep)
//订阅
girl.emit('失恋了','小明')
//取消订阅
girl.off('失恋了',sleep)
girl.emit('失恋了','小明') //再次执行,没有sleep
//只执行一次
girl.once('失恋了',function(boy){
console.log('健身')
})
girl.emit('失恋了','小明')
girl.emit('失恋了','小明') //再次执行没有健身
- 实例化一个events之后,通过on来实现订阅,提供订阅名和执行的行为
- 通过emit来发布之前的订阅,通过emit触发的订阅名,执行之前的行为
- off可以取消之前的订阅
- once函数可以实现只发布一次,即执行一次之后改订阅失效
- 声明一个构造函数,构造函数中有一个对象(队列),用来存储订阅
- 该构造函数的原型上有四个方法,分别是on、emit、off、once
function EventEmitter () {
this._events = {}
}
- 调用on
- 先用on进行订阅,这里要考虑函数通过绑定原型的方式和EventEmitter类关联,然后再通过实例化这个函数来调用EventEmitter相关的原型方法,此时实例是没有_events属性的,因此要进行一次判断
- 属性名不是newListener,需要调用newListener方法
- 根据属性名依次追加行为到对应数组
EventEmitter.prototype.on = function (eventName, callback) {
if (!this._events) {
this._events = {}
}
if (eventName !== "newListener") {
if(this._events["newListener"]){
this._events["newListener"].forEach(fn=>fn(eventName))
}
}
this._events[eventName] = this._events[eventName] || []
this._events[eventName].push(callback)
}
- 调用emit
- 通过属性,依次遍历行为数组,调用该行为
EventEmitter.prototype.emit = function (eventName, ...args) { if (!this._events) { this._events = {} } this._events[eventName].forEach((cb) => { cb(...args) }) }
- 调用off
- off方法对对应的属性上的行为进行取消
EventEmitter.prototype.off = function (eventName, callback) { if (!this._events) { this._events = {} } this._events[eventName] = this._events[eventName].filter(item => { return item !== callback && item.l !== callback }) }
- once 方法
- 利用高阶函数,绑定另一个函数,函数里面执行完callback后,取消该函数
- 该函数属性l绑定callback,这样可以通过off进行取消
EventEmitter.prototype.once = function (eventName, callback) { let once = (...args) => { callback(...args) this.off(eventName, once) } once.l = callback this.on(eventName, once) }
- 导出
module.exports = EventEmitter
Monorepo是管理项目代码的一种方式,指在一个项目仓库管理多个模块/包,Vue3源码采用Monorepo进行管理
- 一个仓库可以维护多个模块。
- 方便版本管理何依赖管理,模块之间的引用,调用都很便捷。
- Vue3使用pnpm workspace实现monorepo
- 全局安装pnpm:npm install pnpm -g
- pnpm init 初始化项目
- 创建pnpm-wrokspace.yaml文件
- 告诉pnpm,文件的包都安装再packages文件夹下:
packages: - 'packages/*'
- 这样,基本的Monorepo环境就配置好了,创建packages文件夹,然后在里面创建两个项目文件夹,分别为reactivity、shared
- 安装vue,并指定安装在根目录(-w)
- pnpm install vue -w
- 安装相关模块依赖
- -D指开发依赖
- pnpm install esbuild typescript minimist -D -w
- 分别初始化模拟的两个项目(reactivity和shared)
- 并配置打包后的引用路径,package.json内容如下:
{ "name": "@vue/reactivity", "version": "1.0.0", "description": "", "main": "dist/reactivity.cjs.js", "module": "dist/reactivity.esm-bundler.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
{ "name": "@vue/shared", "version": "1.0.0", "description": "", "main": "dist/shared.cjs.js", "module": "dist/shared.esm-bundler.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
- 创建ts配置文件
- pnpm tsc --init
- 配置tsconfig.json文件
{ "compilerOptions": { "outDir": "dist", // 输出的目录 "sourceMap": true, // 采用sourcemap "target": "es2016", // 目标语法 "module": "esnext", // 模块格式 "moduleResolution": "node", // 模块解析方式 "strict": false, // 严格模式 "resolveJsonModule": true, // 解析json模块 "esModuleInterop": true, // 允许通过es6语法引入commonjs模块 "jsx": "preserve", // jsx 不转义 "lib": ["esnext", "dom"], // 支持的类库 esnext及dom, "baseUrl":".", "paths": { "@vue/*":["packages/*/src"] } } }
- 通过配置baseUrl,可以通过"@vue"来访问package/*/src文件
- 配置项目内容
- 在shared的src文件夹下创建index.ts文件,创建检测对象的方法
export const isObject = (value)=>{ return typeof value === 'object'&& value !==null }
- 在reactivity的src的index.ts文件中引用
import { isObject } from "@vue/shared" console.log(isObject('abc'))
- 配置打包文件
- 根目录新建scripts/dev.js
- package.json添加scripts选项
"scripts": { "dev":"node scripts/dev.js reactivity -f global" },
- 表示dev命令会运行dev.js ,并处理reactivity,global表示全局,cjs表示commonjs规范,esm表示esmodule规范
- 配置dev.js
- 引入minimist来解析参数
const args = require("minimist")(process.argv.slice(2)) console.log(args)
- 运行npm run dev查看输出结果为:{ _: [ 'reactivity' ], f: 'global' }
- 最终文件如下:
const args = require("minimist")(process.argv.slice(2)) const path = require('path') //对接收的文件做处理 const target = args._[0] || 'reactivity' const format = args.f||'global' const entry = path.resolve(__dirname,`../packages/${target}/src/index.ts`); //定义打包格式 // iife 自执行函数 global (function(){})() 增加一个全局变量 // cjs commonjs 规范 // esm es6Module const outputFormat = format.startsWith('global')?'iife':format === 'cjs'?'cjs':'esm' //出口文件 const outfile = path.resolve(__dirname,`../packages/${target}/dist/${target}.${format}/index.js`) //引入打包模块 const {build} = require('esbuild'); build({ entryPoints: [entry], outfile, bundle: true, sourcemap: true, format: outputFormat, // globalName, platform: format === 'cjs' ? 'node' : 'browser', watch: { // 监控文件变化 onRebuild(error) { if (!error) console.log(`rebuilt~~~~`) } } }).then(() => { console.log('watching~~~') })
- 运行npm run dev 即可打包文件
不同于Vue2中的defineProperty方法,Vue3中采用proxy实现对数据的绑定,这种方式有以下好处:
- 无需重写getter和setter对数据进行劫持
- 无需实现$set和$delete方法实现对新增和删除属性的监控
- 不许对数组进行单独处理
Vue3通过reactive模块来实现数据绑定,通过effect模块实现数据的更新
const {effect,reactive } = Vue
//对数据进行绑定
const obj = {
name:'sx',
age:13,
address:{
num:30
},
flag:true
}
//这里只能传入对象,因为proxy只支持对象格式
const state = reactive(obj)
//数据响应
//effect函数会默认执行一次,后续数据发生变化会重新执行effect函数
effect(()=>{
app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
})
setTimeout(()=>{
state.age++
},1000)
根据其借助proxy的原理,可以实现reactive模块中的数据劫持
- 新建reactive.ts文件,用于实现主要逻辑
import {isObject} from "@vue/shared"
export function reactive(target){
//传入的值需要是一个对象,如果不是,则不需要进行绑定
if(!isObject(target)){
return target
}
}
- 原index.ts负责导出reactive模块
export {reactive} from "./reactive"
- 在reactive中实现初步的proxy代理
//实现最初的proxy
const proxy = new Proxy(target,{
get(target,key,receiver){
console.log('这个属性被取到了')
return target[key]
},
set(target,key,value,receiver){
console.log('这个属性改变了')
target[key] = value;
return true
}
})
return proxy
对数据进行绑定的意义,在于当数据变化时可以随时更新页面,这里通过get方法,当数据被访问时,就可以和effect绑定,当该属性触发set函数时,同样触发effect更新即可实现数据的劫持
但是这种设置却有很大的问题,那就是对象的this指向问题
- 对象一
let person = {
name:'sx',
age(){
return 18
}
}
const proxy = new Proxy(person,{
get(target,key,receiver){
console.log('这个属性被取到了')
return target[key]
},
set(target,key,value,receiver){
console.log('这个属性改变了')
target[key] = value;
return true
}
})
proxy.name
proxy.age
可以看到,这种对象用proxy可以顺利绑定每一个属性,但是下面这个却不能
- 对象二
let person = {
name:'sx',
get age(){
return this.name
}
}
const proxy = new Proxy(person,{
get(target,key,receiver){
console.log('这个属性被取到了')
console.log(key)
return target[key]
},
set(target,key,value,receiver){
console.log('这个属性改变了')
target[key] = value;
return true
}
})
proxy.age
这里访问proxy.age,按照设想和代码逻辑,当触发get之后,age属性会和effect进行绑定,当age属性值变化后,set中会让effect进行更新
但是这里的age依赖的却是name,也就是说,当name发生改变时,实际上age的返回值也会发生改变,但是proxy却监听不到,也就无法触发effect对age进行更新
这里的原因就是,age中的this指的是person对象,当通过proxy.age访问时,只会触发age属性的get,而不会触发name属性的get,自然当name变化时,effect不会触发更新
要解决这个办法,只需要把this的指向改为proxy对象即可,而Reflect对象即可完成这件事
let person = {
name:'sx',
get age(){
return this.name
}
}
const proxy = new Proxy(person,{
get(target,key,receiver){
console.log('这个属性被取到了')
console.log(key)
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver){
console.log('这个属性改变了')
target[key] = value;
return Reflect.set(target,key,value,receiver)
}
})
proxy.age
此时访问proxy.age,会同时触发age的get和name的get 事实上,每次进行Proxy绑定对象属性也会对性能有所消耗,因此要尽可能减少proxy的使用,reactive接收一个对象,如果用户多次输入同一个对象,则没有必要每次对对象进行Proxy,只需要从缓存里拿就好
- 情况一:用户输入同一数据对象
- 新建weakMap对象,判断是否存在该数据对象即可
const existing = reactiveMap.get(target) if(existing){ return existing }
- 在创建好proxy之后,存入weakMap中
reactiveMap.set(target,proxy)
- 情况二:用户第二次传入数据对象对应的proxy
const state = reactive(obj)
const state2 = reactive(state)
如果第二次传入的是proxy实例,则只需要触发其get方法验证,如果get能触发,则说明是proxy,直接返回即可
- 新建枚举对象
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
- 触发get
if(target[ReactiveFlags.IS_REACTIVE]){
return target
}
- get中进行判断
if(key === ReactiveFlags.IS_REACTIVE){
return true
}
自此实现了Vue3中的数据绑定
- reactive函数对数据进行proxy劫持
- 调用effect函数,传入用户定义函数
- 用户定义函数会自执行一次,其内存在对数据的调用
- 对数据的调用会触发proxy接触
- 如果是触发get,则把当前触发的属性和当前effect绑定
- 如果触发set,则把当前属性绑定的effect取出,并调用,使之进行数据更行
const {effect,reactive } = VueReactivity
//对数据进行绑定
const obj = {
name:'sx',
age:13,
address:{
num:30
},
flag:true
}
//这里只能传入对象,因为proxy只支持对象格式
const state = reactive(obj)
// const state2 = reactive(state)
//数据响应
//effect函数会默认执行一次,后续数据发生变化会重新执行effect函数
effect(()=>{
app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
})
setTimeout(()=>{
state.age++
},1000)
ReactiveEffect类是effect的构造函数,其内部有控制传入的函数执行的函数run
- 声明effect函数,实质是在内部实例化一个ReactiveEffect对象,并调用其run函数实现初次effect的执行,从而触发proxy,
export function effect(fn){
const _effect = new ReactiveEffect(fn)
_effect.run()
}
- run函数的作用不仅是执行其传入的回调,触发proxy,同时会将上下文暴漏给外部,赋值给activeEffect,由于js执行机制为单线程,因此当暴漏出指针后,触发proxy的get或者set,利用track函数进行依赖收集
run(){
//依赖收集,让属性和effect产生关联
//如果没有激活,则不进行依赖收集
if(!this.active){
return this.fn()
}else {
try{
//让activeEffect指向当前effect,
activeEffect = this
//触发react中的get或set
return this.fn()
}
finally{
activeEffect = undefined
}
}
}
- 当触发proxy中的get,会调用依赖收集函数track,收集属性对应哪个effect,主要格式为:obj -> key -> effect。
const targetMap = new weakMap()
export function track(target,key){
if(activeEffect){
//判断是否存在该对象的键值
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap = new Map()))
}
//判断是否存在该属性的键值
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,(deps = new Set()))
}
trackEffects(deps)
}
}
export function trackEffects(deps){
let shouldTrack = !deps.has(activeEffect)
if(shouldTrack){
deps.add(activeEffect)
}
// 在ReactiveEffect中声明公有变量deps,用来存储属性对应的集合deps
activeEffect.deps.push(deps)
}
- 触发更新函数trigger在proxy的set中,当传进来的新值不等于旧值时,执行set的赋值操作,并触发trigger,trigger函数中会取出该属性所依赖的effect,依次执行其中的run函数,这样就完成了数据的更新
export function trigger(target,key,value){
let depMaps = targetMap.get(target)
if(!depMaps){
return //没有依赖收集
}
let effects = depMaps.get(key)
triggerEffects(effects)
}
export function triggerEffects(effects){
if(effects){
effects.forEach(effect=>{
effect.run()
})
}
}
至此,我们根据最初的范例,已经能够实现数据改变,页面更新的效果,但是依然还有一些细节需要完善
- effect的嵌套问题
effect(()=>{
effect(()=>{
state.age = 18
})
app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
})
我们通过用activeEffect记录effect内部实例的方式来暴漏出effect,从而实现依赖收集,到那时effect的run函数执行完之后,activeEffect会赋值为undefined,这就暴漏一个问题,activeEffect起初指向外层effect,然后指向内层effect,再然后执行完内层effect被赋值为undefined,但是外层还没有进行依赖收集,此时进行依赖收集将无法找到绑定的effect
这种嵌套的调用类似一种树状结构,因此我们可以用activeEffect记录当前环境,当环境改变,记录其parent即可
run(){
if(!this.active){
return this.fn()
}else {
try{
this.parent = activeEffect
activeEffect = this
return this.fn()
}
finally{
activeEffect = this.parent
this.parent = null
}
}
}
- effect调用自己的问题
effect(()=>{
state.age = Math.random()
app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
})
setTimeout(()=>{
state.age++
},1000)
当effect完成依赖收集后,调用setTimeout函数,触发proxy的set,从而触发trigger更新,但是触发的过程中遇到 app.age = Math.random() ,会重复触发,使程序停不下来
这种明显是我们不想看到的,因此再触发的时候可以进行判断,如果触发的effect和当前的activeEffect相同,则不进行更新操作
effects.forEach(effect=>{
if(effect !== activeEffect){
effect.fn()
}
})
- 清除多余依赖
effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
})
setTimeout(() => {
state.flag = false
setTimeout(() => {
state.age++
}, 1000);
}, 1000)
effect进行依赖收集之后,调用外面的定时器,会将数据隐藏,此时再调用里面定时器改变age的值,页面依然刷新,这对性能印象很大
解决这个问题就需要对依赖进行清除,当之前run函数进行依赖收集之前,将属性对该effect产生的依赖进行清除
function cleanEffect(effect){
let deps = effect.deps
for(let i = 0;i<deps.length;++i){
deps[i].delete(effect)
}
effect.deps.length = 0
}
这样并不能解决问题,反而造成程序死循环,原因就是在进行trigger更新的时候,会循环遍历effect,依次执行run,再run 中又会cleaneffect依赖,重新收集依赖,从而造成死循环 要解决这个方法只需要trigger时,新建一个effects集合即可
if(effects){
effects = new Set(effects)
effects.forEach(effect=>{
if (effect !== activeEffect) { // 保证要执行的effect不是当前的effect
effect.run(); // 数据变化了,找到对应的effect 重新执行
}
})
}
- effect返回值
effect可以返回一个值runner,其包含了停止更新的函数stop,也可以手动控制更新runner()
- ReactiveEffect中新增一个stop函数
stop(){
if(this.active){
this.active = false
}
cleanEffect(this)
}
- effect中新增返回值runner
export function effect(fn){
const _effect = new ReactiveEffect(fn)
_effect.run()
let runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
let runner = effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
})
runner.effect.stop()
setTimeout(() => {
state.flag = false
setTimeout(() => {
state.age++
}, 1000);
}, 1000)
可以看到,调用stop后,数据不再更新,重新调用runner,页面继续更新
let runner = effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
})
runner.effect.stop()
setTimeout(() => {
state.flag = false
runner()
}, 1000)
- 更新调度函数的实现
let runner = effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
})
setTimeout(() => {
state.age++
state.age++
state.age++
}, 1000)
这里页面会渲染三次,而不会是等age全部更新完渲染一次,可以采用promise异步来做调度
let runner = effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
if(flag){
flag = false
Promise.resolve().then(()=>{
runner()
})
}
})
setTimeout(() => {
state.age++
state.age++
state.age++
}, 1000)
effect同样自身也实现了调度函数,即effect可以传递第二个参数,为一个对象,对象中如果有scheduler函数,则数据变化执行scheduler函数,如果没有,则执行effect.run
let flag = true
let runner = effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
},{
scheduler(){
if(flag){
flag = false
Promise.resolve().then(()=>{
runner()
})
}
}
})
setTimeout(() => {
state.age++
state.age++
state.age++
}, 1000)
在triggerEffects函数中进行判断
export function triggerEffects(effects) {
if(effects){
effects = new Set(effects)
effects.forEach(effect =>{
if(effect !== activeEffect){
if(effect.scheduler){
effect.scheduler()
}else{
effect.run()
}
}
})
}
}
- 对引用类型进行数据绑定
通过proxy进行绑定的数据只是对obj最外层做代理,里面不会被监控到
let runner = effect(() => {
console.log('render')
app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
},{
scheduler(){
if(flag){
flag = false
Promise.resolve().then(()=>{
runner()
})
}
}
})
setTimeout(() => {
state.address.num = 40
}, 1000)
如上,改变state.address.num不会触发更新,但是在访问到state.address时,会触发,只需要在触发的时候做一层判断即可
get(target,key,receiver){
if(key === ReactiveFlags.IS_REACTIVE){
return true
}
track(target,key)
let res = Reflect.get(target,key,receiver)
if(isObject(res)){
return reactive(res)
}
return res
},
computed可以传入一个函数,也可以传入一个对象(带有get和set方法),计算属性返回一个计算值,该值通过value属性访问,当参与计算的数据发生改变,则重新计算,不发生改变,则直接返回之前缓存的值
- render 只执行了一次
const { effect, reactive,computed } = VueReactivity
const state = reactive({
firstName:'s',
lastName:'x'
})
let fullName = computed(()=>{
console.log('runner')
return state.firstName + state.lastName
})
console.log(fullName.value)
console.log(fullName.value)
console.log(fullName.value)
计算属性返回值可以作为属性参与effect更新
const { effect, reactive,computed } = VueReactivity
const state = reactive({
firstName:'s',
lastName:'x'
})
let fullName = computed({
get(){
return state.firstName + state.lastName
},
set(value){
state.lastName = value
}
})
fullName.value = 100
effect(()=>{
app.innerHTML = fullName.value
})
setTimeout(()=>{
state.firstName = 'x'
},1000)
- 导出一个computed函数,函数内部有两个变量getter、setter
- 函数传入一个对象或者函数。
- 如果是函数,则将该函数赋值给getter,setter赋值为一个报错函数(即抛出错误)
- 如果是对象,则将其get和set分别赋值给getter和setter
- 创建computedRefImpl类,有两个形参(getter、setter),在computer函数中实例化并返回该类
- 类中在实例的constructor构造器中通过实例化ReactiveEffect类,传入getter,实现对计算属性函数传入的属性的数据绑定,传入第二个参数,在数据更新完之后将_dirty标记为true,并实现数据更新(triggerEffects)
- 私有变量_dirty控制数据是否更新
- 其有两个方法get value 和 set value
- 当调用get,让ReactiveEffect实例运行,获取其返回值赋值给内部变量_value,并返回改变量,将_dirty设置为false,表示没有新的数据改变了,可以使用缓存,判断当前环境是否存在activeEffect,存在则进行依赖收集(调用trackEffects)
- 调用set则运行setter
完整代码如下:
import { isFunction } from "@vue/shared"
import { activeEffect, trackEffects, triggerEffects,ReactiveEffect } from "./effect";
export function computed(getterOrOptions){
let getter = null;
let setter = null;
let fn = ()=>{
throw new Error("this function is onlyRead");
}
let isGetter = isFunction(getterOrOptions)
if(isGetter){
getter = getterOrOptions
setter = fn
}
else {
getter = getterOrOptions.get
setter = getterOrOptions.set || fn
}
return new computedRefImpl(getter,setter)
}
class computedRefImpl{
private _value = null
private _dirty = true
public effect = null
public deps = null
constructor(getter,public setter){
this.effect = new ReactiveEffect(getter,()=>{
if(!this._dirty){
this._dirty = true
triggerEffects(this.deps)
}
})
}
get value(){
debugger
if(activeEffect){
// 存在effect,则进行依赖收集
trackEffects(this.deps || (this.deps = new Set()) )
}
if(this._dirty){
this._dirty = false
this._value = this.effect.run()
}
return this._value
}
set value(newValue){
this.setter(newValue)
}
}
Vue3中的watch方法用来监测数据的变化,然后执行某些操作,如监测用户的输入
- 如果监控的是一个对象,则无法拿到新值和旧值
const { effect, reactive, computed, watch } = Vue
// const { effect, reactive,computed } = VueReactivity
const state = reactive({
firstName: 's',
lastName: 'x'
})
watch(state, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
setTimeout(() => {
state.lastName = 'xxx'
}, 1000);
输入newValue和oldValue结果都为改变后的值
- 如果监控的是一个对象,则会深度监控,即迭代监控对象的每一个属性,因此要尽量避免使用对象,减少资源浪费
- 监控的是某个属性,则需写成函数式
const { effect, reactive, computed, watch } = Vue
// const { effect, reactive,computed } = VueReactivity
const state = reactive({
firstName: 's',
lastName: 'x'
})
watch(()=>state.lastName, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
setTimeout(() => {
state.lastName = 'xxx'
}, 1000);
- 其第二个参数可以传入三个参数,第三个参数是节流控制参数,方式数据多次改变异步并发
- 当第一次改变state.age时,onCleanup中的回调不执行
- 第二次改变时,执行onCleanUp中的回调,此时如果上一步的异步还在执行,则会使其不发生作用
const state = reactive({ flag: true, name: 'jw ', age: 30 })
let i = 2000;
function getData(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(timer)
}, timer);
})
}
// 调用下一次的watch会执行上一次的onCleanup
watch(() => state.age, async (newValue, oldValue,onCleanup) => {
let f = false;
onCleanup(()=>{ // 当我们把age= 32的时候会执行第一次的onCleanup中的回掉
f = true;
})
i-=1000;
let r = await getData(i); // 2000
!f && (document.body.innerHTML = r);
}, { flush: 'sync' });
state.age = 31; // f = true 1s后应该渲染1000
state.age = 32; // f = false 0s后显示0
新值和旧值会输出
- watch传入两个参数,第一个参数为要监听的属性source,第二个参数为属性变化后的回调cb
- 判断source类型,若是Proxy,则深度监控,交给监控函数,若是函数,则执行下一步
- 创建job函数
- 函数首先判断是否存在onCleanUp回调,存在则执行
- 创建ReactiveEffect实例,第一个参数为source,第二个参数为job
- 运行一次ReactiveEffect实例,获取原先值oldValue
- 在job中运行ReactiveEffect实例,获取新值newValue,运行cb
- 将newValue赋值给oldValue
import { isFunction, isObject } from "@vue/shared";
import { isReactive } from "./baseHandler";
import { ReactiveEffect } from "./effect";
function traversal(value,set = new Set()){
if(!isObject(value)){
return value
}
if(set.has(value)){
return value
}
set.add(value)
for(let key in value){
traversal(value[key],set)
}
return value
}
export function watch(source,cb){
let get = null;
let cleanUp = null;
let oldValue = null;
if(isReactive(source)){
get = ()=>traversal(source)
}else if(isFunction(source)){
get = source
}
const onCleanUp = (fn)=>{
this.cleanUp = fn
}
let job = () => {
cleanUp && cleanUp()
let newValue = effect.run
cb(oldValue,newValue,onCleanUp)
oldValue = newValue
}
let effect = new ReactiveEffect(get,job)
oldValue = effect.run()
}
ref可以使普通值转换为响应式的对象
const { effect, reactive, computed, watch,ref } = Vue;
const state = reactive({
name:'s',
age:13
})
let flag = ref(false)
effect(()=>{
app.innerHTML = flag.value
})
setTimeout(()=>{
flag.value = true
},1000)
- ref实际就是对一个普通值做了一层包装,包装成一个对象,并通过其get和set实现依赖收集和更新,其实现原理类似于computed
import { isObject } from "@vue/shared";
import { reactive } from "./reactive";
import { activeEffect, trackEffects, triggerEffects } from "./effect";
export function ref(value){
return new RefImpl(value);
}
class RefImpl {
private _value = null;
private __v_isRef = true;
private dep = null;
constructor(public rawValue){
this._value = toReactive(rawValue)
}
get value(){
if(activeEffect){
trackEffects(this.dep ||(this.dep = new Set()))
}
return this._value
}
set value(newValue){
if(newValue !== this.rawValue){
this._value = toReactive(newValue)
this.rawValue = newValue
triggerEffects(this.dep)
}
}
}
export function toReactive(value){
return isObject(value) ? reactive(value):value
}
- 其中有一个toRef函数,可以解析proxy对象,对单个属性形成响应式
const { effect, reactive, computed, watch,ref,toRef } = Vue;
const state = reactive({
name:'s',
age:13
})
let name = toRef(state,'name')
effect(()=>{
app.innerHTML = name.value
})
setTimeout(()=>{
name.value = 'xx'
},1000)
其实现原理是利用get和set,当访问属性的value时,返回object[key].value
export function toRef(object,key){
return new ObjectRefImpl(object,key)
}
class ObjectRefImpl{
private __v_isRef = true;
private _value = null;
constructor(public object,public key){
}
get value(){
return this.object[this.key]
}
set value(newValue){
this.object[this.key] = newValue
}
}
- 同理 toRefs函数可以对proxy对象的所有属性进行响应式
const { effect, reactive, computed, watch,ref,toRef,toRefs } = Vue;
const state = reactive({
name:'s',
age:13
})
let {name,age }= toRefs(state)
effect(()=>{
app.innerHTML = name.value + age.value
})
setTimeout(()=>{
name.value = 'xx'
},1000)
- 通过toRefs可以直接通过属性.value进行访问,但是每次访问加上value,未免太过麻烦,有什么办法可以直接访问属性吗?proxyRefs可以实现这个需求
export function proxyRefs(object){
return new Proxy(object,{
get(target,key,receiver){
let r = Reflect.get(target,key,receiver)
return r.__v_isRef?r.value:r
},
set(target,key,value,receiver){
if(target[key].__v_isRef ){
target[key].value = value
return true
}
return Reflect.set(target,key,value,receiver)
}
})
}
vue3中渲染模块有两个子模块,分别是runtime-dom和runtime-core,其中,runtime-dom模块提供了常用的节点操作api和属性操作的api,而runtime-core中则包含虚拟dom的创建,diff算法等。
通过vue3中的runtime-core可以实现自己的渲染逻辑
本节来完善runtime-dom中的domapi,以便后面供runtime-core模块使用
<script src="./runtime-dom.global.js"></script>
<div id="app"></div>
<script>
const {createRenderer,h } = VueRuntimeDOM
const {render,createApp} = createRenderer({
})
console.log(h('h1','hello world'),app)
</script>
- 结果输出虚拟dom h1节点
- h函数为runtime-core中创建虚拟dom的函数
- createRenderer函数可以让用户自己创建一个渲染器,从而创建元素
- createRenderer函数里面,需要用户传入如何操作节点的命令,即元素创建api和属性创建api
- 在runtime-dom新建nodeOps模块和patchProp模块,分别处理dom操作和属性操作
- dom 操作
- nodeOps模块为一个类,包含各种元素操作方法,主要有:
- 元素创建
- 文本节点创建
- 元素插入
- 移除节点
- 获取节点
- 获取节点父节点
- 获取兄弟节点
- 给文本节点设置内容
- 给元素节点设置内容
export const nodeOps = { createElement(tagName){ return document.createElement(tagName) }, createTextNode(text){ return document.createTextNode(text) }, insert(element,container,anchor = null){ container.insertBefore(element,anchor); // ==appendChild }, remove(child){ const parent = child.parentNode; if(parent){ parent.removeChild(child); } }, querySelector(selectors){ return document.querySelector(selectors); }, parentNode(child){ // 父节点 return child.parentNode }, nextSibling(child){ // 获取兄弟元素 return child.nextSibling }, setText(element,text){ // 给文本节点设置内容 element.nodeValue = text; }, setElementText(element,text){ // 给元素节点设置内容 innerHTML element.textContent = text; } }
- 属性操作
- 对节点的属性操作包含多种情况,取决于节点的属性是什么,总体来说可以分为四种:
- 类名
- 行内样式
- 事件
- 其它属性
- patchProp方法需要接收四个参数,分别是el:元素,key:属性名,preValue:之前的属性值,nextValue:新的属性值
- 通过key判断属于哪一种情况,分别进行相关的处理
- key === class:
- 当是对类名的操作时,只需要判断nextValue是否存在,若存在则直接替换,若不存在则删除改属性
export function patchClass(el,nextValue){ if(nextValue == null){ el.removeAttribute('class') }else{ el.className = nextValue } }
- 当是对类名的操作时,只需要判断nextValue是否存在,若存在则直接替换,若不存在则删除改属性
- key === 'style':
- 若key为style,直接用nextValue进行替换对效率不太好,最好的方法是将nextValue和preValue进行对比,若之后存在的则保留,不存在的则删除。
export function patchStyle(el,preValue,nextValue){ // 我如何比较两个对象的差异? if(preValue == null) preValue = {}; if(nextValue == null ) nextValue = {} // 比对两个对象 需要同时遍历 新的和老的 const style = el.style for(let key in nextValue){ style[key] = nextValue[key] } if(preValue){ for(let key in preValue){ if(nextValue[key] == null){ // 老的有 新的没有 需要删除老的 style[key] = null; } } } }
- key为事件
- 若为事件,则需要将之前的事件缓存起来,当有新事件时,则直接添加新事件并缓存,若事件名不变,事件函数繁盛改变,则需要从缓存中取出原事件,将事件的执行内容替换
function createInvoker(preValue) { const invoker = (e) => { invoker.value(e) }; // 这个地方需要调用才会执行 invoker.value invoker.value = preValue; // 后续只需要修改value的引用就可以 达到调用不同的逻辑 return invoker } export function patchEvent(el, eventName, nextValue) { const invokers = el._vei || (el._vei = {}); const exitingInvoker = invokers[eventName]; if (exitingInvoker && nextValue) { // 进行换绑 exitingInvoker.value = nextValue; } else { // 不存在缓存的情况 addEventListener('click') const eName = eventName.slice(2).toLowerCase() if (nextValue) { const invoker = createInvoker(nextValue); // 默认会将第一次的函数绑定到invoker.value上 // el._vei = {onClick: invoker} invokers[eventName] = invoker; // 缓存invoker el.addEventListener(eName, invoker); } else if (exitingInvoker) { // 没有新的值,但是之前绑定过 我需要删除老 el.removeEventListener(eName, exitingInvoker); invokers[eventName] = null; // 缓存invoker } } }
- 其它属性的处理和class处理类似
- 对节点的属性操作包含多种情况,取决于节点的属性是什么,总体来说可以分为四种:
- nodeOps模块为一个类,包含各种元素操作方法,主要有:
- 渲染器的创建
- 除去自己创建一个渲染器外,根据domapi和属性操作api,vue内部会给用户提供一个创建好的渲染器, 渲染器函数为createRenderer,传入要操作的domapi,即可返回一个渲染器函数render
const renderOptions = {patchProp,...nodeOps} // vue内置的渲染器,我们也可以通过createRenderer 创建一个渲染器,自己决定渲染方式 export function render(vnode,container){ let {render} = createRenderer(renderOptions); return render(vnode,container) }
vue3中节点的渲染操作主要在runtime-core包中,runtime-core不关心运行的平台。
vue3中通过函数createVNode函数创建一个虚拟节点,其传入三个参数,分别是节点名,属性对象,孩子,如:
const {createVNode,render,h} = VueRuntimeDOM
console.log(createVNode('h1',{key:1},'Hello world'))
可以看到,输出的对象就是一个虚拟节点,其对象包含内容如下:
- children:节点包含的孩子内容,可以是一个字符串,也可能是一个虚拟节点数组
- el:改虚拟节点后面渲染成真实节点后,会被el记录,即 vnode.el 就是其真实节点
- key:虚拟节点的标识,也用作区分不同的节点
- props:虚拟节点的属性
- shapeFlags:节点的类型标识
- type:节点名
- __v_isVNode:判断是否是真实节点
- h函数似乎拥有与createVnode函数类似的功能,如上述例子改成h函数
console.log(h('h1',{key:1},'Hello world'))
- 可以看到其结果依然是一个虚拟节点,其实h函数就是在createVNode函数的基础上,做了一层封装,createVNode函数必须传入三个参数,而h函数则不用,如
h('h1','Hello world')
,内部会将其转换成createVNode('h1,null,'Hello world')
- 虚拟节点中有了type来识别节点类型,为什么还要有shapeFlags类型呢,原因就是为了判断该节点是否有孩子及孩子类型是什么
- 首先规定不同的孩子类型对应不同的标识号
export const enum ShapeFlags { // vue3提供的形状标识
ELEMENT = 1, // 1
FUNCTIONAL_COMPONENT = 1 << 1, // 2
STATEFUL_COMPONENT = 1 << 2, // 4
TEXT_CHILDREN = 1 << 3, // 8
ARRAY_CHILDREN = 1 << 4, // 16
SLOTS_CHILDREN = 1 << 5, // 32
TELEPORT = 1 << 6, // 64
SUSPENSE = 1 << 7, // 128
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
- 而ShapeFlags就是记录这些标识,比如:
- 一个创建一个vnode,其type为h1
- 那么其类型属于ELEMENT,
- 即ShapeFlags = 1
- 若为其添加一个文本“hello world”,那么改文本节点属于TEXT_CHILDREN
- 则 ShapeFlags变为 1 | 8 = 9,即shapeFlags变为9
- 同样拿到一个节点如何判断其包含某个节点呢,如一个节点的 shapeFlags为17,
- 那么其包含TEXT_CHILDREN吗? 只需 shapeFlags & ShapeFlags.TEXT_CHILDREN
- 即上述结果变成17 & 8 ,结果为0,则不包含
- 那么其包含ARRAY_CHILDREN吗,同样操作shapeFlags & ShapeFlags.ARRAY_CHILDREN
- 即上述结果变为17 & 16 ,结果为1,说明包含
- 可以看到,通过给不同节点类型不同的编码,然后利用按位与(&)与按位或(|)就可以识别改节点的子节点类型
- 调用createVNode函数后,会默认创建一个vnode对象,该对象会根据传入的参数设定默认值
- 首先判断type类型是否为字符串,若是则shapeFlags默认为1
- 其次判断孩子类型,若是字符串,则 shapeFlags改为 shapeFlags | ShapeFlags.TEXT_CHILDREN
- 若为数组,则改为 shapeFlags | ShapeFlags.ARRAY_CHILDREN
- 最终返回节点
export function createVNode(type,props = null, children = null){
// 后续判断有不同类型的虚拟节点
let shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0 // 标记出来了自己是什么类型
// 我要将当前的虚拟节点 和 自己儿子的虚拟节点映射起来 权限组合 位运算
const vnode = { // vnode 要对应真实际的节点
__v_isVNode:true,
type,
props,
children,
key: props && props.key,
el:null,
shapeFlag
// 打个标记
}
if(children){
let temp = 0;
if(isArray(children)){ // 走到createVnode 要么是数组要么是字符串 h()中会对children做处理
temp = ShapeFlags.ARRAY_CHILDREN
}else{
children = String(children);
temp = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag = vnode.shapeFlag | temp;
}
// shapeFlags 我想知到这个虚拟节点的儿子是数组 还是元素 还是文本
return vnode;
}
- h函数是对createVNode函数的封装,默认接收三个参数,也可以接收两个参数 ,接收两个参数的情况有两种
- 元素+属性
- 元素+孩子
- 首先判断函数传入的参数长度,如果为三个参数
- 判断第三个参数的类型,如果是单个孩子节点,则封装成数组(孩子只划分为两种:字符串或者孩子节点数组)
- 若是两个参数
- 判断第二个参数是否为数组
- 若是,则说明是孩子数组,完善createVNode的参数即可
- 若不是,则需要判断这个参数是孩子节点还是属性对象
export function h(type,propsOrChildren,children){
// h方法 如果参数为两个的情况 1) 元素 + 属性 2) 元素 + 儿子
const l = arguments.length;
if(l === 2){
// 如果propsOrChildren是对象的话 可能是属性 可能是儿子节点
if(isObject(propsOrChildren) && !isArray(propsOrChildren)){ // h(type,属性或者元素对象)
// 要么是元素对象 要么是属性
if(isVnode(propsOrChildren)){ // h(type,元素对象)
return createVNode(type,null,[propsOrChildren])
}
return createVNode(type,propsOrChildren) // h(type,属性)
}else{
// 属性 + 儿子的情况 儿子是数组 或者 字符
return createVNode(type,null,propsOrChildren) // h(type,[] ) h(type,'文本‘)
}
}else{
if(l === 3 && isVnode(children)){ // h(type,属性,儿子)
children = [children]
}else if(l > 3){
children = Array.from(arguments).slice(2)// h(type,属性,儿子数组)
}
return createVNode(type,propsOrChildren,children)
// l > 3
}
}