最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [Vue官方教程笔记]- 尤雨溪手写mini-vue

    正文概述 掘金(全栈然叔)   2020-12-30   598

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

    ?这周我看了看了尤大神亲手写的mini版Vue3,笔记如下请大家指正。

    【原版视频】

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

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

    一、整体工作流程

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

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

    二、三大模块的分工

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

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

    1. 数据响应式模块

    提供创建一切数据变化都是可以被监听的响应式对象的方法。 [Vue官方教程笔记]- 尤雨溪手写mini-vue

    2. 编译模块

    [Vue官方教程笔记]- 尤雨溪手写mini-vue 将html模板编译为渲染函数

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

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

    3. 渲染函数

    渲染函数通过以下三个周期将视图渲染到页面上 [Vue官方教程笔记]- 尤雨溪手写mini-vue

    • Render Phase
    • Mount Phase
    • Patch Phase

    三、MVVM原型(Mock版)

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

    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. 程序骨架

    程序执行过程大概如图:

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

    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
    }
    

    四、数据响应实现

    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中的值,就会打印出来。

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

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

    当然我们需要的响应就是重新更新视图也就是重新运行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>
    
    

    五、 视图渲染过程

    1. 什么是Dom 、Document Object Model

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

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

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

    2. 什么是虚拟Dom

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

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

    3. 什么是渲染函数

    在Vue中我们通过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom [Vue官方教程笔记]- 尤雨溪手写mini-vue

    4. 通过DomDiff高效更新视图

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

    5. 总结

    举个栗子? 虚拟Dom和Dom就像大楼和大楼设计图之间的关系。 [Vue官方教程笔记]- 尤雨溪手写mini-vue 假设你要在29层添加一个厨房 ❌ 拆除整个29层,重新建设 ✅先绘制设计图,找出新旧结构不同然后建设

    六、实现渲染函数

    在Vue中我们通过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom [Vue官方教程笔记]- 尤雨溪手写mini-vue

    渲染流程通常会分为三各部分:

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

    • 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 - 高效更新视图

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

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

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

    七、编译器原理

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

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

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

    简单的说就是

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

    细分起来还可以分为三个个小步骤

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

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

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

    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

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

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

      • v-html

      • v-model

      • v-clock

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

    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】索取完整代码

    关注全栈然叔

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

    • ? 【Vue3官方教程】?万字笔记 | 同步导学视频 1050赞
    • ? 39岁的夺路狂奔| 掘金年度征文 100赞
    • ? Element3开发内幕 - Vue CLI插件开发 167赞
    • ? 天天造轮子系列 500+ 赞
    • ? Vue3.0全球发布会干货总结 267赞

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

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


    起源地下载网 » [Vue官方教程笔记]- 尤雨溪手写mini-vue

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元