# Mini-Vue 实现思路

  • 渲染系统,改模块主要包含三个功能:
    • 功能一:h 函数,用于返回一个 VNode 对象;
    • 功能二:mount 函数,用于将 VNode 挂载到 DOM 上;
    • 功能三:patch 函数,用于对比两个 VNode 进行对比,决定如何处理新的 VNode;
  • 响应式系统:
    • 通知依赖 notify ()
    • 数据劫持 reactive ()
    • 数据结构 getDep ()
    • 添加方法 depend ()
    • 方法依赖 watchEffect ()
    • Vue2 的 defineProperty 实现
    • Vue3 的 Proxy 实现

# 渲染器实现

# h 函数与 mount 函数实现

# index

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Render</title>
</head>
<body>
  <div id="app"></div>
  <script src="./render.js"></script>
  <script>
    // 1. 通过 h 函数来创建 vnode
    const vnode1 = h('div', { id: "main" }, [
      h("h3", null, "当前计数:100"),
      h("button", { onclick: () => { console.log("+1"); } }, "+1"),
      h("button", { onclick: () => { console.log("-1"); } }, "-1")
    ]) // vdom
    mount(vnode1, document.querySelector("#app"))
  </script>
</body>
</html>

# render

const h = (tag, props, children) => {
  // 返回一个对象给 vnode
  return {
    tag,
    props,
    children
  }
}
const mount = (vnode, container) => {
  // 1. 创建出真实的原生,并且在 vnode 上保留 el
  const el = vnode.el = document.createElement(vnode.tag)
  
  // 2. 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key];
      // 判断是否是 on 开头的 API 操作
      if (key.startsWith("on")) {
        // 切割并转小写取到 click 并绑定事件
        el.addEventListener(key.slice(2).toLowerCase(),vnode.props[key])
      } else {
        // 设置属性
        el.setAttribute(key, value)
      }
    }
  }
  // 3. 处理 children
  if (vnode.children) {
    if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
      el.textContent = vnode.children
    } else {
      // 对数组进行循环
      vnode.children.forEach(item => {
        console.log('item',item); // 打印 vnode 的子元素
        // 递归调用
        mount(item,el)
      })
    }
  }
  // 4. 将 el 挂载到 container 上
  container.appendChild(el)
  return {
    vnode,
    container
  }
}

参考结果:

render_basic

# patch 函数实现

# index

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Render</title>
</head>
<body>
  <div id="app">
    <button id="newVNode">创建新的vnode</button>
  </div>
  <script src="./render.js"></script>
  <script>
    // 1. 通过 h 函数来创建 vnode
    const vnode1 = h('div', { id: "main" }, [
      h("h3", null, `当前计数:100`),
      h("button", { onclick: () => { console.log("+1"); } }, "+1"),
      h("button", { onclick: () => { console.log("-1"); } }, "-1")
    ]) // vdom
    const instanceMount = mount(vnode1, document.querySelector("#app"))
    console.log(instanceMount); // 可以拿到 mount 的返回值
    // 创建新的 vnode
    const newVNode = document.querySelector("#newVNode")
    newVNode.onclick = () => {
      const vnode2 = h('div', { id: "neko", class: 'title' }, [
        h("h3", null, `新的VNode`),
        h("span", null, "新的VNode比旧的Vnode长度短,所以执行patch函数时会将旧的VNode数组进行切割与新数组长度一样"),
      ])
      const instancePatch = patch(vnode1, vnode2)
      console.log(instancePatch); // 可以拿到 patch 的返回值
    }
  </script>
</body>
</html>

# render

const h = (tag, props, children) => {
  // 返回一个对象给 vnode
  return {
    tag,
    props,
    children
  }
}
const mount = (vnode, container) => {
  // 1. 创建出真实的原生,并且在 vnode 上保留 el
  const el = vnode.el = document.createElement(vnode.tag)
  // 2. 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key];
      // 对事件监听的判断
      if (key.startsWith("on")) {
        // 切割并转小写取到 click 并绑定事件
        el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
      } else {
        // 设置属性
        el.setAttribute(key, value)
      }
    }
  }
  // 3. 处理 children
  if (vnode.children) {
    if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
      el.textContent = vnode.children
    } else {
      // 对数组进行循环
      vnode.children.forEach(item => {
        console.log('item', item); // 打印 vnode 的子元素
        // 递归调用
        mount(item, el)
      })
    }
  }
  // 4. 将 el 挂载到 container 上
  container.appendChild(el)
  return {
    vnode,
    container
  }
}
// n1:vnode1  n2:vnode2 
const patch = (n1, n2) => {
  // 判断类型
  if (n1.tag !== n2.tag) {
    // 获取 n1 的父元素
    const n1ElParent = n1.el.parentElement;
    // 移除 n1 元素
    n1ElParent.removeChild(n1.el)
    // 调用 mount 传入要渲染的元素和渲染的父元素
    mount(n2, n1ElParent)
  } else { // 如果类型一样就判断里面的属性  类似 diff 算法
    // 1. 取出 element 对象,并且在 n2 中进行保存
    // 此时是吧 n1 原来的元素同时赋值给 n2
    const el = n2.el = n1.el
    // debugger; 
    // 2. 处理 props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    // console.log(oldProps); // {id: 'main'}
    // console.log(newProps); // {class: 'title'}
    // 2.1. 获取所有的 newProps 添加到 el
    for (const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        // 对事件监听的判断
        if (key.startsWith("on")) {
          // 切割并转小写取到 click 并绑定事件
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          // 设置属性
          el.setAttribute(key, newValue)
        }
      }
    }
    // 2.2. 删除旧的 props
    for (const key in oldProps) {
      // 判断旧的 key 是否存在新的 key 里面
      // if (!(key in newProps)) {
      if (!newProps.hasOwnProperty(key)) {
        // 判断是否是 on 开头的 API 操作
        if (key.startsWith("on")) {
          const value = oldProps[key]
          // 移除具体的某一个函数
          el.removeEventListener(key.slice(2).toLowerCase(), value)
        } else {
          // 移除属性
          el.removeAttribute(key)
        }
      }
    }
    // 3. 处理 children
    const oldChildren = n1.children || []
    const newChildren = n2.children || []
    if (typeof newChildren === 'string') { // 情况一:newChildren 本身是一个 string
      // 边界情况 (edge case)
      if (typeof oldChildren === 'string') {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else { 
        el.innerHTML = newChildren
      }
    } else {  // 情况二:newChildren 本身是一个数组
      if (typeof oldChildren === 'strinf') {
        el.innerHTML = ''
        // 遍历新元素里面的 children
        newChildren.forEach(item => {
          mount(item,el)
        })
      } else {
        // oldChildren:[v1,v2,v3]
        // newChildren:[v1,v2,v3,v4,v5,v6]
        //commonLength 取两个数组长度更短的那一个值
        // 1. 前面有相同节点的元素进行 patch 操作  
        const commonLength = Math.min(oldChildren.length, newChildren.length)
        for (let i = 0; i < commonLength; i++){
          patch(oldChildren[i],newChildren[i])  // 相等的在这里已经处理完了
        }
        // 2.newChildren.length > oldChildren.length  如果新数组比旧数组长的话,则直接将 oldChildren 切割到与 newChildren 一样长,再直接挂载!
        if (newChildren.length > oldChildren.length) {
          newChildren.slice(oldChildren.length).forEach(item => {
            mount(item,el)
          })
        }
 
        // 3.newChildren.length < oldChildren.length  如果旧数组长度比新数组长的话会将剩下的元素切割掉。
        if (newChildren.length < oldChildren.length) {
          oldChildren.slice(newChildren.length).forEach(item => {
            el.removeChild(item.el)
          })
        }
      }
    }
  }
  return {
    n1,
    n2
  }
}

参考结果:

render

# 响应式系统

# 手动响应式

# index

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Retive</title>
</head>
<body>
<script src="./reactive.js"></script>
</body>
</html>

# reactive

class Dep {
  constructor() {
    // 不用数组用 Set 是因为元素不会重复
    this.subscribers = new Set();
  }
  // 数据发送改变产生的副作用
  addEffect(effect) {
    this.subscribers.add(effect)
  }
  // 订阅者通知
  notify(effect){
    this.subscribers.forEach(effect => {
        effect()
    }) 
  }
}
const dep = new Dep()
const info = { counter: 100 }
function doubleCounter() {
  console.log(info.counter * 2)
}
function powerCounter() {
  console.log(info.counter * info.counter)
}
// 手动收集依赖
dep.addEffect(doubleCounter)
dep.addEffect(powerCounter)
info.counter++;
// 手动通知
dep.notify()
console.log(dep);

参考结果:

reactive_basic

# effect 代码进行重构 (不能追踪具体属性)

# index

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Retive</title>
</head>
<body>
<script src="./reactive.js"></script>
</body>
</html>

# reactive

class Dep {
  constructor() {
    // 不用数组用 Set 是因为元素不会重复
    this.subscribers = new Set();
  }
  // 数据发送改变产生的副作用
  // addEffect(effect) {
  //   this.subscribers.add(effect)
  // }
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  // 订阅者通知
  notify(effect){
    this.subscribers.forEach(effect => {
        effect()
    }) 
  }
}
const dep = new Dep()
let activeEffect = null;
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面
function watchEffect(effect) {
  activeEffect = effect;
  // 执行 depend 自动把 effect 添加到 set 里面
  dep.depend();
  // 把传进来的函数默认应该执行一次  第一批执行
  effect();
  //activeEffect 判断完后置空
  activeEffect = null;
}
const info = { counter: 100 }
watchEffect(function () {
  console.log('info.counter * 2:',info.counter * 2)
})
watchEffect(function () {
  console.log('info.counter * info.counter:',info.counter * info.counter)
})
watchEffect(function () {
  console.log('info.counter *+ 10:',info.counter *+ 10)
})
// 手动收集依赖
// dep.addEffect(doubleCounter)
// dep.addEffect(powerCounter)
info.counter++;
// 手动通知
dep.notify()
console.log(dep);

参考结果:

reactive_effect_reconsitution

# Vue2 defineProperty 实现思路

# index

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Retive</title>
</head>
<body>
<script src="./reactive.js"></script>
</body>
</html>

# reactive

class Dep {
  constructor() {
    // 不用数组用 Set 是因为元素不会重复
    this.subscribers = new Set();
  }
  // 数据发送改变产生的副作用
  // addEffect(effect) {
  //   this.subscribers.add(effect)
  // }
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  // 订阅者通知
  notify(effect) {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}
// const dep = new Dep()
let activeEffect = null;
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面
function watchEffect(effect) {
  activeEffect = effect;
  // 执行 depend 自动把 effect 添加到 set 里面
  //dep.depend (); // 不需要了 因为调用 effect 函数的时候劫持数据已经调用了 depned 了
  // 把传进来的函数默认应该执行一次  第一批执行
  effect();
  //activeEffect 判断完后置空
  activeEffect = null;
}
// Map ({key: value}): key 是一个字符串
// WeakMap ({key (对象): value}):key 是一个对象,弱引用
const targetMap = new WeakMap()
function getDep(target, key) {
  // 1. 根据对象 (target) 取出对应的 Map 对象
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 2. 取出具体的 Map 对象
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep)
  }
  return dep;
}
//vue2 对 raw 进行数据劫持
function reactive(raw) {
  Object.keys(raw).forEach(key => {
    const dep = getDep(raw, key);
    let value = raw[key];
    Object.defineProperty(raw, key, {
      get() {
        dep.depend();
        return value;
      },
      set(newValue) {
        if (value !== newValue) {
          value = newValue;
          dep.notify();
        }
      }
    })
  })
  return raw;
}
// 测试代码
const info = reactive({ counter: 100, name: 'neko' })
const foo = reactive({ height: 1.88 })
// watchEffect1
watchEffect(function () {
  console.log('effect1:', info.counter * 2, "info.name:", info.name)
})
// watchEffect2
watchEffect(function () {
  console.log('effect2:', info.counter * info.counter)
})
// watchEffect3
watchEffect(function () {
  console.log('effect3:', info.counter * + 10, "info.name:", info.name)
})
// watchEffect4
watchEffect(function () {
  console.log('effect4:', foo.height)
})
// 手动收集依赖
// dep.addEffect(doubleCounter)
// dep.addEffect(powerCounter)
info.counter++;
// 手动通知
// dep.notify()
// 数据发生修改需要进行劫持
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('-----------2s后info.name发生了修改-----------');
    //info.name = 'neko' //name 未发生改变时不用执行赋值与通知 
    info.name = 'saber'
    resolve("name已发生改变可以,2秒后改变foo.height会触发effect4收集依赖")
  }, 2000)
})
p.then(res => {
  console.log(res);
  setTimeout(() => {
    foo.height = 1.68
  }, 2000)
})
// foo.height = 2
// console.log(dep);

参考结果:

Vue2_defineProperty_reactive

# Vue3 Proxy 实现思路

# index

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Retive</title>
</head>
<body>
  <script src="./reactive.js"></script>
</body>
</html>

# reactive

class Dep {
      constructor() {
        // 不用数组用 Set 是因为元素不会重复
        this.subscribers = new Set();
      }
      // 数据发送改变产生的副作用
      // addEffect(effect) {
      //   this.subscribers.add(effect)
      // }
      depend() {
        if (activeEffect) {
          this.subscribers.add(activeEffect)
        }
      }
      // 订阅者通知
      notify(effect) {
        this.subscribers.forEach(effect => {
          effect()
        })
      }
    }
    // const dep = new Dep()
    let activeEffect = null;
    // 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面
    function watchEffect(effect) {
      activeEffect = effect;
      // 执行 depend 自动把 effect 添加到 set 里面
      //dep.depend (); // 不需要了 因为调用 effect 函数的时候劫持数据已经调用了 depned 了
      // 把传进来的函数默认应该执行一次  第一批执行
      effect();
      //activeEffect 判断完后置空
      activeEffect = null;
    }
    // Map ({key: value}): key 是一个字符串
    // WeakMap ({key (对象): value}):key 是一个对象,弱引用
    const targetMap = new WeakMap()
    function getDep(target, key) {
      // 1. 根据对象 (target) 取出对应的 Map 对象
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }
      // 2. 取出具体的 Map 对象
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep)
      }
      return dep;
    }
    //vue3 对 raw 进行数据劫持
    function reactive(raw) {
      return new Proxy(raw, {
        get(target, key) {
          const dep = getDep(target, key)
          dep.depend();
          return target[key];
        },
        set(target, key, newValue) {
          const dep = getDep(target, key);
          target[key] = newValue;
          dep.notify();
        }
      });
    }
    // 测试代码
    const info = reactive({ counter: 100, name: 'neko' })
    const foo = reactive({ height: 1.88 })
    // watchEffect1
    watchEffect(function () {
      console.log('effect1:', info.counter * 2, "info.name:", info.name)
    })
    // watchEffect2
    watchEffect(function () {
      console.log('effect2:', info.counter * info.counter)
    })
    // watchEffect3
    watchEffect(function () {
      console.log('effect3:', info.counter * + 10, "info.name:", info.name)
    })
    // watchEffect4
    watchEffect(function () {
      console.log('effect4:', foo.height)
    })
    // 手动收集依赖
    // dep.addEffect(doubleCounter)
    // dep.addEffect(powerCounter)
    info.counter++;
    // 手动通知
    // dep.notify()
    // 数据发生修改需要进行劫持
    const p = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('-----------2s后info.name发生了修改-----------');
        //info.name = 'neko' //name 未发生改变时不用执行赋值与通知 
        info.name = 'saber'
        resolve("name已发生改变可以,2秒后改变foo.height会触发effect4收集依赖")
      }, 2000)
    })
    p.then(res => {
      console.log(res);
      setTimeout(() => {
        foo.height = 1.68
      }, 2000)
    })
// foo.height = 2
// console.log(dep);

参考结果:也能实现上面的效果,但无疑使用 Proxy 更好!

Vue3_Proxy_reactive

# Mini-Vue 实现结果

# index html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mini Vue实现</title>
</head>
<body>
  <div id="app"></div>
  <script src="./render.js"></script>
  <script src="./reactive.js"></script>
  <script src="./index.js"></script>
  <script>
    // 1. 创建根组件
    const App = {
      data: reactive({
        counter: 0
      }),
      render() {
        return h("div", null, [
          h('h2', null, `当前计数:${this.data.counter}`),
          h("button", {
            onClick: () => {
              this.data.counter++
            }
          }, "+1")
        ])
      }
    }
    // 2. 挂载根组件
    const app = createApp(App)
    app.mount("#app")
  </script>
</body>
</html>

# render (改进)

const h = (tag, props, children) => {
  // 返回一个对象给 vnode
  return {
    tag,
    props,
    children
  }
}
const mount = (vnode, container) => {
  // 1. 创建出真实的原生,并且在 vnode 上保留 el
  const el = vnode.el = document.createElement(vnode.tag)
  // 2. 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key];
      // 对事件监听的判断
      if (key.startsWith("on")) {
        // 切割并转小写取到 click 并绑定事件
        el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
      } else {
        // 设置属性
        el.setAttribute(key, value)
      }
    }
  }
  // 3. 处理 children
  if (vnode.children) {
    if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
      el.textContent = vnode.children
    } else {
      // 对数组进行循环
      vnode.children.forEach(item => {
        console.log('item', item); // 打印 vnode 的子元素
        // 递归调用
        mount(item, el)
      })
    }
  }
  // 4. 将 el 挂载到 container 上
  container.appendChild(el)
  return {
    vnode,
    container
  }
}
// n1:vnode1  n2:vnode2 
const patch = (n1, n2) => {
  // 判断类型
  if (n1.tag !== n2.tag) {
    // 获取 n1 的父元素
    const n1ElParent = n1.el.parentElement;
    // 移除 n1 元素
    n1ElParent.removeChild(n1.el)
    // 调用 mount 传入要渲染的元素和渲染的父元素
    mount(n2, n1ElParent)
  } else { // 如果类型一样就判断里面的属性  类似 diff 算法
    // 1. 取出 element 对象,并且在 n2 中进行保存
    // 此时是吧 n1 原来的元素同时赋值给 n2
    const el = n2.el = n1.el
    // debugger; 
    // 2. 处理 props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    // console.log(oldProps); // {id: 'main'}
    // console.log(newProps); // {class: 'title'}
    // 2.1. 获取所有的 newProps 添加到 el
    for (const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        // 对事件监听的判断
        if (key.startsWith("on")) {
          // 切割并转小写取到 click 并绑定事件
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          // 设置属性
          el.setAttribute(key, newValue)
        }
      }
    }
    // 2.2. 删除旧的 props
    for (const key in oldProps) {
      // 判断旧的 key 是否存在新的 key 里面
      // if (!(key in newProps)) {
      //if (!newProps.hasOwnProperty (key)) { // 这里移除了判断 因为每次执行 patch 都添加了事件 每次判断取到的对象不是同一个对象
        // 每次判断这里的 key 是以 on 开头的情况下都执行移除
        if (key.startsWith("on")) {
          const value = oldProps[key]
          // 移除具体的某一个函数
          el.removeEventListener(key.slice(2).toLowerCase(), value)
        }
        if(!(key in newProps)) {
          // 移除属性
          el.removeAttribute(key);
        }
      // }
    }
    // 3. 处理 children
    const oldChildren = n1.children || []
    const newChildren = n2.children || []
    if (typeof newChildren === 'string') { // 情况一:newChildren 本身是一个 string
      // 边界情况 (edge case)
      if (typeof oldChildren === 'string') {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else { 
        el.innerHTML = newChildren
      }
    } else {  // 情况二:newChildren 本身是一个数组
      if (typeof oldChildren === 'strinf') {
        el.innerHTML = ''
        // 遍历新元素里面的 children
        newChildren.forEach(item => {
          mount(item,el)
        })
      } else {
        // oldChildren:[v1,v2,v3]
        // newChildren:[v1,v2,v3,v4,v5,v6]
        //commonLength 取两个数组长度更短的那一个值
        // 1. 前面有相同节点的元素进行 patch 操作  
        const commonLength = Math.min(oldChildren.length, newChildren.length)
        for (let i = 0; i < commonLength; i++){
          patch(oldChildren[i],newChildren[i])  // 相等的在这里已经处理完了
        }
        // 2.newChildren.length > oldChildren.length  如果新数组比旧数组长的话,则直接将 oldChildren 切割到与 newChildren 一样长,再直接挂载!
        if (newChildren.length > oldChildren.length) {
          newChildren.slice(oldChildren.length).forEach(item => {
            mount(item,el)
          })
        }
 
        // 3.newChildren.length < oldChildren.length  如果旧数组长度比新数组长的话会将剩下的元素切割掉。
        if (newChildren.length < oldChildren.length) {
          oldChildren.slice(newChildren.length).forEach(item => {
            el.removeChild(item.el)
          })
        }
      }
    }
  }
  return {
    n1,
    n2
  }
}

# reactive (Proxy)

class Dep {
  constructor() {
    // 不用数组用 Set 是因为元素不会重复
    this.subscribers = new Set();
  }
  // 数据发送改变产生的副作用
  // addEffect(effect) {
  //   this.subscribers.add(effect)
  // }
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  // 订阅者通知
  notify(effect) {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}
// const dep = new Dep()
let activeEffect = null;
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面
function watchEffect(effect) {
  activeEffect = effect;
  // 执行 depend 自动把 effect 添加到 set 里面
  //dep.depend (); // 不需要了 因为调用 effect 函数的时候劫持数据已经调用了 depned 了
  // 把传进来的函数默认应该执行一次  第一批执行
  effect();
  //activeEffect 判断完后置空
  activeEffect = null;
}
// Map ({key: value}): key 是一个字符串
// WeakMap ({key (对象): value}):key 是一个对象,弱引用
const targetMap = new WeakMap()
function getDep(target, key) {
  // 1. 根据对象 (target) 取出对应的 Map 对象
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 2. 取出具体的 Map 对象
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep)
  }
  return dep;
}
//vue3 对 raw 进行数据劫持
function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key)
      dep.depend();
      return target[key];
    },
    set(target, key, newValue) {
      const dep = getDep(target, key);
      target[key] = newValue;
      dep.notify();
    }
  });
}
// 测试代码
const info = reactive({ counter: 100, name: 'neko' })
const foo = reactive({ height: 1.88 })
// watchEffect1
watchEffect(function () {
  console.log('effect1:', info.counter * 2, "info.name:", info.name)
})
// watchEffect2
watchEffect(function () {
  console.log('effect2:', info.counter * info.counter)
})
// watchEffect3
watchEffect(function () {
  console.log('effect3:', info.counter * + 10, "info.name:", info.name)
})
// watchEffect4
watchEffect(function () {
  console.log('effect4:', foo.height)
})
// 手动收集依赖
// dep.addEffect(doubleCounter)
// dep.addEffect(powerCounter)
info.counter++;
// 手动通知
// dep.notify()
// 数据发生修改需要进行劫持
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('-----------2s后info.name发生了修改-----------');
    //info.name = 'neko' //name 未发生改变时不用执行赋值与通知 
    info.name = 'saber'
    resolve("name已发生改变可以,2秒后改变foo.height会触发effect4收集依赖")
  }, 2000)
})
p.then(res => {
  console.log(res);
  setTimeout(() => {
    foo.height = 1.68
  }, 2000)
})
// foo.height = 2
// console.log(dep);

# index js

function createApp(rootComponent) {
  return {
    // 选择器
    mount(selector) {
      const container = document.querySelector(selector);
      let isMounted = false;
      let oldVNode = null;
      // 数据发送更新
      watchEffect(() => {
        // 没有挂载的情况下  调用 render 里面的 mount
        if (!isMounted) { 
          oldVNode = rootComponent.render()
          mount(oldVNode, container);
          // 下次已经挂载完了
          isMounted = true;
        } else {
          const newVNode = rootComponent.render();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      })
    }
  }
}

参考结果:

Mini-Vue