Vue3 流程学习

567 阅读6分钟

数据响应式演变

现在要实现如下功能。

b的值依赖于a的值,当a变化之后要重新获取b的值。

于是我们写下了下面的代码。

let a = 10
let b = a + 10  // b 的值依赖于 a 的值

console.log(b)

a = 20
b = a + 10

console.log(b)

我们发现在a更新完成之后,我们要重新给b进行赋值才可以拿到最近的b的值。于是我们再进行封装了。

let a = 10

let b = a + 10

console.log(b)

function update() {
  b = a + 10
}

a = 20
// 现在每当我们改完 a  的值之后,就调用一下 update()方法。就可以得到最新的 b 的值了。
update()
console.log(b)

现在我们想想能不能有个操作当a变化之后可以自动的执行某一段函数。

// 用来临时存放依赖信息
let currentEffect;

// 新建一个依赖的类
class Dep {
  constructor(val) {
    // 存放依赖的地方
    this.effects = new Set();
    this._val = val
  }

  get value() {
    return this._val
  }

  set value(newVal) {
    this._val = newVal
  }

  // 添加依赖
  depend() {
    // 当前有依赖信息,再执执行添加操作。
    if (currentEffect) {
      this.effects.add(currentEffect);
    }
  }

  // 触发更新
  notice() {
    // 循环遍历依赖信息
    this.effects.forEach(effect => {
      effect()
    });
  }
}

function effectWatch(effect) {
  currentEffect = effect;
  // 默认执行一次
  effect()
  dep.depend()
  currentEffect = null
}

const dep = new Dep(10)

let b

effectWatch(()=>{
  b = dep.value + 10
  console.log(b)
})

// 值发生变化,触发更新
dep.value = 100
// 调用一下就可以收集依赖信息
dep.notice()

基于上面这代码,我们可以实现对一个对象的属性进行监测。

感受Vue3实现的响应式效果

// 好了现在我们来感受一下 vue3的响应式。

import {ref, effect} from '@vue/reactivity'

// ref 是产生一个响应式对象
// effect 是当这个对象变化之后的副作用
let num = ref(10)

// 这个副作用会默认执行一次
effect(()=>{
  console.log(num.value)
})

num.value = 100

// 好了,基于以上的代码。我们想一想
// 依赖是什么?
// 副作用又是什么?

然后再基于Proxy实现对一个对象的拦截。代码如下

reactivity.js

let currentEffect;

// 新建一个依赖的类
class Dep {
  constructor() {
    // 存放依赖的地方
    this.effects = new Set();
  }
  // 添加依赖
  depend() {
    // 当前有依赖信息,再执执行添加操作。
    if (currentEffect) {
      this.effects.add(currentEffect);
    }
  }

  // 触发更新
  notice() {
    // 循环遍历依赖信息
    this.effects.forEach((effect) => {
      effect();
    });
  }
}
// Map(1) { { val: 100 } => Map(1) { 'val' => Dep { effects: [Set] } } }
const targetMap = new Map();

/**
 *
 * @param {依赖的对象} target
 * @param {对象的key} key
 */
function getDep(target, key) {
  // 我们知道要给对象的每个属性都进行依赖收集
  // 先存一下不同对象的映射,然后存对象下面 key 映射的 dep
  // 既每一个 key ---> 对应一个 dep
  // 存每个对象的映射
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 存key  的映射
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}

// 创建一个响应式对象
export function reactive(raw) {
  return new Proxy(raw, {
    // 获取对象的时候再添加依赖(有点动态的意思)
    // 添加依赖的时机,一定是有地方读取了响应式对象的属性。(没有读,就不会添加依赖)
    get(target, key) {
      let dep = getDep(target, key);
      // 收集依赖
      dep.depend();
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      // 获取到依赖 触发更新
      let dep = getDep(target, key);
      // 先设置值,后触发依赖
      let result = Reflect.set(target, key, value);
      dep.notice();
      return result;
    },
  });
}

export function effectWatch(effect) {
  currentEffect = effect;
  // 默认执行一次
  effect();
  currentEffect = null;
}

// 收集的依赖一定是
// ref reactive 包装过的变量才收集依赖。
let obj = reactive({ 
  val: 100,
});

let double

effectWatch(() => {
  double = obj.val
  // console.log(double)
});

obj.val = 999;

现在我们就实现了vue3中的 reactiveeffect,假设我们在数据变化了之后还需要视图进行更新。下面我们模仿一下vue3中的创建组件,并把组件挂载到真实dom中的流程

初渲染

index.js

import { effectWatch } from "./reactivity.js";
import { diff, mountElement } from "./renderer.js";

// 把根组件 mount到那个节点上面。
export function createApp(rootComponent) {
  return {
    mount(rootContainer) {
      const context = rootComponent.setup();
      // 标记是否是初次渲染。
      let isMounted = false;
      let prevVnode

      // 数据变了之后重新 render
      effectWatch(() => {
        if (!isMounted) {
          rootContainer.innerHTML = "";
          // let element = rootComponent.render(context);
          // document.body.append(element)
          let vnode = rootComponent.render(context);
          // mount 的时候给虚拟节点添加了 el 属性。挂载了真实的节点。
          mountElement(vnode, rootContainer);
          prevVnode = vnode
          isMounted = true
        }
        // 更新逻辑
        else {
          console.log('update')
          let vnode = rootComponent.render(context)
          // 老节点与新节点进行对比。
          diff(prevVnode, vnode)
          prevVnode = vnode
        }
      });
    },
  };
}

h.js

export function h(tag, props, children) {
  return {
    tag,
    props,
    children
  }
}

简易diff

两个虚拟节点对比的时候分以下几种情况

先比较tag

  1. tag 不相同的时候,直接用新的节点替换老的节点

tag 相同

  1. 老的没有孩子,新的有孩子。用新的append到老的dom里面
  2. 新的没有孩子,老的有孩子。删除老的dom中的节点
  3. 两个子节点都是string ,用新的内容替换掉老的节点内容。
  4. 两个都有孩子(这个是核心)

renderer.js

/**
 * 把vnode 转换成真实节点,并挂载到容器上。
 * @param {*} vnode
 * @param {*} container
 */
export function mountElement(vnode, container) {
  let { tag, props, children } = vnode;
  // 处理 tag
  let element = document.createElement(tag);

  // 处理 props
  if (props) {
    for (let key in props) {
      // 给元素设置属性
      element.setAttribute(key, props[key]);
    }
  }
  // 检测一下 children 的类型
  if (Array.isArray(children)) {
    // 遍历子节点
    children.forEach((v) => {
      // 递归 添加儿子
      mountElement(v, element);
    });
  } else {
    element.innerText = children;
  }
  container.appendChild(element);
  vnode.el = element;
}

/**
 *
 * @param {oldVnode} n1
 * @param {newVnode} n2
 */
export function diff(n1, n2) {
  console.log(n1, n2);
  // tag
  // 看两个虚拟节点的 tag 是否相同
  if (n1.tag !== n2.tag) {
    // 直接用新的节点替换掉老的节点
    n1.el.replaceWith(document.createElement(n2.tag));
  } else {
    // 在每次 diff 之前先把老的虚拟节点的真实节点放在 新的虚拟节点下面
    n2.el = n1.el;
    //props
    const { props: newProps } = n2;
    const { props: oldProps } = n1;
    // new {id: 'xxx', 'data-xxx':'xxx', data-b: 100}
    // old {id: 'xxx', 'data-xxx':'xxx'}
    // 1. 新节点的 props 比老节点的 props 多  用新的替换老的
    if (newProps && oldProps) {
      Object.keys(newProps).forEach((key) => {
        const newVal = newProps[key];
        const oldVal = oldProps[key];
        if (newVal !== oldVal) {
          // 用新的替换老的
          n1.el.setAttribute(key, newVal);
        }
      });
    }
    // 2. 新节点的 props 比老节点的 props 少
    if (oldProps && newProps) {
      Object.keys(oldProps).forEach((key) => {
        // 如果新的props 里面没有老的 key, 就从老的节点中删除这个属性。
        if (!newProps[key]) {
          n1.el.removeAttribute(key);
        }
      });
    }

    // children
    const { children: oldChildren } = n1;
    const { children: newChildren } = n2;

    if (typeof newChildren === "string") {
      // 1. 两个都是 字符串
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          n1.el.textContent = newChildren;
        }
        // 2. 新节点是字符串, 老节点是数组
        else if (Array.isArray(oldChildren)) {
          n1.el.textContent = newChildren;
        }
      }
    } else if (Array.isArray(newChildren)) {
      // 新的是数组, 老的是字符串
      if (typeof oldChildren === "string") {
        n1.el.innerText = "";
        // 遍历新的孩子加入到原来的节点中
        mountElement(n2, n1.el);
      }
      // 两个都是数组
      // [1,2,3,4]
      // [1,2,3]
      else if (Array.isArray(oldChildren)) {
        // 取一个公共长度
        let length = Math.min(newChildren.length, oldChildren.length);

        // 处理公共的 length
        for (let index = 0; index < length; index++) {
          const newVal = newChildren[index];
          const oldVal = oldChildren[index];
          // 再对比公共的 length
          diff(oldVal, newVal);
        }

        // 如果新的长度大于公共长度 , 在老节点里面追加元素
        if (newChildren.length > length) {
          for (let i = length; i < newChildren.length; i++) {
            const newVnode = newChildren[i]
            mountElement(newVnode);
          }
        }
        // 如果新的长度小于公共长度 ,删除老节点里面的元素
        if (newChildren.length < length) {
          for(let i = length; i < oldChildren.length; i++) {
            const oldVnode = oldChildren[i]
            // 从老的里面删除老的
            oldVnode.el.parent.removeChild(oldVnode.el);
          }
        }
      }
    }
  }
}

组件

App.js

import { h } from './core/h.js';
import {reactive} from './core/reactivity.js'
export default {
  // 根据数据创建出最新的视图节点
  // 这里先不考虑 vnode 和 patch
  render(context) {
    return h(
      "div",
      {
        class: "class----"+String(context.state.count),
      },
      [h("p", null, "我是第一个儿子"+ context.state.count), h("p", null, "我是第二个儿子")]
    );
  },
  setup() {
    const state = reactive({
      count: 0,
    });
    window.state = state;
    return {
      state,
    };
  },
};

代码入口

index.js

import { createApp } from "./core/index.js";
import App from './App.js'

const app = createApp(App)
.mount(document.querySelector('#app'))

这样我们就可以实现了一个简易的Vue数据响应式,以及 render函数到页面的渲染过程。

还需要更进一步的了解的就是 在template如转换成render函数。

以及通过render函数如何生成的vnode

diff中当两个vnode的子节点都是数组的情况下,里面的diff流程。

还有根组件与子组件之间的渲染流程。

代码目录

image.png

{
  "name": "up-mini-vue",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "dependencies": {
    "@vue/reactivity": "^3.2.23"
  }
}

根据以上的代码,可以先感受一下vue内部为我们不手动操作dom背后所做的事情吧。