Simple Mini-Vue implementation

Implementing a Mini-Vue can deepen the understanding of vue, including rendering system modules, responsive system modules, and application entry modules (without implementing the compilation system module).

vue core module

Vue has three core modules, a compilation system, a rendering system, and a responsive system.

image.png

build system

  1. Parse the template template into an abstract syntax tree (AST).
  2. Optimize the AST.
  3. Generate a render function from the AST.

rendering system

  1. reder function returns vnode
  2. A tree structure vdom will be formed between vnodes
  3. Generate real dom based on vdom and render to browser

Responsive system

  1. Compare the old and new vnodes using the diff algorithm
  2. The rendering system regenerates the DOM based on the vnode and renders it to the browser

rendering system

image.png

Generate vnode by h function

The h function includes 3 parameters, element, attribute, child element. The resulting vnode is a javascript object

function h(tag, props, children) {
// vnode --> javascript对象
  return {
    tag,
    props,
    children
  }
}
复制代码
 //  1.通过h函数创建vnode
const vnode = h("div", {class: 'lin'}, [
  h("span", null, '我是靓仔'),
  h("button", {onClick: function() {}}, 'change')
])
复制代码

Documentation: H-function

Mount vnode to div#app through the mount function

mount(vnode, document.querySelector("#app"))

const h = (tag, props, children) => {
  // vnode --> javascript对象
  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 (let 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. 处理子元素  字符串,数组
  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)
}
复制代码

image.png

patch compares old and new nodes

When a node changes, compare the old and new nodes and update them. Based on the new VNode, transform the old VNode to be the same as the new VNode (patch)

setTimeout(() => {
  const vnode2 = h("div", {class: 'jin', onClick: function() {console.log("我是靓仔")}}, [
    h("button", {class: "zhang"}, '按钮')
  ])
  patch(vnode, vnode2)
}, 2000)
复制代码
const patch = (n1, n2) => {
  // n1旧 n2新
  // 如果父元素不一样, 直接替换
  if (n1.tag !== n2.tag) {
    // 获取父元素
    const n1Elparent = n1.el.parentElement
    // 移除旧节点
    n1Elparent.removeChild(n1.el)
    // 重新挂载新节点
    mount(n2, n1Elparent)
  } else {
    // 引用, 修改一个另一个也会改变, n1.el 在n1挂载(mount)时赋值
    const el = n2.el = n1.el 
    // 对比props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    // 把新的props添加到el上
    for (let 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)
        }
      }
    }
    // 删除旧的props 移除监听器, 属性
    for (let key in oldProps) {
      if (key.startsWith("on")) {
        el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
      } else {
        if (!key in newProps) {
          el.removeAttribute(key)
        }
      }
    }
    // 对比children
    // children 可能是字符串, 数组, 对象(插槽), 字符串跟数组比较常见
    // n1 [v1, v2, v3, v4, v5]
    // n2 [v1, v7, v8]
    
    const oldChildren = n1.children || []
    const newChidlren = n2.children || []
    
    if (typeof newChidlren === 'string') {   // 如果newChidlren是字符串
      if (typeof oldChildren === "string") {
        if (newChidlren !== oldChildren) {
        // textContent属性表示一个节点及其后代的文本内容
          el.textContent = newChidlren
        }
      } else {
       // innerHTML 返回 HTML  textContent 通常具有更好的性能,因为文本不会被解析为HTML 使用 textContent 可以防止 XSS 攻击。
        el.innerHtml = newChidlren
      }
    } else {  // 如果newChidlren是数组
      const oldLength = oldChildren.length
      const newLength = newChidlren.length
      const minLength = Math.min(oldLength, newLength)
      // 先对比相同长度的部分
      for (let i = 0; i <  minLength; i++) {
        patch(oldChildren[i], newChidlren[i])
      }
      // 如果新的比较长, 则mount新增的节点
      if (newLength > oldLength) {
        newChidlren.slice(minLength).forEach(item => {
          mount(item, el)
        })
      }
      // 如果旧的比较长, 则移除多余节点
      if (newLength < oldLength) {
        oldChildren.slice(minLength).forEach(item => {
          el.removeChild(item.el)
        })
      }
    }
  }
}
复制代码

Responsive system

When the data changes, everything that uses the data should also change

let obj = {
  name: 'lin'
}
const  change = () => {
  console.log('输出为:', obj.name)
}
change()
obj.name = 'jin'
// 当obj发生变化时,有使用到到obj的地方也会发生相应的改变
change()
复制代码

Define a dependency collection class

class Depend {
  constructor() {
    // Set对象允许你存储任何类型的唯一值,不会出现重复
    this.reactiveFns = new Set()
  }
  addDepend(reactiveFn) {
    this.reactiveFns.add(reactiveFn)
  }
  notify() {
    this.reactiveFns.forEach(item => {
      item()
    })
  }
}
let obj = {
  name: 'lin'
}
const change = () => {
  console.log('输出为:', obj.name)
}
const dep = new Depend()
dep.addDepend(change)
obj.name = 'jin'
dep.notify()
复制代码

Automatically monitor object changes

Every time the object changes, we have to call the notifymethod again, and we can use the proxy to monitor the changes of the object.

reactive function

Not every function needs to be made reactive, we can define a function to receive functions that need to be made reactive.

let activeReactiveFn = null
function watchFn(fn) {
  activeReactiveFn = fn
  fn()  // 调用一次触发get(看下面代码)
  activeReactiveFn = null
}

复制代码

Monitor object changes

Vue2 and Vue3 are implemented differently:

  • Can't monitor array changes
  • Each property of the object must be traversed
  • Nested objects must be deeply traversed
  • vue3 uses proxy to monitor object changes
  • For objects: For the entire object, not a property of the object, so there is no need to traverse the keys.
  • Support for arrays: Proxy does not need to overload the methods of arrays, which saves many hacks, reduces the amount of code, and reduces maintenance costs, and the standard is the best.
  • The second parameter of Proxy can have 13 interception methods, which is richer than Object.defineProperty()
  • Proxy, as a new standard, has been focused on and optimized by browser manufacturers. In contrast, Object.defineProperty() is an existing old method.

const reactive = (obj) => {
  let depend = new Depend()  
  // 返回一个proxy对象, 操作代理对象, 如果代理对象发生变化, 原对象也会发生变化
  return new Proxy(obj, {
    get: (target, key) => {
      // 收集依赖
      depend.addDepend()
      return Reflect.get(target, key)
    },
    set: (target, key, value) => {
      Reflect.set(target, key, value)
      // 当值发生改变时 触发
      depend.notify()
    }
  })
}
// 修改 Depend类中的addDepend方法
// addDepend() {
  // if (activeReactiveFn) {
  //  this.reactiveFns.push(activeReactiveFn)
  // }
// }
let obj = {
  name: 'lin'
}
let proxyObj = reactive(obj)
const foo = () => {
  console.log(proxyObj.name)
}
watchFn(foo)
proxyObj.name = 'jin'
复制代码

Documentation:

Correctly collect dependencies

image.pngWhenever we change the proxy object (vue2 object), for example we add a ageproperty, even if changeit is not used in the function age, we will trigger the changefunction. So we need to collect dependencies correctly, how to collect dependencies correctly.

  • Different objects are stored separately
  • Different properties of the same object should also be stored separately
  • To store objects we can use WeakMap

WeakMap An object is a collection of key/value pairs, where the keys are weak references (which can be garbage collected when the original object is destroyed). Its key must be 对象, and the value can be arbitrary.

Map Objects hold key-value pairs and are able to remember the original insertion order of keys. Any value (object or primitive ) can be used as a key or a value.

image.png

const targetMap = new WeakMap()
const getDepend = (target, key) => {
  // 根据target对象获取Map
  let desMap = targetMap.get(target)
  if (!desMap) {
    desMap = new Map()
    targetMap.set(target, desMap)
  }
  // 根据key获取 depend类
  let depend = desMap.get(key)
  if (!depend) {
    depend = new Depend()
    desMap.set(key, depend)
  }
  return depend
}

复制代码
const reactive = (obj) => {
  return new Proxy(obj, {
    get: (target, key) => {
      // 收集依赖
      const depend = getDepend(target, key)
      depend.addDepend()
      return Reflect.get(target, key)
    },
    set: (target, key, value) => {
      const depend = getDepend(target, key)
      Reflect.set(target, key, value)
      // 当值发生改变时 触发
      depend.notify()
    }
  })
}
复制代码

application entry module

Create a new html file and import all the js files where the created function is located

<script>
    // 创建根组件
    const App = {
       // 需要进行响应式的数据
      data: reactive({
        counter: 0
      }),
      render() {
        // h函数渲染节点
        return h("div", {class: 'lin'}, [
          h("div", {class: 'text'}, `${this.data.counter}`),
          h("button", {onClick: () => {
            this.data.counter++
            console.log(this.data.counter)
          }}, '+')
        ])
      }
    }
    // 挂载根组件
    const app = createApp(App)
    app.mount("#app")
  </script>
复制代码

Create a new js file save createAppfunction, this function returns an object, and there is a mountmethod in the object

const createApp = (rootComponent) => {
  return {
    mount(selector) {
      const container = document.querySelector(selector)
      // 响应式函数
      watchEffect(function() {
        const vNode = rootComponent.render()
        // 把节点挂载到 #div
        mount(vNode, container)
      })
    }
  }
}
复制代码

image.pngThere is a little problem with this, every time the +button is clicked, a new node will be added

First mount (mount) --> value change --> patch

const createApp = (rootComponent) => {
  return {
    mount(selector) {
      const container = document.querySelector(selector)
      // isMounted 是否已经挂载
      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
        }
      })
    }
  }
}
复制代码

The writing is very good, and I will come back to improve it when I become bald! ! !

Reference documentation

王红元 《深入vue3 + typescript》

Guess you like

Origin juejin.im/post/7080438171049132068