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

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

六、实现渲染函数


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


网络异常,图片无法展示
|


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


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


网络异常,图片无法展示
|


网络异常,图片无法展示
|


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


七、编译器原理


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


网络异常,图片无法展示
|


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


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


简单的说就是


  • 输入:视图模板


  • 输出:渲染函数


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


网络异常,图片无法展示
|


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


网络异常,图片无法展示
|


编译函数解析


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


网络异常,图片无法展示
|


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


  • v-html


  • v-model


  • v-clock


网络异常,图片无法展示
|


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
}


相关文章
|
1天前
|
存储 JavaScript 前端开发
【Vue】绝了!这生命周期流程真...
【Vue】绝了!这生命周期流程真...
|
1天前
|
JavaScript 索引
【vue】框架搭建
【vue】框架搭建
6 1
|
1天前
|
JavaScript 前端开发 容器
< 每日小技巧: 基于Vue状态的过渡动画 - Transition 和 TransitionGroup>
Vue 的 `Transition` 和 `TransitionGroup` 是用于状态变化过渡和动画的组件。`Transition` 适用于单一元素或组件的进入和离开动画,而 `TransitionGroup` 用于 v-for 列表元素的增删改动画,支持 CSS 过渡和 JS 钩子。
< 每日小技巧: 基于Vue状态的过渡动画 - Transition 和 TransitionGroup>
|
1天前
|
JavaScript
【vue】setInterval的嵌套实例
【vue】setInterval的嵌套实例
5 1
|
1天前
|
JavaScript 前端开发 安全
【Vue】内置指令真的很常用!
【Vue】内置指令真的很常用!
|
1天前
|
JavaScript
【Vue】过滤器Filters
【Vue】过滤器Filters
|
1天前
|
存储 JavaScript
Vue的状态管理:Vuex的使用和最佳实践
【4月更文挑战第24天】Vue状态管理库Vuex用于集中管理组件状态,包括State(全局状态)、Getters(计算属性)、Mutations(同步状态变更)和Actions(异步操作)。Vuex还支持Modules,用于拆分大型状态树。使用Vuex时,需安装并创建Store,定义状态、getter、mutation和action,然后在Vue实例中注入Store。遵循最佳实践,如保持状态树简洁、使用常量定义Mutation类型、避免直接修改状态、在Actions中处理异步操作、合理划分Modules,以及利用Vuex提供的插件和工具,能提升Vue应用的稳定性和可维护性。
|
1天前
|
资源调度 JavaScript 前端开发
Vue的路由管理:VueRouter的配置和使用
【4月更文挑战第24天】VueRouter是Vue.js的官方路由管理器,用于在单页面应用中管理URL路径与组件的映射。通过安装并引入VueRouter,设置路由规则和创建router实例,可以实现不同路径下显示不同组件。主要组件包括:`&lt;router-link&gt;`用于创建导航链接,`&lt;router-view&gt;`负责渲染当前路由对应的组件。此外,VueRouter还支持编程式导航和各种高级特性,如嵌套路由、路由参数和守卫,以应对复杂路由场景。
|
1天前
|
JavaScript 前端开发 开发者
Vue的响应式原理:深入探索Vue的响应式系统与依赖追踪
【4月更文挑战第24天】Vue的响应式原理通过JavaScript getter/setter实现,当数据变化时自动更新视图。它创建Watcher对象收集依赖,并通过依赖追踪机制精确通知更新。当属性改变,setter触发更新相关Watcher,重新执行操作以反映数据最新状态。Vue的响应式系统结合依赖追踪,有效提高性能,简化复杂应用的开发,但对某些复杂数据结构需额外处理。
|
2天前
|
缓存 JavaScript
【vue】如何搭建拦截器和设置路由守卫(基于token认证)
【vue】如何搭建拦截器和设置路由守卫(基于token认证)
16 0