阅读 141

浅析vue3原理 (持续更新)

手写vue3

在线地址:github.com/xuxiaozhou/…

Vue3目录介绍

  • packages
    • reactivity: 响应式
    • runtime-core: 运行时核心
    • runtime-dom: DOM环境的运行
    • shared: 公共函数
  • scripts: 脚本
  • rollup.config.js: rollup编译文件
  • tsconfig.json:

包的关系

graph RL
reactivity-->runtime-core --> runtime-dom --> Vue
compiler-core --> compiler-dom --> Vue

reactivity

响应式api

api作用dep
reactive响应式深层
readonly只读深层
shallowReactive响应式浅层
shallowReadonly只读浅层
graph RL
reactive--> proxy
proxy --> get --> track
proxy --> set --> trigger

API

  • 通过高阶函数,只需要传入深浅层和对应的proxy的get,set处理即可实现4个方法
export function reactive<T = object>(obj: T) {
  return createReactiveObject(obj, false, reactiveHandler);
}

// 对象的任何层都只读
export function readonly<T = object>(obj: T) {
  return createReactiveObject(obj, true, readonlyHandler);
}

// 对象的浅层响应式,深层不响应
export function shallowReactive(obj) {
  return createReactiveObject(obj, false, shallowReactiveHandler);
}

// 对象的浅层只读,深度只读
export function shallowReadonly(obj) {
  return createReactiveObject(obj, true, shallowReadonlyHandler);
}
复制代码

createReactiveObject

  • 都是采用proxy代理响应式
  • 只能代理对象
  • 通过reactiveMap和readonlyMap的防止重复代理
  • 使用weakMap的作用可以在target被销毁是,weakMap能够同时垃圾回收
// 缓存已经处理过的对象:在target销毁时能被垃圾回收
const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();

function createReactiveObject(target, isReadonly, baseHandlers) {
  // 只能处理是对象类型
  if (!isObject(target)) {
    return target;
  }

  const map = isReadonly ? readonlyMap : reactiveMap;

  if (map.has(target)) {
    // 返回已经缓存
    return map.get(target);
  }

  const proxy = new Proxy(target, baseHandlers);
  map.set(target, proxy);

  return proxy;
}
复制代码

handlers

  • 主要是实现4个api不同的get和set
  • 封装get和set高阶函数,可以传入不同参数
export const reactiveHandler = {
  get: createGet(),
  set: createSet(true)
};

export const shallowReactiveHandler = {
  get: createGet(false, true),
  set: createSet(true)
};

// readonly的设置都返回报错
export const readonlyHandler = {
  get: createGet(true),
  set: () => {
    console.log('error');
  }
};
export const shallowReadonlyHandler = {
  get: createGet(true, true),
  set: () => {
    console.log('error');
  }
};
复制代码

createGet

  • 创建handler的get
  • track的作用是此对象和此属性收集依赖
/**
 * createGet
 * @param isReadonly 是否只读
 * @param shallow 是否浅层
 * @returns function
 */
function createGet(isReadonly = false, shallow = false) {
  return function (target, key, receiver) {
    // 类比 target[key]
    const result = Reflect.get(target, key, receiver);

    if (!isReadonly) {
      // 非只读,需要收集依赖
      track(target, key, TrackOptType.GET);
    }

    if (shallow) {
      // 浅层的就结束
      return result;
    }

    // 如果是对象,且非浅层的,还是继续递归调用readonly和reactive
    if (isObject(result)) {
      return isReadonly ? readonly(result) : reactive(result);
    }

    // 非对象就直接返回
    return result;
  };
}
复制代码

createSet

  • 设置会判断操作类型:新增或修改
    • 新增:
      • 对象:设置的key不是对象本身已有的属性
      • 数组:key为索引,且索引值大于数组本身长度
    • 修改:新旧值不一样则是修改
  • trigger:通知依赖触发更新操作
/**
 * 创建set
 * @param shallow 是否浅层
 * @returns 
 */
function createSet(shallow = false) {
  return function (target, key, value, receiver) {
    // 获取原来值
    const oldValue = Reflect.get(target, key);

    // 设置后会返回一个是否成功
    const result = Reflect.set(target, key, value, receiver);

    /**
     * 新增
     *    数组:并且设置索引,并且设置的索引比当前数组的长度还长
     *    对象:设置的key不是对象本身已有的属性
     * 修改
     *    新旧值不一样则是修改
     */
    const hasKey = isArray(target) && isInteger(key)
      ? Number(key) < target.length - 1
      : hasOwn(target, key);

    if (!hasKey) {
      // 通知:新增操作
      trigger(target, TriggerOpt.ADD, key, value);
    } else if (hasChanged(oldValue, value)) {
      // 通知:修改操作
      trigger(target, TriggerOpt.SET, key, value, oldValue);
    }

    // 返回修改结果
    return result;
  };
}
复制代码

了解effect

在了解trigger和track之前,先了解effect

其作用:默认会执行一次,在effect执行函数用到的响应式变量(reactive,readonly,shallowReactive,shallowReadonly,ref,shallowRef包装过)会被收集依赖,当这边变量发生变化是会再次执行此函数

const state = reactive({ hello: 'world', other: 1 })

// 默认会执行此函数fn
effect(()=>{  // fn
  // 使用累reactive包装后的变量
  app.innerHTML = state.hello
})

setTimeout(() => {
  state.hello = 'vue'
  // 修改后会默认调用fn函数
}, 1000);
复制代码

effect:副作用

export function effect(fn, options: EffectOptions = {}) {
  // 创建响应式
  const effect = createReactiveEffect(fn, options);

  // 如果没有设置lazy,会立即执行一次
  if (!options.lazy) {
    effect();
  }

  return effect;
}
复制代码

createReactiveEffect:创建响应式副作用

// 存放当前的effect,方便track将target,key和对应的effect关联起来
let activeEffect;
// 存放一个栈形结构,保证activeEffect指向正确
const effectStack = [];

let uid = 0;
function createReactiveEffect(fn, options) {
  const effect = function createEffect() {
    // 保证不再重复
    if (!effectStack.includes(effect)) {
      try {
        effectStack.push(effect);
        activeEffect = effect;
      
        // 其实还是调用传进来的函数
        return fn();
      } finally {
        // 保证effect永远正确
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
      }
    }
  };

  effect.id = uid++;
  effect.__isEffect = true;
  effect.raw = fn;
  effect.options = options;

  return effect;
}
复制代码

存在effectStack的原因

effect(()=>{  // effect1
  state.name =1
  effect(()=>{  // effect2
    state.age=2
  })
  state.address=3   // 此处应该存effect1,如果没有当一个effect执行完后重制为数组最后一个,此时存的就是effect1
})
复制代码

track: 收集依赖

对象,key和effect需要关联起来

/**
 * 收集依赖,将target,key关联起来
 * @param target 对象
 * @param key  属性
 * @param type 类型
 * @returns 
 */
export function track(target, key, type) {
  // 此处的activeEffect是跟effect在同一文件。
  // 主要利用js是单线程的机制
  // 如果当前使用值不在effect里面使用是不需要收集
  if (!activeEffect) {
    return;
  }

  /***
  存储的数据结果
  Weakmap {
    [ target ]: Map {
      [ key ] : Set [ effect1, effect2 ]
    }
  }
  */
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let depMap = depsMap.get(key);
  if (!depMap) {
    depMap = new Set();
    depsMap.set(key, depMap);
  }

  const dep = depMap.has(activeEffect);
  if (!dep) {
    depMap.add(activeEffect);
  }
}
复制代码

trigger: 触发更新

  • 通过target获取此对象收集的依赖
  • 修改数组长度
    • 触发收集length的effect
    • 触发收集索引大于length的effect
  • 修改
    • 触发收集对应key的effect
  • 如果是数组,且是新增操作
    • 触发收集length的effect
/**
 * 触发更新
 * @param target 对象
 * @param type 更新类型
 * @param key 属性
 * @param newValue 新值
 * @param oldValue 旧值
 * @returns 
 */
export function trigger(target, type, key: string, newValue?, oldValue?) {
  // 此对象没被依赖收集就不需要处理
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }

  // 要执行的effect存到新的集合
  const effects = new Set();
  const add = (effectToAdd) => {
    if (effectToAdd) {
      effectToAdd.forEach(effect => {
        effects.add(effect);
      });
    }
  };

  // 修改数组的长度
  if (isArray(target) && key === 'length') {
    depsMap.forEach((dep, key) => {
      // 更改长度
      // length收集的effects要执行
      // 大于新设置长度的索引收集的effects也要执行
      if (key === 'length' || (isNumber(key) && Number(key) > newValue)) {
        add(dep);
      }
    });
  } else {
    if (key !== undefined) {
      // 修改
      add(depsMap.get(key));
    }

    switch (type) {
      case TriggerOpt.ADD:
        // 数组:新增操作,length收集的effects要执行
        if (isArray(target) && isInteger(key)) {
          add(depsMap.get('length'));
        }
    }
  }

  // 要执行的effects依次执行
  effects.forEach((effect: any) => {
    // effect不一定都是立即执行的,可能做一下其他事情,比如computed
    if (effect.options?.scheduler) {
      effect.options.scheduler(effect);
    } else {
      effect();
    }
  });
}
复制代码

ref

使用

const name = ref('hello')
const age = ref({ num: 10 })

effect(() => {
  app.innerHTML = name.value + '---' + age.value.num
})

setTimeout(() => {
  name.value = 'world'
}, 1000);

setTimeout(() => {
  age.value.num = 20
}, 2000);
复制代码

特点

  • 针对reactive等不支持对象的形式支持
  • 支持除了普通类型也是支持引用类型
  • 使用.value
  • 使用的Object.defineProperty去实现对value的响应式

ref/shallowRef

// 传进来的是普通值
export function ref(value: string | number | boolean | object) {
  return createRef(value);
}

// 传进来的是普通值
export function shallowRef(value) {
  return createRef(value, true);
}
复制代码

createRef

function createRef(value, isShallow = false) {
  return new RefImpl(value, isShallow);
}

// 如果是对象则采用reactive
const convert = val => isObject(val) ? reactive(val) : val;

class RefImpl {
  // 存放处理过的值
  public _value;
  // 存放原值
  public _rawValue;
  private isShallow;
  // 增加标识
  public _v_isRef = true;

  public constructor(value, isShallow) {
    // 如果是深度且是对象,只用reactive去处理响应式
    this._value = isShallow ? value : convert(value);
    // 保存原值
    this._rawValue = value;
    this.isShallow = isShallow;
  }

  get value() {
    // 收集此对象的 value 字段的依赖
    track(this, 'value', TrackOptType.GET);
    return this._value;
  }

  set value(newValue) {
    // 判断是否有改变
    if (hasChanged(newValue, this._rawValue)) {
      this._rawValue = newValue;
      // 如果非浅层,则判断是否是对象,如果是对象则进行调用reactive
      this._value = this.isShallow ? newValue : convert(newValue);
      
      // 触发此对象 设置 value 字段的更新
      trigger(this, TriggerOpt.SET, 'value', newValue, this._value);
    }
  }
}
复制代码

toRef

使用

将响应式对象的某个属性转成ref

const person = reactive({ name: 'allen', age: 10 })
const name = toRef(person, 'name')

effect(() => {
  app.innerHTML = name.value
})

setTimeout(() => {
  name.value = 'tom'
}, 1000)
复制代码

实现

  • 只是做一个层代理
class ObjectRefImpl {
  public _v_isRef = true;
  public target;
  public key;
  constructor(target, key) {
    this.target = target;
    this.key = key;
  }

  get value() {
    return this.target[this.key];
  }

  set value(newValue) {
    this.target[this.key] = newValue;
  }
}

// 传进来的是响应式对象
// 将一个响应式对象的属性变成ref
// 代理了对象的属性
export function toRef(target, key) {
  return new ObjectRefImpl(target, key);
}
复制代码

toRefs

// 传进来的是响应式对象
export function toRefs(target) {
  const ret = isArray(target) ? new Array(target.length) : {};
  for (const key in target) {
    ret[key] = toRef(target, key);
  }

  return ret;
}
复制代码

computed

使用

const age = ref(18)
const myAge = computed(() => {
  console.log('runner')
  return age.value + 10
})

// 第一次获取,会走函数
// runner
console.log(myAge.value)  // 28
// 这次computed依赖项没有修改,则不走函数,直接读缓存的值
console.log(myAge.value) // 28

// 修改age,computed依赖项发生改变,但不会立即执行,需要等待下次取值才会走函数
age.value = 10
// 还没更新
// {"_dirty":true,"_value":28}
console.log(JSON.stringify(myAge))  
// 因为依赖项发生改变,会重新执行
// runner
// 20
console.log(myAge.value)
复制代码

computed

  • getterOrOptions可以是函数/对象
type Getter<T = unknown> = () => T;
type GetterOption<T = unknown> = {
  get: Getter<T>;
  set: () => void;
};
type GetterOrOptions<T> = Getter<T> | GetterOption<T>;

export function computed<T = unknown>(getterOrOptions: GetterOrOptions<T>) {
  let getter;
  let setter;

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions;
    setter = () => {
      console.log('no change');
    };
  } else if (isObject(getterOrOptions)) {
    getter = (getterOrOptions as GetterOption).get;
    setter = (getterOrOptions as GetterOption).set;
  }

  return new ComputedRefImpl(getter, setter);
}
复制代码

ComputedRefImpl

  • computed基于effect
  • 通过_dirty是否使用缓存值还是重新计算
  • computed的值通过.value
  • computed本身函数默认不执行,只有获取值是去调用
  • 但computed依赖项没有发生改变时,不去走计算函数,直接使用缓存的值
  • 但依赖项发生改变,不会立即执行。下次获取获取值是才会重新计算
// effect的执行由自己控制什么时候执行
class ComputedRefImpl {
  // 需不需要重新计算的标识符
  public _dirty = true;
  // 存储最新的值
  public _value;
  public setter: any;
  public effect;

  constructor(getter, setter) {
    this.setter = setter;
    this.effect = effect(getter, {
      // 不是立即执行
      lazy: true,
      // 调度
      scheduler: () => {
        if (!this._dirty) {
          // 设置下次get的时候执行重新获取最新的值
          // 修改this._dirty,在下次获取时就可以去获取新值
          this._dirty = true;
          // 触发收集这个依赖的effect
          trigger(this, TriggerOpt.SET, 'value');
        }
      }
    });
  }

  get value() {
    if (this._dirty) {
      // 缓存起来,不是每个获取都是去走effect的
      this._value = this.effect();
      this._dirty = false;
    }
    // 收集依赖
    track(this, 'value', TrackOptType.GET);
    return this._value;
  }

  set value(newValue) {
    this.setter(newValue);
  }
}
复制代码

runtime-dom

  • 将dom操作等平台相关操作作为参数传入给createRenderer,是runtime-core是与平台无关
  • 重写mount的原因是进行一些平台相关业务操作
// 渲染用到的方法:
// 跟平台相关的api
// 创建dom,修改dom属性等
const rendererOptions = { ...nodeOps, patchProp };

export function createApp(rootComponent, rootProps = null) {
  const app: any = createRenderer(rendererOptions).createApp(rootComponent, rootProps);
  const { mount } = app;
  // 重写mount,执行一些跟平台相关的操作
  app.mount = function (container) {
    // 获取真实dom
    container = nodeOps.querySelector(container);
    // 清空
    container.innerHTML = '';
    // 才是真正的挂载
    mount(container);
  };
  return app;
}

// 导出runtime-core
export * from '@vue/runtime-core';
复制代码

runtime-core

结合runtime-dom调用

createRenderer(rendererOptions).createApp(rootComponent, rootProps);
复制代码

createRenderer

runtime-core/renderer.ts

// 创建渲染器
export function createRenderer(rendererOptions) {
  // 核心代码: 在此函数内能获取到平台相关的操作方法,减少传递
  // 。。。

  // 渲染
  const render = (vnode, container) => {
    patch(null, vnode, container);
  };

  return {
    // 调用createAppApi返回createApp,同时传递render
    createApp: createAppApi(render)
  };
}
复制代码

createAppApi

runtime-core/createAppApi

export function createAppApi(render) {
  // 根组件,根组件的props
  return function createApp(rootComponent, rootProps) {
    const app = {
      _props: rootProps,
      _component: rootComponent,
      _container: null,
      mount(container) {
        // 创建虚拟节点
        const vnode = createVNode(rootComponent, rootProps);

        // 调用render
        render(vnode, container);

        app._container = container;
      }
    };
    return app;
  };
}
复制代码

createVNode 创建VNode

// 目前只考虑元素和组件
export function createVNode(type, props, children = null) {
  // 标识:为后期patch比较提供便利
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : (isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : 0);

  const vnode = {
    // 是否是vnode
    _v_isVnode: true,

    type,
    props,
    children,

    key: props && props.key,

    // 真实的节点
    el: null,
    // 实例
    component: null,

    shapeFlag
  };

  // 根据children标识
  normalizeChildren(vnode, children);

  return vnode;
}

// 设置children的类型
function normalizeChildren(vnode, children) {
  let type = 0;
  if (children === null) { }
  else if (isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN;
  } else {
    type = ShapeFlags.TEXT_CHILDREN;
  }

  vnode.shapeFlag |= type;
}
复制代码

render 渲染

// 调用patch
patch(null, vnode, container);
复制代码

patch: 核心中的核心

挂载和更新组件/元素都走此方法

  • n1为null是代表走挂载流程
  • 否则走更新流程
  • 如果新旧VNode的类型不一样,则把旧VNode干掉,重新走挂载流程
  • 根据VNode不同类型做不同操作
/**
  * 打补丁:挂载和更新的功能
  * @param n1 旧节点
  * @param n2 新节点
  * @param container 挂载容器
  * @param anchor 当前元素的参照物
  */
const patch = (n1, n2, container, anchor = null) => {
  const { shapeFlag, type } = n2;

  // 如果有旧vnode,且不是相同类型:直接把旧节点干掉,走挂载的流程
  if (n1 && !isSameVNodeType(n1, n2)) {
    //  删除以前,换成新的
    anchor = hostNextSibling(n1);
    unmount(n1);
    n1 = null;
  }

  // 根据不同类型,做不同操作
  switch (type) {
    case TEXT:
      // 文本节点,特殊处理
      processText(n1, n2, container);
      break;
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 元素
        processElement(n1, n2, container, anchor);
      } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        // 组件
        processComponent(n1, n2, container);
      }
  }
};
复制代码

processText:处理文本节点

  • n1为null时,走挂载流程
  • n1不为null时,走更新流程
  • hostCreateText/hostInsert/hostSetText等方法则是调用createRenderer传递的参数
const processText = (n1, n2, container) => {
  if (n1 === null) {
    // 挂载
    // 新建文本节点
    n2.el = hostCreateText(n2.children);
    // 插入父容器
    hostInsert(n2.el, container);
  } else {
    // 更新:复用旧节点的元素
    const el = n2.el = n1.el;
    // 比较新旧文本节点的children是否一致
    if (n2.children !== n1.children) {
      // 如果不同只重新设置文本内容
      hostSetText(el, n2.children);
    }
  }
};
复制代码

processElement:处理Element元素

const processElement = (n1, n2, container, anchor = null) => {
  if (n1 === null) {
    // 元素挂载
    mountElement(n2, container, anchor);
  } else {
    // 元素更新
    patchElement(n1, n2, container, anchor);
  }
};
复制代码

mountElement:挂载元素

const mountElement = (vnode, container, anchor = null) => {
  const { props, shapeFlag, type, children } = vnode;

  // 创建元素
  const el = (vnode.el = hostCreateElement(type));

  // 处理属性
  if (props) {
    for (const key in props) {
      hostPatchProp(el, key, null, props[key]);
    }
  }

  // 处理元素children
  // 1. children为文本节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 文本内容
    hostSetElementText(el, children);
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 2. 元素数组
    mountChildren(children, el);
  }

  // 挂载到容器上
  hostInsert(el, container, anchor);
};
复制代码

mountChildren: 挂载子节点(文本/Element/组件节点)

递归调用patch

// 挂载子节点
const mountChildren = (children, el) => {
  for (let i = 0; i < children.length; i++) {
    // 标准化子VNode
    // 如果是字符串:则生成文本节点
    const child = normalizeVNode(children[i]);
    // 调用patch进行分别挂载
    patch(null, child, el);
  }
};
复制代码

patchElement: 处理Element更新

const patchElement = (n1, n2, container, anchor = null) => {
  // 复用dom节点
  const el = (n2.el = n1.el);

  // 更新属性
  patchProps(n1.props, n2.props, el);

  // 更新子节点
  patchChildren(n1, n2, el);
};
复制代码
文章分类
前端
文章标签