scheduler调度器
从我们的单侧入手
// 44 实现effect的scheduler功能
it('scheduler',()=>{
// 通过effect的第二个参数给定的一个schecdule的 fn
// effect 第一次执行的时候还会执行fn
// 当响应式对象set update 不会执行fn 而是执行scheduler
// 如果说当执行runner的时候 会再次执行fn
let dummy;
let run:any;
const scheduler = jest.fn(()=>{
run = runner
}
);
const obj = reactive({foo:1});
const runner = effect(()=>{
dummy = obj.foo
},{ scheduler }
);
expect(scheduler).not.toHaveBeenCalled();
expect(dummy.toBe(1));
// should be called on first trigger
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
// should not run yet
expect(dummy).toBe(1);
// manually run
run();
// should have run
expect(dummy).toBe(2)
// 48- 基本逻辑完成 执行单元测试 结果通过
}); // 44 实现effect的scheduler功能
在我们的单侧的effect中除了传递了我们的参数fn,还另外传递了我们的options也就是{schduler},schduler也是接受一个函数.在我们的断言中断言了一开始我们的schduler不会被调用,但是当我们的响应式对象发生改变时也就是foo++了,我们的schduler才会被调用,并且我们的run函数并没有被调用,只有当我们再次手动调用run函数时我们响应式对象的值才会发生变化
// 45- 接受第二个参数
const extend = Object.assign;
export function effect(fn,options:any = {}){
// 因为是触发响应,所以接受的是一个函数fn 并且需要先调用一次
// const scheduler = options.scheduler
// 18 创建上面类的一个实例,并将fn传入进去
const _effect = new ReactiveEffect(fn,options.scheduler) // 47 将 scheduler传入到构造函数中去,并接受他
extend(_effect,options)
// 19-我们希望可以通过该实例调用上面类的方法时来间接调用fn函数
_effect.run();
// 41- 结回effect中的调用 在这里就相当于是调用了实例的方法.我们可以返回该函数 在通过一些处理
const runner:any = _effect.run.bind(_effect)
runner.effect=_effect
return runner
}
export class ReactiveEffect{
private _fn:any;
public scheduler:Function | undefined
deps = [];
active = true;
onStop?: ()=> void; // 可有可无
// 21-我们可以通过内部的构造函数来替换一个等价的fn 下面调用得也就换成了this._fn
constructor(fn,scheduler?:Function){ // 加上public使其能够被外界获取到
this._fn = fn
this.scheduler = scheduler
}
.................}
export function trigger(target,key){
// 37-基于target,key 找到所有的dep并调用其中的fn
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
triggerEffects(dep)
// 39-运行yarn test执行所有的单元测试通过
};
// 104 抽离ref中用到的代码进行封装导出
export function triggerEffects(dep){
for (const effect of dep) {
if(effect.scheduler){
effect.scheduler()
}else{
effect.run()
}
}
}
计算属性computed
同样由我们的单侧入手
// 126 computed计算属性的断言
describe('computed',()=>{
// 计算属性具有缓存功能
it('happy path',()=>{
const user = reactive({
age:1
})
// 接受一个函数
const age = computed(()=>{
return user.age
})
expect(age.value).toBe(1)
})
// 第二个单侧
it('should comput lazily',()=>{
const value = reactive({
foo:1
})
const getter = jest.fn(()=>{
return value.foo
})
const cValue = computed(getter)
// 130 lazy 计算属性的懒执行 不会重复调用
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(1)
// should not compute again
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
// should not compute untill need 当给定的值发生变化时还是执行一次
value.foo = 2 // set逻辑会触发我们的trigger 会重新执行我们的getter
expect(getter).toBeCalledTimes(1)
// now it should compute
expect(cValue.value).toBe(2)
expect(getter).toHaveBeenCalledTimes(2)
// should not compute again
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
})
来到我们逻辑代码的实现,主要是实现了我们计算属性的缓存功能,只有当我们的响应式对象的值发生改变时我们的计算属性的getter才会再次被调用,这里我们引入了一个dirty的标识,初始值为true,当首次进入我们的getter时才会进行触发,并且把我们的dirty值改变,类似于上锁.当我们响应式对象的值发生改变时需要将dirty进行解锁,但是我们又不想再次去触发我们的getter,这个时候就再次引入了我们上面实现的scheduler功能,将我们dirty的解锁放在了我们的scheduler中,就不会再去执行我们的run方法了
// 引入 ReactiveEffect
class ComputedRefImp{
private _dirty: boolean = true
private _getter: any
private _value:any
private _effect:any
constructor(getter){
this._getter = getter
// 133利用ReactiveEffect中触发的schduler避免去执行我们的run
this._effect = new ReactiveEffect(getter,()=>{
if(!this._dirty){
this._dirty = true
}
})
}
//129 触发我们的get方法将computed包裹的方法进行返回
get value(){
// 131 当我们调用完一次get后 将这个状态锁住 不再调用我们的方法 只把相应的值给返回出去
// 132 当我们依赖的响应式对象的值发生改变时应该去改变我们的dirty
// 去追踪我们的响应式对象的值发生变化时,我们可以利用effect中的ReactiveEffect
if(this._dirty){
// 初次调用才会进来
this._dirty = false
return this._value = this._effect().run()
// 这里的调用可以用我们的effect来替换
}
return this._value
}
}
// 127 定义导出computed函数
export function computed(getter){
// 128 我们可以返回一个类的实例来实现,因为类中可以触发相应的方法
return new ComputedRefImp(getter)
}
总结:我们的计算属性的内部其实是有一个getter,以及一个getValue;当用户去调用get value时就会去调用我们的effect.run()把我们用户传过来的fn的值给他传出去;那么缓存的能力其实是借由我们的dirty变量来实现的,当我们首次进入时,将value值进行存储然后正常的触发依赖,但是我们的dirty值发生了改变,也就是被上锁了,所以当我们再次触发get时只会讲我们存储的value值进行返回,并不会去执行我们的触发依赖;但是当我们响应式对象发生改变时就再次去触发我们的trigger,从而再去触发我们的scheduler,并且改变dirty的值,也就是开锁;自此我们完整的计算属性的功能完成
nextTick功能
为什么需要实现我们的nextTick功能,我们知道在vue中数据是响应式的,那如果有一个数据在一段循环中持续不断的改变,我们的视图是不是也会随着一起不断发生改变;这样对我们的性能损耗显然是不合理的,这也就是为什么我们的nextTick是属于我们的微任务
先描述我们的程序设计,我们会引入一个队列,当我们的同步任务执行时会向队列中添加一个job,在所有的同步任务执行完毕后再在我们的微任务中取出队列中的job去执行;并且如果我们的队列中已经存在了相同的job就不会重复添加,那么在代码中如何控制我们的更新逻辑把它添加到我们的队列当中呢,这里再次引用了我们实现的scheduler的功能
function setupRenderEffect(instance:any,initialvnode,container,anchor){
instance.update = effect(()=>{
if(!instance.isMounted){
console.log('init')
const {proxy} = instance
const subTree = instance.subTree = instance.render.call(proxy);
// console.log(subTree)
// vndoeTree => patch
// vnode => element =>mountElement
patch(null,subTree,container,instance,anchor)
// 我们这里的subTree就是我们的根节点,我们所要赋值的el可以在subTree上找到
// 传入我们的虚拟节点
initialvnode.el = subTree.el
instance.isMounted = true
}else{
console.log('update')
// 需要一个更新之后的vnode
const {next,vnode} = instance
if(next){
next.el = vnode.el
updateComponentPreRender(instance,next)
}
const {proxy} = instance
const subTree = instance.render.call(proxy);
const prevSubTree = instance.subTree
instance.subTree = subTree
// console.log('current',subTree)
// console.log('pre',prevSubTree)
patch(prevSubTree,subTree,container,instance,anchor)
}
},{
scheduler(){
console.log('update -- scheduler')
queueJobs(instance.update)
}
})
}
实现我们的queueJobs及nextTick逻辑
const queue:any[] = []
// 引入一个开关
let isFlushPending = false
let p = Promise.resolve()
export function nextTick(fn){
return fn? p.then(fn) : p
}
export function queueJobs(job){
if(queue.includes(job)){
}else{
queue.push(job)
}
queueFlush()
}
function queueFlush(){
if(isFlushPending) return
isFlushPending = true
nextTick(flushJobs)
// Promise.resolve().then(()=>{
// 这段代码可以利用上面的nextTick来执行 将下面的代码进行抽离
// })
}
function flushJobs(){
isFlushPending = false // 重置开关
let job
while (job = queue.shift()) {
job && job()
}
}
有限状态机
利用我们的有限状态机原理模拟我们的正则表达式的检测
// 利用有限状态机来模拟正则表达式
// /abc/.test('')
function test(string){
let startIndex
let endIndex
let i
let result = []
function waitForA(char){
if(char === 'a'){
startIndex = i
return waitForB
}
return waitForA
}
function waitForB(char){
if(char === 'b'){
return waitForC
}
i = i-1
return waitForA
}
function waitForC(char){
if(char === 'c'){
endIndex = i
return end
}
return waitForA
}
function end(){
return end
}
let currentState = waitForA;
for ( i = 0; i < string.length; i++) {
let nextState = currentState(string[i])
currentState = nextState
// 判断是否为结束状态
if(currentState === end){
console.log(startIndex,endIndex)
result.push({
start:startIndex,
end:endIndex
})
console.log(result)
currentState = waitForA
}
}
console.log(result)
}
console.log(test('ccxaabcrhertgabcppoancabc'))