Vue3源码(简单手写实现)

1,209 阅读3分钟

阅读Vue3源码,进行简单手写实现

如果想要阅读源码,请阅读我的下一篇文章 vue3源码(梳理阅读)

了解虚拟DOM的优势

先对真实的元素节点进行抽象,抽象成为虚拟节点(VNode)

因为对真实DOM进行操作很不方便,而Vnode就是js对象,操作起来很方便.

VNode方便实现跨平台,将其渲染成为你想要的任意节点(Android,ios...)

template (通过compiler编译)->render函数进行渲染(调用h函数)->虚拟节点->真实dom(解析)->浏览器展示

源码包含三大核心

image.png
image.png

手写h函数,进行挂载,并进行patch(diff算法)

    // 挂载到#app上
   <div id="app"></div>

   <script src="./renderer.js"></script>
   <script>
    //  1.通过h函数来创建一个Vnode
    const vnode1 = h("div",{class:'hao'},[
      h("h2",null,"Fhup"),
      h("button",{onClick:()=>{console.log("你点击了按钮!!");}},"点击")
    ])
    // vdom->很多的vnode组成vdom
    console.log(vnode1);

    // 2.通过mount函数将vnode1进行挂载
    mount(vnode1,document.querySelector("#app"))

    setTimeout(() => {
      // 3.创建新的vnode
      const vnode2 = h("div",{class:'fhup'},[
        h("h2",null,"美美美"),
        h("button",{onClick:()=>{console.log("美美美!!");}},"点击")
      ])
      // 4.两个虚拟dom做对比(diff算法),只更新变化的
      patch(vnode1,vnode2)
    }, 2000);
   </script>
// h函数的实现
const h=(tag,props,children)=>{
  // VNode->js对象=>{}
  // 返回vnode
      return{
        tag,
        props,
        children
      }
  }

const mount = (vnode,container)=>{
  // 将vnode转为真实的dom
  // 1.创建真实的dom,并且在vnode保留el
  const el = vnode.el = document.createElement(vnode.tag)//vnode.el js会动态添加el属性
  // 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)
      }
    }
  }
  if(vnode.children){
    if(typeof vnode.children === "string"){
      el.textContent=vnode.children
    }else{
      vnode.children.forEach(element => {
        mount(element,el)
      });
    }
  }

  // 4.将el挂载到container上
  container.appendChild(el)
}

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中进行保存
    // 注意:n2之前没有el
    const el = n2.el= n1.el
    // 2.处理props
    const oldprops=n1.props || {}
    const newprops=n2.props || {}
    if(newprops){
      for(const key in newprops){
        const newValue=newprops[key]
        const oldValue=oldprops[key]
        //相同不用管,不相同添加上
        if(newValue!=oldValue){
          if(key.startsWith("on")){//对监听事件的判断
            el.addEventListener(key.slice(2).toLowerCase(),newValue)
          }else{
            el.setAttribute(key,newValue)
          }
        }
      }
    }
    // 3.删除旧的props
    if(oldprops){
      for(const key in oldprops){
        if(!(key in newprops)){
          if(key.startsWith("on")){//对监听事件的判断
            el.removeEventListener(key.slice(2).toLowerCase(),oldprops[key])
          }else{
            el.removeAttribute(key)
          }
        }
      }
    }
    //4.处理children
    const oldChildren = n1.children || []
    const newChildren = n2.children || []
    if(typeof newChildren === "string"){//情况一:对children是字符串的处理
      // 边界判断(edge case)
      if(typeof oldChildren === "string"){
        if(oldChildren !== newChildren){
          el.innerHTML = newChildren
        }
      }else{
        el.innerHTML = newChildren
      }
    }else{//情况二:不是String,是数组
      if(typeof oldChildren === "string"){
        el.innerHTML=""
        newChildren.forEach(item=>{
          mount(item,el)
        })
      }else{
        //二个children都是数组的话
        // oldChildren:[v1,v2,v3,v8,v9]
        // newChildren:[v1,v5,v6]
        // 1.前面有相同节点的元素进行patch操作
        const minlength=Math.min(oldChildren.length,newChildren.length)
        for(let i=0;i<minlength;i++){
          patch(oldChildren[i],newChildren[i])
        }
        if(oldChildren.length>newChildren.length){
          oldChildren.slice(newChildren.length).forEach(item=>{
            el.removeChild(item.el)
          })
        }else{
          newChildren.slice(oldChildren.length).forEach(item=>{
            mount(item,el)
          })
        }
      }
    }
  }
}

手写reactivity函数,实现响应式.

// 实现思路: 将所有包含info的函数进行收集,值变化一次,收集到的函数执行一次
// 简单理解为发布订阅者模式

// 定义依赖类dep
class Dep {
  constructor(){// 构造器
    this.subscribes=new Set()// 默认创建set集合
  }
  // 收集依赖函数
  depend(){
    if(activeEffect){
      this.subscribes.add(activeEffect)
    }
  }
  // 值变化进行通知,执行函数
  notify(){
    this.subscribes.forEach(effect=>{
      effect()
    })
  }
}

// 是否进行响应式,进行判断
let activeEffect = null
function watchEffect(effect){
  activeEffect = effect
  effect()// 默认执行一次
  activeEffect = null
}

// Map :key是一个字符串
// WeakMap :key是一个对象,弱引用
const targetMap = new WeakMap()
function getDep(target,key) {//({counter:100},counter)
  // 1.根据target取出对应的map对象
  let depMap=targetMap.get(target)
  if(!depMap){
    depMap=new Map()
    targetMap.set(target,depMap)
  }
  // 2.取出具体的dep对象
  let dep=depMap.get(key)
  if(!dep){
    dep = new Dep()
    depMap.set(key,dep)
  }
  return dep
}

// vue2对raw进行数据劫持
// function reactive(raw){
//   // 对raw里面所有的key进行劫持
//   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
// }

// vue3对raw进行数据劫持
// 选择Proxy,因为Proxy劫持的是整个对象
// vue2对象新增时,需要再次调用defineProperty
function reactive(raw){
  // 返回代理对象
  return new Proxy(raw,{// 所有的修改,代理到raw对象中
    //之后执行set()和get()
    get(target, key){//target就是raw对象
      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: "Fhup"})
const foo= reactive({height: 1.88})

watchEffect(function(){
  console.log("effect1", info.counter * 2, info.name);
})
watchEffect(function(){
  console.log("effect2", info.counter * info.counter, info.counter);
})
watchEffect(function(){
  console.log("effect3", info.counter + 10, info.name);
})
watchEffect(function(){
  console.log("effect4", foo.height);
})

// info.counter++
// info.name="Fhup"
foo.height=2

将上面二个手写的函数进行合并,实现mini-vue

<!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="renderer.js"></script>
  <script src="reactivity.js"></script>
  <script src="createapp.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++
              console.log(this.data.counter);
            }
          }, "+1")
        ])
      }
    }

    // 2.挂载根组件
    const app = createApp(App);
    app.mount("#app");
  </script>

</body>
</html>
// createapp.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 {// 数据改变进行patch(diff算法)
          const newVNode = rootComponent.render();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      })
    }
  }
}

image.png