实现mini-vue3

Posted _阿锋丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现mini-vue3相关的知识,希望对你有一定的参考价值。

点击下面链接看视频

视频地址

初始化项目

yarn init -y

vue3源码采用的是monorerpo的管理方式,我们这就简单点的方式

创建包

集成typescript

注意如果没有安装typescript需要先安装typescript

npx tsc --init

集成jest

yarn add jest @types/jest --dev

注意安装之后还是不能识别spec.ts文件

需要在ts.config文件中配置

配置测试脚本命令

配置ts.config

解决jest不兼容esmodule的问题

配置一下babel

https://jestjs.io/docs/getting-started

安装插件jest

实现收集依赖

先创建effect.spec.js

describe('effect', () => 
  it("enter", () => 
    const af = reactive(
      age: 1
    )

    /**
     * 所谓的收集依赖就是将effect下面的回调函数fn get的时候先放在一个容器中
     * set的时候在执行所有被收集起来的fn
     */
    let newAge
    effect(() => 
      newAge =af.age + 1
    )
    expect(newAge).toBe(2)

    af.age++
    expect(newAge).toBe(3)
  )


)

配置ts.config使其兼容es6

reactive.spec.ts

import  reactive  from '../reactive'

describe("reactive", () => 
  it("enter", () => 
    const obj =  a: 1 
    const proxyObj = reactive(obj)

    expect(proxyObj).not.toBe(obj)
    expect(proxyObj.a).toBe(1)
  )
)

下面代码完成上面测试

reactive.ts

export function reactive(raw) 
  return new Proxy(raw, 
    get(target, key) 
      const res = Reflect.get(target, key)
      // 依赖收集
      return res
    ,
    set(target, key, value) 
      const res = Reflect.set(target, key, value)
      return res
    
  )

effect.ts

let activeEffect
class ReactiveEffect 
  fn: any
  constructor(fn) 
    this.fn = fn
  

  run() 
    activeEffect = this
    this.fn()
  


/**
 * 依赖收集 收集的就是ReactiveEffect的实例
 * 
 */

export function effect(fn) 
  // fn
  const _effect = new ReactiveEffect(fn)

  _effect.run()


// target->key->dep
//收集依赖
let targetMap = new Map()
export function track(target, key) 
  let depsMap = targetMap.get(target)
  if (!depsMap) 
    depsMap = new Map()
    targetMap.set(target, depsMap)
  


  let dep = depsMap.get(key)
  if (!dep) 
    dep = new Set()
    depsMap.set(key, dep)
  

  dep.add(activeEffect)


//触发set 更新
export function trigger(target, key) 
  let depsMap = targetMap.get(target)
  let dep = depsMap.get(key)

  for (let effect of dep) 
    effect.run()
  

reactive.ts 添加量函数

完成和effect相关的功能

runner

一句话概括这个功能:effect函数会有一个返回一个可执行函数,并且这个函数返回值是fn return出来的值

测试用例

  it("runner", () => 
    let a = 1
 
    const runner = effect(() => 
      a++
      return 'a'
    )

    expect(a).toBe(2)
    const r = runner()
    expect(a).toBe(3)
    expect(r).toBe('a')
  )

在effect 函数中return

在run方法中return

scheduler

scheduler 就是 effect能传入的第二个参数,而且这个参数应该是个函数 一开始不会被调用

等响应式对象再次更新的时候 会发现 scheduler会被调用 而第一个参数的回调函数不会再被调用了

测试用例

  it("scheduler", () => 
    let dummy
    let run: any
    const scheduler = jest.fn(() => 
      run = runner
    )
    const obj = reactive( foo: 1 )
    const runner = effect(
      () => 
        dummy = obj.foo
      ,
       scheduler 
    )
    // scheduler 就是 effect能传入的第二个参数,而且这个参数应该是个函数 一开始不会被调用
    expect(scheduler).not.toHaveBeenCalled()
    expect(dummy).toBe(1)
    // 等响应式对象再次更新的时候 会发现 scheduler会被调用  而第一个参数的回调函数不会再被调用了
    obj.foo++
    expect(scheduler).toHaveBeenCalledTimes(1)
    expect(dummy).toBe(1)

    // 调用runner
    run()
    expect(dummy).toBe(2)
  )

stop

调用stop方法的runner函数 会有一次让响应式更新失效的情况

  it("stop",()=>
    let dummy;
    const obj = reactive(props:1)

    const runner = effect(()=>
      dummy = obj.prop
    )

    obj.prop = 2
    expect(dummy).toBe(2)
    stop(runner)
    obj.prop = 3
      //可以看到没有立即更新为3 因为上面的runner调用了stop方法
    expect(dummy).toBe(2)
    
    runner()
    expect(dummy).toBe(3)
  )

思路:让当前key 对应的dep 里面的依赖 被删除

第三集:优化下stop,完成readonly相关功能

优化stop功能

obj.prop++ 测试在以前的stop测试不会通过,原因就是obj.prop++ 的等价操作就是 obj.prop = obj.prop +1。 其中在获取obj.prop的时候会再一次触发get收集依赖 ,所以上面的stop删除就相当于白删除了

配置launch.json


  "version": "0.2.0",
  "configurations": [
      
          // 调试名称
          "name": "Jest",
          "type": "node",
          // 启动类型 分为launch(启动) 和 attach(附加)两种
          "request": "launch", 
          // 设置运行时可执行文件路径,默认是node可以是其他的执行程序,如npm、yarn
          "runtimeExecutable": "yarn", 
          // 传递给程序的参数
          "args": [
              "jest",
          ], 
          // 指定程序启动调试的目录
          "cwd": "$workspaceRoot", 
          "sourceMaps": false,
          // 如果设置为std,则进程stdout / stderr的输出将显示在调试控制台中,而不是侦听调试端口上的输出
          "outputCapture": "std",
      ,
  ]

解决

readonly

创建文件readonly.spec.ts

import  readonly  from '../reactive'

describe("", () => 
  it("happy path", () => 
    /**
     * 只读属性
     */
    const original =  foo: 1, bar:  a: 2  
    const wrapped = readonly(original)
    expect(wrapped).not.toBe(original)
    expect(wrapped.foo).toBe(1)
  )

    //当改变属性 触发set时发出警告
  it('warn', () => 

    console.warn = jest.fn();

    const user = readonly(
      age: 10
    )

    user.age = 11
    expect(console.warn).toBeCalled()
  )
)

创建一个baseHandler.ts文件 ,对reactive.ts里面的代码进行重构,

import  track, trigger  from './effect'

/**
 * 为节约内存,所以全局环境下存储高阶函数的返回值
 */
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)


function createGetter(isReadonly = false) 
  return function get(target, key) 
    const res = Reflect.get(target, key)
    // 依赖收集
    if (!isReadonly) 
      track(target, key)
    

    return res
  



function createSetter() 
  return function set(target, key, value) 
    const res = Reflect.set(target, key, value)
    trigger(target, key)
    return res
  


export const mutableHandlers = 

  get: get,
  set: set



export const readonlyHandlers = 
  get: readonlyGet,
  set(target, key, value) 
    console.warn('只读');
    return true
  



reactive.ts

import  track, trigger  from './effect'
import  mutableHandlers, readonlyHandlers  from './baseHandlers'





export function reactive(raw) 
  return createActiveObject(raw, mutableHandlers)


export function readonly(raw) 
  return createActiveObject(raw, readonlyHandlers)



function createActiveObject(raw, baseHandlers) 
  return new Proxy(raw, baseHandlers)

实现isReactive和isReadonly

测试用例

解决

深层reactive

  it("recursion reactives", () => 
    const original = 
      nested: 
        foo: 1
      ,
      array: [ bar: 2 ]
    

    const observed = reactive(original)
    expect(isReactive(observed.nested)).toBe(true)
    expect(isReactive(observed.array)).toBe(true)
    expect(isReactive(observed.array[0])).toBe(true)
  )

shallowReadonly

shallowReadonly.spec.ts

import  isReadonly, shallowReadonly  from '../reactive';


describe("shallowReadonly", () => 
  it("no recursion", () => 
    // reactive值作用于对象的表层
    const props = shallowReadonly( n:  foo: 1  )
    expect(isReadonly(props)).toBe(true)
    expect(isReadonly(props.n)).toBe(false)
  )



  it('warn', () => 

    console.warn = jest.fn();

    const user = shallowReadonly(
      age: 10
    )

    user.age = 11
    expect(console.warn).toBeCalled()
  )
)




reactive.ts

baseHandlers.ts

isProxy

reactive.ts

ref

import  effect  from '../effect'
import  ref  from '../ref'

describe("ref", () => 

  it("ref init", () => 
    const a = ref(1)
    expect(a.value).toBe(1)
  )

  it("be reactive", () => 
    const a = ref(1)
    let dummy;
    let calls = 0
    effect(() => 
      calls++
      dummy = a.value
    )

    expect(calls).toBe(1)
    expect(dummy).toBe(1)

    a.value = 2

    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // same value should not trigger

    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)


  )


  it("should be recursion", () => 
    const a = ref(
      count: 1,
    )

    let dummy
    effect(() => 
      dummy = a.value.count
    )
    expect(dummy).toBe(1)
    a.value.count = 2
    expect(dummy).toBe(2)

  )
)

ref.ts

import  isObjet  from '../utils'
import  isTracking, trackEffects, triggerEffects  from './effect'
import  reactive  from './reactive'


class RefImpl 
  private _value: any
  // ref和reactive的区别就是ref只有一个value,所以dep只需单独一个,不需要reactive的复杂对应关系
  public dep
  private _rawValue: any
  constructor(value) 
    // _rawValue的声明 是因为害怕this._value进行reactive后变成proxy
    // 从而不方便下面set的比较
    this._rawValue = value
    this._value = isObjet(value) ? reactive(value) : value

    this.dep = new Set()
  

  get value() 
    if (isTracking()) 
      trackEffects(this.dep)
    

    return this._value
  

  set value(newValue) 
    if (!Object.is(newValue, this._rawValue)) 
      this._rawValue = newValue
      // 注意:是先修改value的值然后进行trigger
      this._value = isObjet(newValue) ? reactive(newValue) : newValue
      triggerEffects(this.dep)
    



  


export function ref(value) 
  return new RefImpl(value)

以上是关于实现mini-vue3的主要内容,如果未能解决你的问题,请参考以下文章

实现mini-vue3

实现mini-vue3-更新2集含视频

青训营Pro 前端框架设计理念 - Vue3动机 - 手写实现mini-vue

青训营Pro 前端框架设计理念 - Vue3动机 - 手写实现mini-vue

Vue3源码快速学习

记录学习vue-next源码 - reactivity