根据vue3源码实现mini-vue实现

620 阅读16分钟

实现Mini-Vue

  • 实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:

    • 渲染系统模块;
    • 可响应式系统模块;
    • 应用程序入口模块;

渲染系统实现

  • 渲染系统,模块主要包含三个功能;

    • 功能一: h函数,用于返回一个VNode对象;
    • 功能二:mount函数,用于将VNode挂载到DOM上
    • 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode

介绍h函数

我们一般在编写vue代码时,会首先编写模板代码,也就是template标签中的代码。如果我们想要比模板更加接近编译器,此时我们可以使用渲染函数。 我们编写的代码转化为真正的dom时,首先会先转换为VNode,然后多个Vnode进行结合起来转化为VDOM,最后VDOM才渲染成真实的DOM,此时我们思考一个问题,如果我们直接编写生成vnode的代码,效率会更高,这里我们就是h()函数。h函数我们也可以称为createVnode函数。h函数接收三个参数

  • 第一个参数:,可以为一个html标签,一个组件,一个异步组件,或者是一个函数式组件
  • 第二个参数:{ Object } Props,与**attributesprops,以及事件对应的对象**,我们可以在模板中使用,如果没有需要传入的属性,可以设置为null
  • 第三个参数:{String | Object |Array}可以是字符串Text文本或者是h函数构建的对象再者可以是有插槽的对象

实现h函数

1、先创建一个index.html和render.js

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>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./render.js"></script>
    <script>
      // 1、通过h函数来创建一个vnode
      const vnode = h("div", { class: "why" }, [
        h("h2", null, "当前计数:100"),
        h("button", null, "+1"),
      ]);
    </script>
  </body>
</html>

render.js文件

const h = (tag, props, children) => {
  // 其实vnode就是一个js对象 =>   {}
  return {
    tag,
    props,
    children,
  };
};
 

实现mount函数

首先实现mount函数会有点小复杂,这里的childern不考虑插槽的问题,我们只考虑string类型和Array类型,其实如果是string类型我就挂载到元素上面,如果是Array我们就直接递归调用mount函数就行。

const mount = (vnode, container) => {
  // vnode -> element
  // 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")) {
        // 对事件的监听
        el.addEventListener(key.slice(2).toLowerCase(), value);
      } else {
        el.setAttribute(key, value);
      }
    }
  }
​
  // 3、处理children
  if (vnode.children) {
    if (typeof vnode.children === "string") {
      el.textContent = vnode.children;
    } else {
      vnode.children.forEach((item) => {
        mount(item, el);
      });
    }
  }
  // 4、 将el挂载到container上
  container.appendChild(el);
};

mount函数的实现我们分四步去理,由h函数我们知道有三块元素分别是(tag, props, children),所以我们先将tag的元素进行创建,再将props的属性进行挂载,最后再去判断children。

render.js文件变成

const h = (tag, props, children) => {
  // 其实vnode就是一个js对象 =>   {}
  return {
    tag,
    props,
    children,
  };
};
​
const mount = (vnode, container) => {
  // vnode -> element
  // 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")) {
        // 对事件的监听
        el.addEventListener(key.slice(2).toLowerCase(), value);
      } else {
        el.setAttribute(key, value);
      }
    }
  }
​
  // 3、处理children
  if (vnode.children) {
    if (typeof vnode.children === "string") {
      el.textContent = vnode.children;
    } else {
      vnode.children.forEach((item) => {
        mount(item, el);
      });
    }
  }
  // 4、 将el挂载到container上
  container.appendChild(el);
};

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>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./render.js"></script>
    <script>
      // 1、通过h函数来创建一个vnode
      const vnode = h("div", { class: "why" }, [
        h("h2", null, "当前计数:100"),
        h("button", null, "+1"),
      ]); // vdom
      console.log(vnode);
      
      // 2、通过mount函数,将vnode挂载到div#app上
      mount(vnode, document.querySelector("#app"));
    </script>
  </body>
</html>

页面渲染完成

image-20230104143948898.png

实现patch函数

  • patch函数的实现,分为两种情况

  • n1和n2是不同类型的节点

    • 找到n1的el父节点,删除原来的n1节点的el;
    • 挂载n2节点到n1的el父节点上;
  • n1和n2节点是相同的节点:

    • 处理props的情况

      • 先将新节点的props全部挂载到el上;
      • 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
    • 处理children的情况

      • 如果新节点是一个字符串类型,那么直接调用 el.textContent= newChildren

      • 如果新节点不同一个字符串类型:

        • 旧节点是一个字符串类型

          • 将el的textContent设置为空字符串;
          • 旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
        • 旧节点也是一个数组类型

          • 取出数组的最小长度;
          • 遍历所有的节点,新节点和旧节点进行path操作;
          • 如果新节点的length更长,那么剩余的新节点进行挂载操作;
          • 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
// 实现diff
const patch = (n1, n2) => {
  if (n1.tag !== n2.tag) {
    const n1ElParent = n1.el.parentElement;
    n1ElParent.removeChild(n1.el);
    mount(n2, n1ElParent);
  } else {
    // 1、去除element对象,并且n2中进行保存
    const el = (n2.el = n1.el);
    // 2、处理props
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    // 2.1 获取所有的newProps添加到el
    for (const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        if (key.startsWith("on")) {
          // 对事件的监听
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          el.setAttribute(key, newValue);
        }
      }
    }
    // 2.2 删除旧的props
    for (const key in oldProps) {
      if (!(key in newProps)) {
        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") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren;
        }
      } else {
        el.innerHTML = newChildren;
      }
    } else {
      // 情况二:newChildren 本身是一个数组
      if (typeof oldChildren === "string") {
        el.innerHTML = "";
        newChildren.forEach((itme) => {
          mount(item, el);
        });
      } else {
        // oldChildren:[v1,v2,v3]
        // newChildren:[v1,v5,v6,v7,v8]
        // 1、前面有相同节点的原生进行patch操作
        const oldLength = oldChildren.length;
        const newLength = newChildren.length;
        const commonLength = Math.min(oldLength, newLength);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i]);
        }
        // 2、oldLength < newLength
        if (oldLength < newLength) {
          // mount new
          newChildren.slice(oldLength).forEach((item) => {
            mount(item, el);
          });
        }
        // 3、oldLength > newLength
        if (oldLength > newLength) {
          // remove old
          oldChildren.slice(newLength).forEach((item) => {
            el.removeChild(item.el);
          });
        }
      }
    }
  }
};

旧节点:n1

新节点:n2

先把patch函数贴出来,我们来分析一下tag我们先判断是不是同一个标签,如果不是我们就移除掉n1,如果是同一个标签,那我们就去判断第二个元素也就是props,我们可以看到代码const el = (n2.el = n1.el);可能会有人问我什么这样写,其实这边n2.el是不存在的为什么呢,上节我们知道el是在mount挂载的时候去填充的为什么要这个el,其实很简单对removeChildappendChild是比较方便的,因为n2.el是不存在,我们已经判断了n2n1的tag是一样的,所以我们直接给n2.el去赋值并没有什么问题,那么我们el = (n2.el = n1.el)为什么这样写呢,其实他们是对象,当el修改了,n1n2也就修改了也就是回流,不懂回流的建议这边去看看定义

JS回流与重绘,其次我们判断props会去遍历

   // 2.1 获取所有的newProps添加到el
    for (const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        if (key.startsWith("on")) {
          // 对事件的监听
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          el.setAttribute(key, newValue);
        }
      }
    }

这段是获取newProps把新属性加入到el里,对于旧节点和新节点共同key但是不同value进行替换

    // 2.2 删除旧的props
    for (const key in oldProps) {
      if (!(key in newProps)) {
        if (key.startsWith("on")) {
          // 对事件的监听
          const value = oldProps[key];
          el.removeEventListener(key.slice(2).toLowerCase(), value);
        } else {
          el.removeAttribute(key);
        }
      }
    }

这边主要是遍历旧节点的props对于这些属性如果不在newProps我们就直接remove;

现在我们已经讲解完了对于props新旧节点的删除和添加,我们接下来讲解children是如何实现的,其实我也看了vue3的源码他对于很多边界进行了判断,当然我们只是学习他的思想,并不可能全部去实现。

 if (typeof newChildren === "string") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren;
        }
      } else {
        el.innerHTML = newChildren;
      }
    }

可能会有人问了,为什么用textContent?为什么又用innerHTML,我们看看如果newChildrenoldChildren,如果都是文本的话那就是说我们只需要替换文本内容,如果oldChildren不是文本内容而是HTML那就需要去用innerHTML,为什么用的不一样呢,因为textContentinnerHTML性能更好,这里我给出解释

正如其名称,Element.innerHTML 返回 HTML。通常,为了在元素中检索或写入文本,人们使用 innerHTML。但是,textContent 通常具有更好的性能,因为文本不会被解析为 HTML。

这段文字时MDN给出的解释innerHTML和textContent

      // 情况二:newChildren 本身是一个数组
      if (typeof oldChildren === "string") {
        el.innerHTML = "";
        newChildren.forEach((itme) => {
          mount(item, el);
        });
      }

如果newChildren是一个数组那我们把之前的清空,并且去遍历newChildren通过mount去挂载它

else {
        // oldChildren:[v1,v2,v3]
        // newChildren:[v1,v5,v6,v7,v8]
        // 1、前面有相同节点的原生进行patch操作
        const oldLength = oldChildren.length;
        const newLength = newChildren.length;
        const commonLength = Math.min(oldLength, newLength);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i]);
        }
        // 2、oldLength < newLength
        if (oldLength < newLength) {
          // mount new
          newChildren.slice(oldLength).forEach((item) => {
            mount(item, el);
          });
        }
        // 3、oldLength > newLength
        if (oldLength > newLength) {
          // remove old
          oldChildren.slice(newLength).forEach((item) => {
            el.removeChild(item.el);
          });
        }

这段代码主要是模仿vue3源码去写的,这边要先去找最短的长度去遍历,然后去判断oldChildrennewChildren的长度,如果oldLength < newLength那我们就去把newChildren多余的去挂载上去,如果oldLength > newLength,那就把oldChildren多余的去删除。这里我把vue3源码贴出来

image-20230105111957656.png

响应式系统

  • 上面我们已经实现了渲染系统的代码,接下来我们实现响应式系统,当然vue3和vue2在实现响应式的时候采取了不同策略,vue3采用了Proxy,而vue2采用的是Object.defineProperty,那么我们要实现简单的响应式系统首先我们必须对这俩个API进行学习

1. 通过defineProperty给对象创建属性描述符

我们知道在面向对象中对象属性描述符中有`set`与`get`可以对对象中的属性进行取值与赋值的监听。
const obj = {
  name: "why",
  age: 18
}

Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    get: function() {
      console.log(`监听到obj对象的${key}属性被访问了`)
      return value
    },
    set: function(newValue) {
      console.log(`监听到obj对象的${key}属性被设置值`)
      value = newValue
    }
  })
})

obj.name = "kobe"
obj.age = 30

console.log(obj.name)
console.log(obj.age)
	虽然现在这种方式可以做到监听属性,因为对象属性描述符设计的初衷是用来定义属性的,我们强行将它变成了数据属性描述符。但是,我们想监听更加丰富的操作,比如新增、删除属性,我们这种方式就无能为力了。

2.Proxy

在ES6中新增了一个Proxy类,又称代理对象。如果我们希望监听一个对象的相关操作,都可以通过代理对象来完成。代理对象可以监听我们想要对原对象进行的一些操作(13种)

Proxy的使用:

const obj = {
  name: "why",
  age: 18
}

const objProxy = new Proxy(obj, {})


objProxy.name = "kobe"
objProxy.age = 30

console.log(obj.name)
console.log(obj.age)

上面new一个Proxy对象时我们不对相关属性进行见监听的话,我们去更改代理对象时也会更改原对象的值。

7ee0c8e372b8ab0a41cbf1f3e038fb3d.png

设置捕获器的放式其实跟第一种方式的写法一样,在创建时对代理对象的捕获方法进行重写:

const obj = {
  name: "why",
  age: 18
}

const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function(target, key) {
    console.log(`监听到对象的${key}属性被访问了`, target)
    return target[key]
  },

  // 设置值时的捕获器
  set: function(target, key, newValue) {
    console.log(`监听到对象的${key}属性被设置值`, target)
    target[key] = newValue
  }
})

console.log(objProxy.name)
console.log(objProxy.age)

objProxy.name = "kobe"
objProxy.age = 30

console.log(obj.name)
console.log(obj.age)

get捕获器中的两个参数targetkey分别是被代理的对象与当前获取属性的key值。

set捕获器中的前两个参数与get一直,newValue显而易见就是我们要去把对象属性的原值修改成的新值。新增属性时也会被set进行捕获。

其实他们还有最后一个参数receiver这里先不展开。

Proxy中的其他捕获器

has: 监听in操作符的捕获器

const objProxy = new Proxy(obj, {
  // 监听in的捕获器
  has: function(target, key) {
    console.log(`监听到对象的${key}属性in操作`, target)
    return key in target
  }
})

console.log("name" in objProxy)

deleteProperty:监听delete的捕获器

const objProxy = new Proxy(obj, {
  // 监听delete的捕获器
  deleteProperty: function(target, key) {
    console.log(`监听到对象的${key}属性in操作`, target)
    delete target[key]
  }
})

delete objProxy.name

Proxy的所有的捕获器

beee7ae9ff075097667028b9c9750f62.png

最后的两个捕获器applyconstruct比较特殊,他们是用来监听函数对象的捕获器。

function foo() {

}

const fooProxy = new Proxy(foo, {
  apply: function(target, thisArg, argArray) {
    console.log("对foo函数进行了apply调用")
    return target.apply(thisArg, argArray)
  },
  construct: function(target, argArray, newTarget) {
    console.log("对foo函数进行了new调用")
    return new target(...argArray)
  }
})

fooProxy.apply({}, ["abc", "cba"])
new fooProxy("abc", "cba")

apply是对函数进行apply调用时进行捕获,其参数除了第一个是目标对象,其他的参数跟调用apply传的参数一样。

construct是对函数进行new操作时进行捕获,它的第一个参数与第三个参数其实都是目标对象,argArray是new调用时传递的参数,它是个数组。

3. Reflect的作用

Reflect又称反射,是ES6新增的一个API,它是一个对象。它提供了很多操作对象的方法,有点类似于Object中操作对象的方法。那就有个疑问了,既然我们已经有Object了,为什么还要加一个Reflect呢。

这是因为在Js早期设计时,没有考虑到要对对象进行操作,而后面不知道要将这些操作的api放在哪里,于是一口气将这些api全部方法了Object上面。而Object作为一个构造函数,包含这些api显然不合适,而且还有类似in、delete这些操作,就让js看起来变的很奇怪。于是在ES6中,将这些操作就都放到了Reflect中。

MDN上对二者区别的描述:比较Reflect和Object

beee7ae9ff075097667028b9c9750f62.png

Reflect和Proxy一起使用

我们在使用Proxy进行捕获操作时:

const obj = {
  name: "why",
  age: 18
}

const objProxy = new Proxy(obj, {
  get: function(target, key) {
    return target[key]
  },
  
  set: function(target, key, newValue) {
    target[key] = newValue
  }
})

objProxy.name = "kobe"

console.log(obj.name)

我们在setget中都是使用target[key]的方式进行操作,其实这种操作还是对原对象进行的操作,这与我们使用Porxy对对象进行代理操作的方式背道而驰了。

于是我们可以使用Reflect来进行操作

const obj = {
  name: "why",
  age: 18
}

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log("get---------")
    return Reflect.get(target, key)
  },
  
  set: function(target, key, newValue, receiver) {
    console.log("set---------")
    Reflect.set(target, key, newValue)
})

objProxy.name = "kobe"
console.log(objProxy.name)

这样的写法还有一个好处,当我们进行Reflect.set操作时,它会返回一个Boole类型的值来判断是否设置成功

const obj = {
  name: "why",
  age: 18
}

const objProxy = new Proxy(obj, {
  set: function(target, key, newValue, receiver) {
    console.log("set---------")
    target[key] = newValue

    const result = Reflect.set(target, key, newValue)
    if (result) {
    } else {
    }
  }
})

objProxy.name = "kobe"
console.log(objProxy.name)

4. Receiver参数的作用

现在我们有这样一串代码:

const obj = {
  _name: "why",
  get name() {
    return this._name
  },
  set name(newValue) {
    this._name = newValue
  }
}

const objProxy = new Proxy(obj, {
  get: function(target, key) {
    console.log("get方法被访问--------", key)
    return Reflect.get(target, key)
  },
  set: function(target, key, newValue) {
    Reflect.set(target, key, newValue)
  }
})

console.log(objProxy.name)

我们来分析一下它的运行流程,我们通过代理对象objProxy.name去问obj对象_name时,它会先去调用代理对象的get方法,通过代理方法的Reflect.get去访问到obj对象中name的get方法,而其中的this指向的是obj对象。于是this._name就等于直接是对原对象obj._name进行访问,那么在obj中的get方法会绕过代理对象的拦截,直接访问原对象的内容。我们想要对原对象的所有访问都受到拦截就是失去了意义。

3fb5c3efe1508512cd6546181a6707e4.png

所以这里只会拦截到 name的访问,而我们其实想要对_name的访问也进行拦截。

所以我们需要去想办法让obj对象中get方法的this指向我们的代理对象。这个时候我们就需要用到Receiver参数了。

const obj = {
  _name: "why",
  get name() {
    return this._name
  },
  set name(newValue) {
    this._name = newValue
  }
}

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    // receiver是创建出来的代理对象
    console.log("get方法被访问--------", key, receiver)
    console.log(receiver === objProxy)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    console.log("set方法被访问--------", key)
    Reflect.set(target, key, newValue, receiver)
  }
})

console.log(objProxy.name)
objProxy.name = "kobe"

在代理对象中的getset方法中传入的receiver会改变原对象中getset方法中的

this变成receiver,而这里的receivergetset方法传入时又指向了我们的代理对象,于是就可以将this变成我们的代理对象。

8780cceb77136ee8b2c474ea91ece21c.png

这里我们就能对_name的访问进行拦截了。

5. Reflect中construct的作用

现在我们有两个构造函数:

function Student(name, age) {
  this.name = name
  this.age = age
}

function Teacher() {

}

const stu = new Student("why", 18)
console.log(stu)
console.log(stu.__proto__ === Student.prototype)

10c1f8a036ed407379a671e98329115a.png

如果我们想要使用Student中的构造函数但是创建出来的类型是Teacher要怎么做呢?

这里就要使用到Reflect中construct了。

function Student(name, age) {
  this.name = name
  this.age = age
}

function Teacher() {

}

// 执行Student函数中的内容, 但是创建出来对象是Teacher对象
const teacher = Reflect.construct(Student, ["why", 18], Teacher)
console.log(teacher)
console.log(teacher.__proto__ === Teacher.prototype)

ea1bfa4737e556ceeabf8728ee3a908e.png

6.vue2 实现响应式

image-20230105114348603.png

现在有张这种图片意思就是当info.counter值改变了,doubleCounter函数能够自己调用,那么我们可以在他后面继续调用,那么问题来了,如果有很多地方都用到了,那我们应该怎么去实现呢?

这里我们使用一个类去基本是实现响应式的

class Dep {
  constructor() {
    this.subscriber = new Set();
  }
  addEffect(effect) {
    this.subscriber.add(effect);
  }
  notify() {
    this.subscriber.forEach((effect) => effect());
  }
}

const dep = new Dep();

const info = { counter: 100 };
function doubleCounter() {
  console.log("doubleCounter", info.counter * 2);
}
function powerCounter() {
  console.log("powerCounter", info.counter * info.counter);
}
dep.addEffect(doubleCounter);
dep.addEffect(powerCounter);
info.counter++;
dep.notify();

我们通过这个类去收集了依赖,并且在info.counter++;的时候去调用dep.notify();便可打印收集的依赖了,但是这样不方便。

接下来我将使用vue2Object.defineProperty来写响应式

class Dep {
  constructor() {
    this.subscriber = new Set();
  }

  depend() {
    if (activeEffect) {
      this.subscriber.add(activeEffect);
    }
  }
  notify() {
    this.subscriber.forEach((effect) => effect());
  }
}
let activeEffect = null;
function watchEffect(effect) {
  activeEffect = effect;
  effect();
  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、取出具体的dep对象
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}

// vue2 对row对数据进行劫持
function reactive(raw) {
  Object.keys(raw).forEach((key) => {
    const dep = getDep(raw, key);
    let value = raw[key];
    console.log("dep==>", dep);
    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: "why" });
const foo = reactive({ height: 1.88 });
// watchEffect1
watchEffect(function () {
  console.log("watchEffect1", info.counter * 2, info.name);
});
// watchEffect2
watchEffect(function () {
  console.log("watchEffect2", info.counter * info.counter);
});
// watchEffect3
watchEffect(function () {
  console.log("watchEffect3", info.counter * info.counter, info.name);
});
// watchEffect4
watchEffect(function () {
  console.log("watchEffect4", foo.height);
});
info.counter++;
info.name = "kobe";
foo.height = 1;

这段代码可能回避实现渲染系统的代码更加的绕,

// vue2 对row对数据进行劫持
function reactive(raw) {
  Object.keys(raw).forEach((key) => {
    const dep = getDep(raw, key);
    let value = raw[key];
    console.log("dep==>", dep);
    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: "why" });

先看这段代码吧,这段代码主要是将变量进行收集,当然熟悉的同学应该知道reactivevue3就是使用reactive创建一个响应式对象

通过Object.keys(raw)去遍历raw里的key主要是去收集依赖

// 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、取出具体的dep对象
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}
let activeEffect = null;
function watchEffect(effect) {
  activeEffect = effect;
  effect();
  activeEffect = null;
}
// watchEffect1
watchEffect(function () {
  console.log("watchEffect1", info.counter * 2, info.name);
});

我们先看watchEffect这个函数他会打印info.counter的值,这个时候就会调用Object.defineProperty定义的get方法get方法里面有dep.depend();就会调用这个方法,此时activeEffect已经将这个函数复制给了它,就会counter这个key收集了这个函数,当我们去info.counter++;就会通知这个函数的调用

但是vue3采用的是proxy

7. 为什么Vue3选择Proxy呢?

  • Object.definedProperty 是劫持对象的属性时,如果新增元素

    • 那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;、
    • this.$set(object, key, value) ,vue2如果想在初始化后添加一个属性并进行监听操作,可以使用$set:
  • 修改对象的不同:

    • 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
    • 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
  • Proxy 能观察的类型比 defineProperty 更丰富

    • has:in操作符的捕获器;
    • deleteProperty:delete 操作符的捕捉器;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;

  • 缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

8.vue3实现响应式

vue2的响应式的实现我们只需要对reactive这个函数进行改变就行了

// vue3 对 row进行数据劫持
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();
    },
  });
}
​

mini-vue成型

此时我们已经将渲染系统模块可响应式系统模块实现好了,就剩下了入口的问题。我们知道vue3的入口是createApp和挂载mount

<!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>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="../02_渲染器实现/render.js"></script>
    <script src="../03_响应式系统/reactive.js"></script>
    <script src="./index.js"></script>
    <script>
      // 根组件
      const App = {
        data: reactive({
          counter: 0,
        }),
        render() {
          return h("div", null, [
            h("h2", null, `当前计数:${this.data.counter}`),
            h(
              "button",
              {
                onClick: () => {
                  console.log("this", this.data);
                  this.data.counter++;
                },
              },
              `+1`
            ),
          ]);
        },
      };
      // 2、挂载根组件
      createApp(App).mount("#app");
    </script>
  </body>
</html>

我们先将App先实现一下在data里面继续定义响应式数据counter,render函数返回了vdom,导入./index.js也就是入口文件

function createApp(rootComponent) {
  return {
    mount(selector) {
      const container = document.querySelector(selector);
      let isMounted = false;
      let oldVNode = null;
      watchEffect(function () {
        if (!isMounted) {
          oldVNode = rootComponent.render();
          mount(oldVNode, container);
          isMounted = true;
        } else {
          const newVNode = rootComponent.render();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      });
    },
  };
}

可能有人会问了为什么createApp会导出一个对象

const app = createApp(App)
app.mount("#app");
app.directive()
app.use()

其实vue3就是可以导出一个对象然后这个对象里面有很多属性比如directive等等,所以我们这边也导出一个对象app.mount("#app");,mount函数会传入根元素也就是要挂载的标签

    let isMounted = false;
    let oldVNode = null;
      watchEffect(function () {
        if (!isMounted) {
          oldVNode = rootComponent.render();
          mount(oldVNode, container);
          isMounted = true;
        } else {
          const newVNode = rootComponent.render();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      });

如果挂载了,我们就去比较新旧节点的这段代码还是比较好理解的,没有挂载我们就去挂载,这边为什么用watchEffect,因为render函数里面有对data依赖,data里面的数据变化了,watchEffect也就会重新执行

1.完整代码如下

render.js

// 实现h函数
const h = (tag, props, children) => {
  // 其实vnode就是一个js对象 =>   {}
  return {
    tag,
    props,
    children,
  };
};
​
// 实现挂载
const mount = (vnode, container) => {
  // vnode -> element
  // 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")) {
        // 对事件的监听
        el.addEventListener(key.slice(2).toLowerCase(), value);
      } else {
        el.setAttribute(key, value);
      }
    }
  }
​
  // 3、处理children
  if (vnode.children) {
    if (typeof vnode.children === "string") {
      el.textContent = vnode.children;
    } else {
      vnode.children.forEach((item) => {
        mount(item, el);
      });
    }
  }
  // 4、 将el挂载到container上
  container.appendChild(el);
};
​
// 实现diff
const patch = (n1, n2) => {
  if (n1.tag !== n2.tag) {
    const n1ElParent = n1.el.parentElement;
    n1ElParent.removeChild(n1.el);
    mount(n2, n1ElParent);
  } else {
    // 1、去除element对象,并且n2中进行保存
    const el = (n2.el = n1.el);
    // 2、处理props
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    // 2.1 获取所有的newProps添加到el
    for (const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        if (key.startsWith("on")) {
          // 对事件的监听
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          el.setAttribute(key, newValue);
        }
      }
    }
    // 2.2 删除旧的props
    for (const key in oldProps) {
      if (key.startsWith("on")) {
        // 对事件的监听
        const value = oldProps[key];
        el.removeEventListener(key.slice(2).toLowerCase(), value);
      }
      if (!(key in newProps)) {
        // 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") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren;
        }
      } else {
        el.innerHTML = newChildren;
      }
    } else {
      // 情况二:newChildren 本身是一个数组
      if (typeof oldChildren === "string") {
        el.innerHTML = "";
        newChildren.forEach((itme) => {
          mount(item, el);
        });
      } else {
        // oldChildren:[v1,v2,v3]
        // newChildren:[v1,v5,v6,v7,v8]
        // 1、前面有相同节点的原生进行patch操作
        const oldLength = oldChildren.length;
        const newLength = newChildren.length;
        const commonLength = Math.min(oldLength, newLength);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i]);
        }
        // 2、oldLength < newLength
        if (oldLength < newLength) {
          // mount new
          newChildren.slice(oldLength).forEach((item) => {
            mount(item, el);
          });
        }
        // 3、oldLength > newLength
        if (oldLength > newLength) {
          // remove old
          oldChildren.slice(newLength).forEach((item) => {
            el.removeChild(item.el);
          });
        }
      }
    }
  }
};
​
export { h, mount, patch };

reactive.js

class Dep {
  constructor() {
    this.subscriber = new Set();
  }
​
  depend() {
    if (activeEffect) {
      this.subscriber.add(activeEffect);
    }
  }
  notify() {
    this.subscriber.forEach((effect) => effect());
  }
}
let activeEffect = null;
function watchEffect(effect) {
  activeEffect = effect;
  effect();
  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、取出具体的dep对象
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}
​
// vue3 对 row进行数据劫持
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();
      return true;
    },
  });
}
​
export { watchEffect, reactive };
​

index.js

import { watchEffect, reactive } from "./reactive.js";
import { h, mount as mounts, patch } from "./render.js";
function createApp(rootComponent) {
  return {
    mount(selector) {
      const container = document.querySelector(selector);
      let isMounted = false;
      let oldVNode = null;
      watchEffect(function () {
        if (!isMounted) {
          oldVNode = rootComponent.render();
          mounts(oldVNode, container);
          isMounted = true;
        } else {
          const newVNode = rootComponent.render();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      });
    },
  };
}
export { watchEffect, reactive, h, mounts, patch, createApp };

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>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import {
        watchEffect,
        reactive,
        h,
        mounts,
        patch,
        createApp,
      } from "./index.js";
      // 根组件
      const App = {
        data: reactive({
          counter: 0,
        }),
        render() {
          return h("div", null, [
            h("h2", null, `当前计数:${this.data.counter}`),
            h(
              "button",
              {
                onClick: () => {
                  console.log("this", this.data.counter);
                  this.data.counter++;
                },
              },
              `+1`
            ),
          ]);
        },
      };
      // 2、挂载根组件
      createApp(App).mount("#app");
    </script>
  </body>
</html>

代码提交到github