[Vue官方教程笔记]- 尤雨溪手写mini-vue

上周发了 【Vue3官方教程】🎄万字笔记 | 同步导学视频 1050赞javascript

🔥这周我看了看了尤大神亲手写的mini版Vue3,笔记以下请你们指正。html

【原版视频】前端

image-20201230111207968

⚡️关注公众号【前端大班车】 回复 【mini-vue】索取完整代码

1、总体工做流程

Kapture 2020-12-10 at 16.13.53.gif

  1. 编译器将视图模板编译为渲染函数
  2. 数据响应模块将数据对象初始化为响应式数据对象
  3. 视图渲染
    1. RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
    2. MountPhase : 利用虚拟Dom建立视图页面Html
    3. PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,而后作Dom Diff更新视图Html

2、三大模块的分工

image.png

  • 数据响应式模块
  • 编译器
  • 渲染函数

1. 数据响应式模块

提供建立一切数据变化都是能够被监听的响应式对象的方法。 Kapture 2020-12-10 at 11.47.59.gifvue

2. 编译模块

image.png 将html模板编译为渲染函数java

这个编译过程能够在一下两个时刻执行node

  • 浏览器运行时 (runtime)
  • Vue项目打包编译时 (compile time)

3. 渲染函数

渲染函数经过如下三个周期将视图渲染到页面上 image.pngweb

  • Render Phase
  • Mount Phase
  • Patch Phase

3、MVVM原型(Mock版)

MVVM原理

MVVM框架其实就是在原先的View和Model之间增长了一个VM层完成如下工做。完成数据与视图的监听。咱们这一步先写一个Mock版本。其实就是先针对固定的视图和数据模型实现监听。数组

1. 接口定义

咱们MVVM的框架接口和Vue3如出一辙。浏览器

初始化须要肯定性能优化

  • 视图模板
  • 数据模型
  • 模型行为 - 好比咱们但愿click的时候数据模型的message会会倒序排列。
const App = {
  // 视图
  template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `,
  setup() {
    // 数据劫持
    const state = new Proxy(
      {
        message: "Hello Vue 3!!",
      },
      {
        set(target, key, value, receiver) {
          const ret = Reflect.set(target, key, value, receiver);
          // 触发函数响应
          effective();
          return ret;
        },
      }
    );

    const click = () => {
      state.message = state.message.split("").reverse().join("");
    };
    return { state, click };
  },
};
const { createApp } = Vue;
createApp(App).mount("#app");
复制代码

2. 程序骨架

程序执行过程大概如图:

render-proxy

const Vue = {
  createApp(config) {
    // 编译过程
    const compile = (template) => (content, dom) => {
      
    };

    // 生成渲染函数
    const render = compile(config.template);

    return {
      mount: function (container) {
        const dom = document.querySelector(container);
        
				// 实现setup函数
        const setupResult = config.setup();
				
        // 数据响应更新视图
        effective = () => render(setupResult, dom);
        render(setupResult, dom);
      },
    };
  },
};
复制代码

3. 编译渲染函数

MVVM框架中的渲染函数是会经过视图模板的编译创建的。

// 编译函数
// 输入值为视图模板
const compile = (template) => {
  //渲染函数
  return (observed, dom) => {
  	// 渲染过程
	}
}
复制代码

简单的说就是对视图模板进行解析并生成渲染函数。

大概要处理如下三件事

  • 肯定哪些值须要根据数据模型渲染

    // <button>{{message}}</button>
    // 将数据渲染到视图
    button = document.createElement('button')
    button.innerText = observed.message
    dom.appendChild(button)
    复制代码
  • 绑定模型事件

    // <button @click='click'>{{message}}</button>
    // 绑定模型事件
    button.addEventListener('click', () => {
      return config.methods.click.apply(observed)
    })
    复制代码
  • 肯定哪些输入项须要双向绑定

// <input v-model="message"/>
// 建立keyup事件监听输入项修改
input.addEventListener('keyup', function () {
  observed.message = this.value
})
复制代码

完整的代码

const compile = (template) => (observed, dom) => {

    // 从新渲染
    let input = dom.querySelector('input')
    if (!input) {
        input = document.createElement('input')
        input.setAttribute('value', observed.message)
      	
        input.addEventListener('keyup', function () {
            observed.message = this.value
        })
        dom.appendChild(input)
    }
    let button = dom.querySelector('button')
    if (!button) {
        console.log('create button')
        button = document.createElement('button')
        button.addEventListener('click', () => {
            return config.methods.click.apply(observed)
        })
        dom.appendChild(button)
    }
    button.innerText = observed.message
}
复制代码

4、数据响应实现

Vue广泛走的就是数据劫持方式。不一样的在于使用DefineProperty仍是Proxy。也就是一次一个属性劫持仍是一次劫持一个对象。固然后者比前者听着就明显有优点。这也就是Vue3的响应式原理。

Proxy/Reflect是在ES2015规范中加入的,Proxy能够更好的拦截对象行为,Reflect能够更优雅的操纵对象。 优点在于

  • 针对整个对象定制 而不是对象的某个属性,因此也就不须要对keys进行遍历。
  • 支持数组,这个DefineProperty不具有。这样就省去了重载数组方法这样的Hack过程。
  • Proxy 的第二个参数能够有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
  • Proxy 做为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法
  • 能够经过递归方便的进行对象嵌套。

说了这么多咱们先来一个小例子

var obj = new Proxy({}, {
    get: function (target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
        console.log(`setting ${key}!`);
        return Reflect.set(target, key, value, receiver);
    }
})
obj.abc = 132

复制代码

这样写若是你修改obj中的值,就会打印出来。

也就是说若是对象被修改就会得的被响应。

image-20200713122621925

固然咱们须要的响应就是从新更新视图也就是从新运行render方法。

首先制造一个抽象的数据响应函数

// 定义响应函数
let effective
observed = new Proxy(config.data(), {
  set(target, key, value, receiver) {
    const ret = Reflect.set(target, key, value, receiver)
    // 触发函数响应
    effective()
    return ret
  },
})
复制代码

在初始化的时候咱们设置响应动做为渲染视图

const dom = document.querySelector(container)
// 设置响应动做为渲染视图
effective = () => render(observed, dom)
render(observed, dom)
复制代码

1. 视图变化的监听

浏览器视图的变化,主要体如今对输入项变化的监听上,因此只须要经过绑定监听事件就能够了。

document.querySelector('input').addEventListener('keyup', function () {
  data.message = this.value
})
复制代码

2. 完整的代码

<html lang="en">
  <body> <div id="app"></div> <script> const Vue = { createApp(config) { // 编译过程 const compile = (template) => (content, dom) => { // 从新渲染 dom.innerText = ""; input = document.createElement("input"); input.addEventListener("keyup", function () { content.state.message = this.value; }); input.setAttribute("value", content.state.message); dom.appendChild(input); let button = dom.querySelector("button"); button = document.createElement("button"); button.addEventListener("click", () => { return content.click.apply(content.state); }); button.innerText = content.state.message; dom.appendChild(button); }; // 生成渲染函数 const render = compile(config.template); return { mount: function (container) { const dom = document.querySelector(container); const setupResult = config.setup(); effective = () => render(setupResult, dom); render(setupResult, dom); }, }; }, }; // 定义响应函数 let effective; const App = { // 视图 template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `, setup() { // 数据劫持 const state = new Proxy( { message: "Hello Vue 3!!", }, { set(target, key, value, receiver) { const ret = Reflect.set(target, key, value, receiver); // 触发函数响应 effective(); return ret; }, } ); const click = () => { state.message = state.message.split("").reverse().join(""); }; return { state, click }; }, }; const { createApp } = Vue; createApp(App).mount("#app"); </script> </body>
</html>

复制代码

5、 视图渲染过程

Dom => virtual DOM => render functions

1. 什么是Dom 、Document Object Model

image.png

HTML在浏览器中会映射为一些列节点,方便咱们去调用。

image.png

2. 什么是虚拟Dom

Dom中节点众多,直接查询和更新Dom性能较差。

A way of representing the actual DOM with JavaScript Objects. 用JS对象从新表示实际的Dom

image.png

3. 什么是渲染函数

在Vue中咱们经过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom image.png

4. 经过DomDiff高效更新视图

image.png

5. 总结

举个栗子🌰 虚拟Dom和Dom就像大楼和大楼设计图之间的关系。 image.png 假设你要在29层添加一个厨房 ❌ 拆除整个29层,从新建设 ✅先绘制设计图,找出新旧结构不一样而后建设

6、实现渲染函数

在Vue中咱们经过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom image.png

渲染流程一般会分为三各部分:

vue-next-template-explorer.netlify.app/

  • RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
  • MountPhase : 利用虚拟Dom建立视图页面Html
  • PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,而后作Dom Diff更新视图Html
mount: function (container) {
    const dom = document.querySelector(container);
    const setupResult = config.setup();
    const render = config.render(setupResult);

    let isMounted = false;
    let prevSubTree;
    watchEffect(() => {
      if (!isMounted) {
        dom.innerHTML = "";
        // mount
        isMounted = true;
        const subTree = config.render(setupResult);
        prevSubTree = subTree;
        mountElement(subTree, dom);
      } else {
        // update
        const subTree = config.render(setupResult);
        diff(prevSubTree, subTree);
        prevSubTree = subTree;
      }
    });
  },
复制代码

1.Render Phase

渲染模块使用渲染函数根据初始化数据生成虚拟Dom

render(content) {
  return h("div", null, [
    h("div", null, String(content.state.message)),
    h(
      "button",
      {
        onClick: content.click,
      },
      "click"
    ),
  ]);
},
复制代码

2. Mount Phase

利用虚拟Dom建立视图页面Html

function mountElement(vnode, container) {
  // 渲染成真实的 dom 节点
  const el = (vnode.el = createElement(vnode.type));

  // 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const val = vnode.props[key];
      patchProp(vnode.el, key, null, val);
    }
  }

  // 要处理 children
  if (Array.isArray(vnode.children)) {
    vnode.children.forEach((v) => {
      mountElement(v, el);
    });
  } else {
    insert(createText(vnode.children), el);
  }

  // 插入到视图内
  insert(el, container);
}

复制代码

3. Patch Phase(Dom diff)

数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,而后作Dom Diff更新视图Html

function patchProp(el, key, prevValue, nextValue) {
  // onClick
  // 1. 若是前面2个值是 on 的话
  // 2. 就认为它是一个事件
  // 3. on 后面的就是对应的事件名
  if (key.startsWith("on")) {
    const eventName = key.slice(2).toLocaleLowerCase();
    el.addEventListener(eventName, nextValue);
  } else {
    if (nextValue === null) {
      el.removeAttribute(key, nextValue);
    } else {
      el.setAttribute(key, nextValue);
    }
  }
}
复制代码

经过DomDiff - 高效更新视图

image.png

image-20201230104838657

function diff(v1, v2) {
  // 1. 若是 tag 都不同的话,直接替换
  // 2. 若是 tag 同样的话
  // 1. 要检测 props 哪些有变化
  // 2. 要检测 children -》 特别复杂的
  const { props: oldProps, children: oldChildren = [] } = v1;
  const { props: newProps, children: newChildren = [] } = v2;
  if (v1.tag !== v2.tag) {
    v1.replaceWith(createElement(v2.tag));
  } else {
    const el = (v2.el = v1.el);
    // 对比 props
    // 1. 新的节点不等于老节点的值 -> 直接赋值
    // 2. 把老节点里面新节点不存在的 key 都删除掉
    if (newProps) {
      Object.keys(newProps).forEach((key) => {
        if (newProps[key] !== oldProps[key]) {
          patchProp(el, key, oldProps[key], newProps[key]);
        }
      });

      // 遍历老节点 -》 新节点里面没有的话,那么都删除掉
      Object.keys(oldProps).forEach((key) => {
        if (!newProps[key]) {
          patchProp(el, key, oldProps[key], null);
        }
      });
    }
    // 对比 children

    // newChildren -> string
    // oldChildren -> string oldChildren -> array

    // newChildren -> array
    // oldChildren -> string oldChildren -> array
    if (typeof newChildren === "string") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          setText(el, newChildren);
        }
      } else if (Array.isArray(oldChildren)) {
        // 把以前的元素都替换掉
        v1.el.textContent = newChildren;
      }
    } else if (Array.isArray(newChildren)) {
      if (typeof oldChildren === "string") {
        // 清空以前的数据
        n1.el.innerHTML = "";
        // 把全部的 children mount 出来
        newChildren.forEach((vnode) => {
          mountElement(vnode, el);
        });
      } else if (Array.isArray(oldChildren)) {
        // a, b, c, d, e -> new
        // a1,b1,c1,d1 -> old
        // 若是 new 的多的话,那么建立一个新的

        // a, b, c -> new
        // a1,b1,c1,d1 -> old
        // 若是 old 的多的话,那么把多的都删除掉
        const length = Math.min(newChildren.length, oldChildren.length);
        for (let i = 0; i < length; i++) {
          const oldVnode = oldChildren[i];
          const newVnode = newChildren[i];
          // 能够十分复杂
          diff(oldVnode, newVnode);
        }

        if (oldChildren.length > length) {
          // 说明老的节点多
          // 都删除掉
          for (let i = length; i < oldChildren.length; i++) {
            remove(oldChildren[i], el);
          }
        } else if (newChildren.length > length) {
          // 说明 new 的节点多
          // 那么须要建立对应的节点
          for (let i = length; i < newChildren.length; i++) {
            mountElement(newChildren[i], el);
          }
        }
      }
    }
  }
}
复制代码

7、编译器原理

这个地方尤大神并无实现 后续然叔会给你们提供一个超简洁的版本 这个章节咱们主要看看compile这个功能。

compiler

上文已经说过编译函数的功能

// 编译函数
// 输入值为视图模板
const compile = (template) => {
  //渲染函数
  return (observed, dom) => {
  	// 渲染过程
	}
}
复制代码

简单的说就是

  • 输入:视图模板
  • 输出:渲染函数

细分起来还能够分为三个个小步骤

Snip20200713_17

  • Parse 模板字符串 -> AST(Abstract Syntax Treee)抽象语法树

  • Transform 转换标记 譬如 v-bind v-if v-for的转换

  • Generate AST -> 渲染函数

    // 模板字符串 -> AST(Abstract Syntax Treee)抽象语法树
    let ast = parse(template)
    // 转换处理 譬如 v-bind v-if v-for的转换
    ast = transfer(ast)
    // AST -> 渲染函数
    return generator(ast)
    复制代码

    咱们能够经过在线版的VueTemplateExplorer感觉一下

    vue-next-template-explorer.netlify.com/

image-20200713150630150

编译函数解析

1. Parse解析器

解析器的工做原理其实就是一连串的正则匹配。

好比:

标签属性的匹配

  • class="title"

  • class='title'

  • class=title

const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)=("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)/

"class=abc".match(attr);
// output
(6) ["class=abc", "class", "abc", undefined, undefined, "abc", index: 0, input: "class=abc", groups: undefined]

"class='abc'".match(attr);
// output
(6) ["class='abc'", "class", "'abc'", undefined, "abc", undefined, index: 0, input: "class='abc'", groups: undefined]

复制代码

这个等实现的时候再仔细讲。能够参考一下文章。

AST解析器实战

那对于咱们的项目来说就能够写成这个样子

// <input v-model="message"/>
// <button @click='click'>{{message}}</button>
// 转换后的AST语法树
const parse = template => ({
    children: [{
            tag: 'input',
            props: {
                name: 'v-model',
                exp: {
                    content: 'message'
                },
            },
        },
        {
            tag: 'button',
            props: {
                name: '@click',
                exp: {
                    content: 'message'
                },
            },
            content:'{{message}}'
        }
    ],
})
复制代码

2. Transform转换处理

前一段知识作的是抽象语法树,对于Vue3模板的特别转换就是在这里进行。

好比:vFor、vOn

在Vue三种也会细致的分为两个层级进行处理

  • compile-core 核心编译逻辑

    • AST-Parser

    • 基础类型解析 v-for 、v-on

      image-20200713183256931

  • compile-dom 针对浏览器的编译逻辑

    • v-html

    • v-model

    • v-clock

      image-20200713183210079

const transfer = ast => ({
    children: [{
            tag: 'input',
            props: {
                name: 'model',
                exp: {
                    content: 'message'
                },
            },
        },
        {
            tag: 'button',
            props: {
                name: 'click',
                exp: {
                    content: 'message'
                },
            },
            children: [{
                content: {
                    content: 'message'
                },
            }]
        }
    ],
})
复制代码

3. Generate生成渲染器

生成器其实就是根据转换后的AST语法树生成渲染函数。固然针对相同的语法树你能够渲染成不一样结果。好比button你但愿渲染成 button仍是一个svg的方块就看你的喜欢了。这个就叫作自定义渲染器。这里咱们先简单写一个固定的Dom的渲染器占位。到后面实现的时候我在展开处理。

const generator = ast => (observed, dom) => {
    // 从新渲染
    let input = dom.querySelector('input')
    if (!input) {
        input = document.createElement('input')
        input.setAttribute('value', observed.message)
        input.addEventListener('keyup', function () {
            observed.message = this.value
        })
        dom.appendChild(input)
    }
    let button = dom.querySelector('button')
    if (!button) {
        console.log('create button')
        button = document.createElement('button')
        button.addEventListener('click', () => {
            return config.methods.click.apply(observed)
        })
        dom.appendChild(button)
    }
    button.innerText = observed.message
}

复制代码

🔥关注公众号【前端大班车】 回复 【mini-vue】索取完整代码

关注全栈然叔

近期文章(感谢掘友的鼓励与支持🌹🌹🌹)

欢迎拍砖,一块儿探讨更优雅的实现

相关文章
相关标签/搜索