实现一个Mini-Vue可以加深对vue的理解 ,包括渲染系统模块、可响应式系统模块、 应用程序入口模块(没有实现编译系统模块) 。

vue核心模块

vue有三个核心模块 ,编译系统,渲染系统, 响应式系统。

简单Mini-Vue实现

编译系统

  1. 将 temp性能优化的方法late 模板解析算法的五个特性成抽象语法树(AST)。算法的时间复杂度取决于
  2. 对 AST 做优化处理。
  3. 根据 AST 生成 render 函数。

渲染系统

  1. reder函数返回vnode
  2. vnode之间算法是什么会形成树结构vdom
  3. 根据vdom生成真实dom,渲染到浏览器

响应式系统

  1. 将新旧vnode利用diff算法进行对比
  2. 渲染系统根据vnode重新生成dom,渲染到浏览器

渲染系统

简单Mini-Vue实现

通过h函数生成vnode

h函数包括3个参数,元素, 属性, 子元素。生html简单网页代码成的vnode是一个javascript对象

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')
])

文档: H函数

通过mount函HTML数, 将vnode挂载到div#app上

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)
}

简单Mini-Vue实现

patch对比新旧节点

当节点发生变化时, 对比新旧节点并进行更新。以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样(打补丁javascript是干什么的)

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)
        })
      }
    }
  }
}

响应式系统

当数据发生改变的时候,所有使用到数据的地方也应该发生改变

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

定义依赖收集类

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()

自动监听对象的变化

每次对象发生改变我们都要重新调用页面性能优化一次notify方法, 我们可以使算法导论用 proxy来监听对象的变化。

响应式函数

并不是每个函数都需要变成浏览器怎么打开网站响应式函数,我们javascript面试题可以定义一个函数来接收需要转化成响应式的函数。

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

监听对象的变化

vue2跟vue3实现方式不同:

  • vue2使用 Object.defineProperty() 劫持对象监听数据的变化
  • 不能监听数组的变化
  • 必须遍历对象的每个属性
  • 必须深层遍历嵌套的对象
  • vue3使用 proxy 监听对象浏览器历史记录设置的变算法的五个特性
  • 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。
  • 支持数组:Prox算法工程师y 不需要对数组的方法进行重载,省去了众多 hack,页面性能优化减少代码量等于减少了维护成本,而且标准的就是最好的。
  • Proxy 的第二个参数可javascript面试题以有 13 种拦截方法,这比起 Object浏览器的历史.defineProperty() 要更加丰富
  • Proxy 作为新标准受到浏览器厂商的性能优化的方法重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。

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'

文档:

  • Reflect

正确收集依赖

简单Mini-Vue实现
每当我们改变代理对象(vue2对象)的时候,比如我们新增一个age属性,即使chan算法的有穷性是指ge函数里面没有使用到age, 我们也会触发change函数。 所以我们要正确收集依赖,怎样正确收集依赖呢。

  • 不同算法导论的对象单独存储
  • 同一个对象不安卓性能优化同属性也要单独存储
  • 存储对象我们可以使用 WeakMap

WeakMap对象是一组算法分析的目的是键/值对的集合,其中的键是弱引用(原对象销毁的时候可以被垃圾回收javascript:void(0))的。其键必须是对象,而值可以是任意的。

  • 存储对象不同属性可以使用 Map

Map对象保存键浏览器的历史记录在哪值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一javascriptdownload个键或一个值。

简单Mini-Vue实现

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()
    }
  })
}

应用程序入口模块

新建一个html文件,把浏览器怎么打开网站创建浏览器的历史记录在哪的函数所在的js文件都引进来

<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>

新建一个js文件保存createApp函数,这个函数返回一个对象,对象里面有一个mount方法

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

简单Mini-Vue实现
这样汽车性能优化有一点问题,每次点击+按钮都js性能优化会新增节点

第一次挂载(mount) –> 值改变 –> 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
        }
      })
    }
  }
}

写的很菜,等我变秃了再回来改javascript:void(0)进改进!!!

参考文档

王红元 《深入vue3 + typescript》