意外产出——Mini_Vue

59 阅读3分钟

原本是想详细了解Vue的响应式原理,结果一不小心就写出了一个Mini_Vue

一、 主要工作流程

  • 编译器将视图模板编译为渲染函数

  • 数据响应模块将数据对象初始化为响应式数据对象

  • 视图渲染

    1. RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
    2. MountPhase : 利用虚拟Dom创建视图页面Html
    3. PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html

二、 模块各自分工

1. 数据响应式模块 ——reactive函数

提供创建一切数据变化都是可以被监听的响应式对象的方法

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

      depend() {
            if (activeEffect) {
                  this.subscribers.add(activeEffect)
            }
      }

      notify() {
            this.subscribers.forEach(effect => {
                  effect()
            })
      }
}
//实现自动添加effect
let activeEffect = null
function watchEffect(effect) {
      activeEffect = effect
      //初次执行一次effect
      effect()
      activeEffect = null
}

//创建weakMap以raw对象为属性名保存数据,例如info、foo对象
//WeakMap :({key(对象): value}) key是一个对象,并且WeakMap是弱引用 ,可以在垃圾回收时被回收掉
//Map: ({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)//如果没有depsMap则创建一个depsMap
            //2. 取出具体的dep对象
      }
      let dep = depsMap.get(key)
      if (!dep) {
            dep = new Dep()
            depsMap.set(key, dep)
      }
      return dep
}


//Vue2方式做数据劫持,实现自动进行依赖添加,以及通知依赖发生改变,并且执行effect函数
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) {
                        value = newValue
                        dep.notify()
                  }
            })
      })
      return raw
}

//Vue3方式做数据劫持
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: 'christina' })
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);
})
watchEffect(function () {
      console.log('effect3:', info.counter + 100, info.name);
})
watchEffect(function () {
      console.log('effect4:', foo.height);
})


// info.counter++;
// info.name = 'pp'
foo.height = '2.13'

2. 编译模块——挂载

将html模板编译为渲染函数

这个编译过程可以在一下两个时刻执行

  • 浏览器运行时 (runtime)
  • Vue项目打包编译时 (compile time)
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
                        }
                  })
            }
      }
}

3. 渲染函数

渲染函数通过以下三个周期将视图渲染到页面上

const h = (tag, props, children) => {
      return {
            tag,
            props,
            children
      }
}
const mount = (vnode, container) => {
      //1.创建真实的dom节点
      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).toLocaleLowerCase(), 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挂载到containers上
      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中进行
            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)) {
                        el.removeAttribute(key)
                  }
            }
            //3. 处理children
            const oldChildren = n1.children || []
            const newChildren = n2.children || []
            //情况一: 如果children类型是字符串
            if (typeof newChildren === 'string') {
                  if (typeof oldChildren === 'string') {
                        el.textContent = newChildren
                  } else {
                        el.innerHTML = newChildren
                  }
            } else {//情况二,有数组类型,并且新的children是数组,但是旧的是string类型
                  if (typeof oldChildren === 'string') {
                        el.innerHTML = ''
                        newChildren.forEach(item => {
                              mount(item, el)
                        })
                  } else {
                        //新旧children都是数组类型,前面有相同的节点直接进行patch操作
                        const commonLength = Math.min(oldChildren.length, newChildren.length)
                        for (let i = 0; i < commonLength; i++) {
                              patch(oldChildren[i], newChildren[i])
                        }
                        //新的children长度大余旧的children的长度,或者小于旧的children的长度
                        if (newChildren.length > oldChildren.length) {
                              newChildren.slice(oldChildren).forEach(item => {
                                    mount(item, el)
                              })
                        } else {
                              oldChildren.slice(newChildren.length).forEach(item => {
                                    el.removeChild(item.el)
                              })
                        }

                  }

            }


      }
}

代码实践

目录结构

image.png

测试代码

image.png