最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端进阶-手写Vue2.0源码(三)|技术点评

    正文概述 掘金(Big shark@LX)   2021-03-08   497

    前言

    今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈

    此篇主要手写 Vue2.0 源码-初始渲染原理

    上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue 生成虚拟 dom 的基础 模板编译最后转化成了 render 函数 之后又如何能生成真实的 dom 节点去替换掉 el 选项配置呢 那么通过此篇的学习就可以知道 Vue 初始渲染的流程 此篇主要包含虚拟 dom 以及真实 dom 的生成

    适用人群: 没时间去看官方源码或者看源码看的比较懵而不想去看的同学


    正文

    1.组件挂载入口

    // src/init.js
    
    Vue.prototype.$mount = function (el) {
      const vm = this;
      const options = vm.$options;
      el = document.querySelector(el);
    
      // 如果不存在render属性
      if (!options.render) {
        // 如果存在template属性
        let template = options.template;
    
        if (!template && el) {
          // 如果不存在render和template 但是存在el属性 直接将模板赋值到el所在的外层html结构(就是el本身 并不是父元素)
          template = el.outerHTML;
        }
    
        // 最终需要把tempalte模板转化成render函数
        if (template) {
          const render = compileToFunctions(template);
          options.render = render;
        }
      }
    
      // 将当前组件实例挂载到真实的el节点上面
      return mountComponent(vm, el);
    };
    

    接着看$mount 方法 我们主要关注最后一句话 mountComponent 就是组件实例挂载的入口函数 这个方法放在源码的 lifecycle 文件里面 代表了与生命周期相关 因为我们组件初始渲染前后对应有 beforeMount 和 mounted 生命周期钩子

    2.组件挂载核心方法 mountComponent

    // src/lifecycle.js
    export function mountComponent(vm, el) {
      // 上一步模板编译解析生成了render函数
      // 下一步就是执行vm._render()方法 调用生成的render函数 生成虚拟dom
      // 最后使用vm._update()方法把虚拟dom渲染到页面
    
      // 真实的el选项赋值给实例的$el属性 为之后虚拟dom产生的新的dom替换老的dom做铺垫
      vm.$el = el;
      //   _update和._render方法都是挂载在Vue原型的方法  类似_init
      vm._update(vm._render());
    }
    

    新建 lifecycle.js 文件 表示生命周期相关功能 核心导出 mountComponent 函数 主要使用 vm._update(vm._render())方法进行实例挂载

    3.render 函数转化成虚拟 dom 核心方法 _render

    // src/render.js
    
    import { createElement, createTextNode } from "./vdom/index";
    
    export function renderMixin(Vue) {
      Vue.prototype._render = function () {
        const vm = this;
        // 获取模板编译生成的render方法
        const { render } = vm.$options;
        // 生成vnode--虚拟dom
        const vnode = render.call(vm);
        return vnode;
      };
    
      // render函数里面有_c _v _s方法需要定义
      Vue.prototype._c = function (...args) {
        // 创建虚拟dom元素
        return createElement(...args);
      };
    
      Vue.prototype._v = function (text) {
        // 创建虚拟dom文本
        return createTextNode(text);
      };
      Vue.prototype._s = function (val) {
        // 如果模板里面的是一个对象  需要JSON.stringify
        return val == null
          ? ""
          : typeof val === "object"
          ? JSON.stringify(val)
          : val;
      };
    }
    

    主要在原型定义了_render 方法 然后执行了 render 函数 我们知道模板编译出来的 render 函数核心代码主要 return 了 类似于_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))这样的代码 那么我们还需要定义一下_c _v _s 这些函数才能最终转化成为虚拟 dom

    // src/vdom/index.js
    
    // 定义Vnode类
    export default class Vnode {
      constructor(tag, data, key, children, text) {
        this.tag = tag;
        this.data = data;
        this.key = key;
        this.children = children;
        this.text = text;
      }
    }
    
    // 创建元素vnode 等于render函数里面的 h=>h(App)
    export function createElement(tag, data = {}, ...children) {
      let key = data.key;
      return new Vnode(tag, data, key, children);
    }
    
    // 创建文本vnode
    export function createTextNode(text) {
      return new Vnode(undefined, undefined, undefined, undefined, text);
    }
    

    新建 vdom 文件夹 代表虚拟 dom 相关功能 定义 Vnode 类 以及 createElement 和 createTextNode 方法最后都返回 vnode

    4.虚拟 dom 转化成真实 dom 核心方法 _update

    // src/lifecycle.js
    
    import { patch } from "./vdom/patch";
    export function lifecycleMixin(Vue) {
      // 把_update挂载在Vue的原型
      Vue.prototype._update = function (vnode) {
        const vm = this;
        // patch是渲染vnode为真实dom核心
        patch(vm.$el, vnode);
      };
    }
    
    // src/vdom/patch.js
    
    // patch用来渲染和更新视图 今天只介绍初次渲染的逻辑
    export function patch(oldVnode, vnode) {
      // 判断传入的oldVnode是否是一个真实元素
      // 这里很关键  初次渲染 传入的vm.$el就是咱们传入的el选项  所以是真实dom
      // 如果不是初始渲染而是视图更新的时候  vm.$el就被替换成了更新之前的老的虚拟dom
      const isRealElement = oldVnode.nodeType;
      if (isRealElement) {
        // 这里是初次渲染的逻辑
        const oldElm = oldVnode;
        const parentElm = oldElm.parentNode;
        // 将虚拟dom转化成真实dom节点
        let el = createElm(vnode);
        // 插入到 老的el节点下一个节点的前面 就相当于插入到老的el节点的后面
        // 这里不直接使用父元素appendChild是为了不破坏替换的位置
        parentElm.insertBefore(el, oldElm.nextSibling);
        // 删除老的el节点
        parentElm.removeChild(oldVnode);
        return el;
      }
    }
    // 虚拟dom转成真实dom 就是调用原生方法生成dom树
    function createElm(vnode) {
      let { tag, data, key, children, text } = vnode;
      //   判断虚拟dom 是元素节点还是文本节点
      if (typeof tag === "string") {
        //   虚拟dom的el属性指向真实dom
        vnode.el = document.createElement(tag);
        // 解析虚拟dom属性
        updateProperties(vnode);
        // 如果有子节点就递归插入到父节点里面
        children.forEach((child) => {
          return vnode.el.appendChild(createElm(child));
        });
      } else {
        //   文本节点
        vnode.el = document.createTextNode(text);
      }
      return vnode.el;
    }
    
    // 解析vnode的data属性 映射到真实dom上
    function updateProperties(vnode) {
      let newProps = vnode.data || {};
      let el = vnode.el; //真实节点
      for (let key in newProps) {
        // style需要特殊处理下
        if (key === "style") {
          for (let styleName in newProps.style) {
            el.style[styleName] = newProps.style[styleName];
          }
        } else if (key === "class") {
          el.className = newProps.class;
        } else {
          // 给这个元素添加属性 值就是对应的值
          el.setAttribute(key, newProps[key]);
        }
      }
    }
    

    _update 核心方法就是 patch 初始渲染和后续更新都是共用这一个方法 只是传入的第一个参数不同 初始渲染总体思路就是根据虚拟 dom(vnode) 调用原生 js 方法创建真实 dom 节点并替换掉 el 选项的位置

    5._render 和_update 原型方法的混入

    // src/index.js
    
    import { initMixin } from "./init.js";
    import { lifecycleMixin } from "./lifecycle";
    import { renderMixin } from "./render";
    // Vue就是一个构造函数 通过new关键字进行实例化
    function Vue(options) {
      // 这里开始进行Vue初始化工作
      this._init(options);
    }
    // _init方法是挂载在Vue原型的方法 通过引入文件的方式进行原型挂载需要传入Vue
    // 此做法有利于代码分割
    initMixin(Vue);
    
    // 混入_render
    renderMixin(Vue);
    // 混入_update
    lifecycleMixin(Vue);
    export default Vue;
    

    最后就是把定义在原型的方法引入到 Vue 主文件入口 这样所有的实例都能共享方法了

    6.模板编译的思维导图

    前端进阶-手写Vue2.0源码(三)|技术点评

    小结

    至此 Vue 的初始渲染原理已经完结 结合前两篇响应式数据和模板编译 那么这时候我们已经可以把自己写好的模板渲染到页面了 大家可以看着思维导图自己动手写一遍核心代码哈 遇到不懂或者有争议的地方欢迎评论留言

    本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情


    起源地下载网 » 前端进阶-手写Vue2.0源码(三)|技术点评

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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