2021-12-31
Vue
00

目录

1. 前言
2.渲染系统实现
2.1 h函数的实现
2.2 mount函数的实现
2.3 patch函数的实现
2.4 演示
3. 响应式系统
4. createApp实现
5.mini-vue框架最终演示

1. 前言

从vue源码中可以看出vue主要包含下面三大核心模块:

  1. Compiler模块:编译模块 (将template中的模板编译成render渲染函数)
  2. Renderer模块:渲染模块 (将Compiler模块编译后的结果真正渲染到页面上)
  3. Reactivity模块:响应式模块 (当数据发生改变,响应变化)

image.png

本篇文章来实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:

  • 渲染系统模块 (runtime -> vnode -> 真实DOM)
  • 可响应式模块 (reactive)
  • 应用程序入口模块 (createApp)

2.渲染系统实现

该模块主要包含三个功能:

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

2.1 h函数的实现

  • h函数:h函数的作用是返回一个虚拟节点,通常缩写为 VNode接收三个参数:typeprops 和 children,虚拟节点组成虚拟DOM。

虚拟DOM是轻量级的 JavaScript 对象,由渲染函数创建。它包含三个参数:元素,具有数据、prop、attr 等的对象,以及一个数组。数组是我们传递子级的地方,子级也具有所有这些参数,然后它们也可以具有子级,依此类推,直到我们构建完整的元素树为止。

javascript
function h(type, props, children) { return { type, props, children } }

2.2 mount函数的实现

  • mount函数的作用是将虚拟节点挂载的页面上。它接收两个参数,第一个是需要挂载的vnode,第二个是被挂载的真实dom节点
javascript
function mount(vnode, container) { // 1. 创建出真实的元素,并且在vnode上保留el const el = vnode.el = document.createElement(vnode.type) // 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 if(vnode.children instanceof Array) { vnode.children.forEach(item => { mount(item, el) }) } else if (typeof vnode.children === 'object') { mount(vnode.children, el) } } // 4. 挂载到container上 container.appendChild(el) }

2.3 patch函数的实现

  • patch函数的作用是对比两个vnode,决定如何处理新的VNode。它接收两个参数,第一个是旧的vnode,第二个是新的vnode
javascript
function patch(n1, n2) { if(n1.type !== n2.type) { // 1. 标签名不相同 直接移除原来的元素 挂载新的vnode const n1ParentEl = n1.el.parentElement n1ParentEl.removeChild(n1.el) mount(n2, n1ParentEl) } else { // 2 标签名相同 const el = n2.el = n1.el // 2.1 处理props const oldProps = n1.props || {} const newProps = n2.props || {} // 添加新的属性 for(const key in newProps) { const oldValue = oldProps[key] const newValue = newProps[key] if(oldValue !== newValue) { if(key.startsWith('on')) { el.addEventListener(key.slice(2).toLocaleLowerCase(), oldValue) } else { el.setAttribute(key, newValue) } } } // 删除旧的属性 for(const key in oldProps) { const oldValue = oldProps[key] const newValue = newProps[key] if(oldValue !== newValue) { if(key.startsWith('on')) { el.removeEventListener(key.slice(2).toLocaleLowerCase(), value) } else { el.removeAttribute(key) } } } // 2.2 处理children const oldChildren = n1.children const newChildren = n2.children // 情况一:新的children是字符串 if(typeof newChildren === 'string') { if(newChildren !== oldChildren) { el.textContent = newChildren } } // 情况二:新的children是数组 else if (newChildren instanceof Array) { if(typeof oldChildren === 'string') { newChildren.forEach(item => { mount(item, el) }) } else { // 新旧都是数组(不考虑key的情况) const commonLength = Math.min(oldChildren.length, newChildren.length) for(let i = 0; i < commonLength; i++) { patch(oldChildren[i], newChildren[i]) } if(newChildren.length > oldChildren) { newChildren.slice(oldChildren.length).forEach(item => { mount(item, el) }) } if(newChildren.length < oldChildren) { oldChildren.slice(oldChildren.length).forEach(item => { el.removeChild(item.el) }) } } } } }

2.4 演示

测试代码

html
<div id="app"></div> <button id="btn">CHANGE</button> <!-- renderer.js 包含上述h、mount、patch函数 --> <script src="./renderer.js"></script> <script> // 1.通过h函数来创建一个vnode const vnode1 = h("div", { class: "coder" }, [ h("h2", null, "当前计数: 100"), h("button", null, "+1"), ]); // 2.通过mount函数,将vnode挂载到div#app上 mount(vnode1, document.getElementById("app")); // 3.创建新的vnode2 const vnode2 = h( "div", { class: "coder", style: "font-weight: 700; font-size: 30px;" }, [h("h3", null, "哈哈哈"), h("b", null, "嘿嘿嘿")] ); const btn = document.getElementById("btn"); btn.addEventListener("click", (e) => { patch(vnode1, vnode2); }); </script>

结果

动画.gif

3. 响应式系统

响应式我在我之前的一篇文章有讲解过 简单实现vue中的响应式系统

  • 下面贴一下代码
javascript
// 保存当前需要收集的响应式函数 let activeReactiveFn = null class Depend { constructor() { // 使用Set来保存依赖函数, 而不是数组[] this.reactiveFns = new Set() } notify() { this.reactiveFns.forEach(fn => { fn() }) } depend() { if(activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } } // WeakMap({key(对象): value}), key是个对象,弱引用(当将key设置为null时,key被垃圾回收机制回收,对应的value也会被回收) const targetMap = new WeakMap() // 封装一个获取depend函数 function getDepend(target, key) { // 根据target对象获取map的过程 let map = targetMap.get(target) if(!map) { map = new Map() targetMap.set(target, map) } // 根据key获取depend对象 let depend = map.get(key) if(!depend) { depend = new Depend() map.set(key, depend) } return depend } // 封装一个响应式的函数 function watchEffect(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { // 根据target.key获取对应的depend // 做依赖收集 const depend = getDepend(target, key) depend.depend() return Reflect.get(target, key, receiver) }, set(target, key, newValue , receiver) { Reflect.set(target, key, newValue, receiver) // 监听对象变化做出响应 const depend = getDepend(target, key) depend.notify() } }) }

4. createApp实现

  • createApp:要求传入一个根组件实例,并且需要提供一个mount方法挂载函数
javascript
function createApp(rootComponent) { return { mount(selector) { const container = document.querySelector(selector); let isMounted = false; let oldVNode = null; // 监听counter变化做页面的更新 watchEffect(function() { if (!isMounted) { // 第一次做mount操作 oldVNode = rootComponent.render(); mount(oldVNode, container); isMounted = true; } else { // 数据发生更新做patch操作 const newVNode = rootComponent.render(); patch(oldVNode, newVNode); oldVNode = newVNode; } }) } } }

5.mini-vue框架最终演示

  • 测试代码
html
<div id="app"></div> <script src="./renderer.js"></script> <script src="./reactive.js"></script> <script src="./createApp.js"></script> <script> const vnode1 = h("div", { class: "coder" }, [ h("h2", null, "当前计数: 100"), h("button", null, "+1"), ]); const App = { data: reactive({ counter: 0, }), render() { return h("div", { class: "coder" }, [ h("h2", null, `当前计数: ${this.data.counter}`), h("button",{ onClick: () => { this.data.counter--; }, },"-1" ), h("button",{ onClick: () => { this.data.counter++; }, },"+1" ), ]); }, }; const app = createApp(App); app.mount("#app");

mini-vue演示 (1).gif

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:叶继伟

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!