手写Vue2.0源码(二)初次渲染原理

247 阅读2分钟

前言

本文仅以记录自己的学习过程,有其他理解的同学可留言。注意 我学习原理一直保持 28 策略 所谓百分之 20 的代码实现了百分之 80 的功能 所以此系列咱们只关心核心逻辑以及功能的实现


正文

上篇文章介绍了Vue的响应式原理,Vue实例化过程中,在初始化数据之后,就进入渲染过程

入口

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

$mount(el)大约做了哪些事?

// 一:判断如果没有$options.render,没有则生成
// 二:mountComponent(vm, el),生成、挂载真实dom
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节点上面,这时候el为options.el查询到的真实dom,如果是更新渲染,那么el为上次的vnode
  return mountComponent(vm, el);
};

compileToFunctions(template)得到渲染函数render

// 这里不做太多分析,
// parser:使用正则拆分template,得到ast对象
// optimization:优化标记静态节点
// generate:生成render函数
  // 拼接字符串
  // with语法
  // new Function(字符串)

组件挂载核心方法 mountComponent

export function mountComponent(vm, el) {
  // 真实的el选项赋值给实例的$el属性 为之后虚拟dom产生的新的dom替换老的dom做铺垫
  vm.$el = el;
  //   _update和._render方法都是挂载在Vue原型的方法  类似_init
  vm._update(vm._render());
}

分析_render()函数;生成vnode

Vue.prototype._render = function () {
  const vm = this;
  const { render } = vm.$options;
  // 生成vnode--虚拟dom
  const vnode = render.call(vm);
  return vnode;
};
// 执行渲染函数,渲染函数根据最新的数据生成vnode
// 那渲染函数是什么样的? function(){return _c(标签,{属性,指令等}, 子节点)}
// 举一个最简单的例子,模板为<div id="contain"> <span>{{ name }}</span> </div>
// 编译后的渲染函数为
render() {
  return _c(
          'div',
          {data: {id: 'contain'}}, 
          [_c(
            'span',
            {},
            [name])
          ]
  )
}
// render函数里面有_c _v _t方法需要定义
// _c函数,生成元素节点
// _v函数,为生成文本节点
// _t函数,生成插槽节点,这块留到插槽那块讲解

// Vnode类
class Vnode {
  constructor(tag, data, key, children, text) {
    this.tag = tag;
    this.data = data;
    this.key = key;
    this.children = children;
    this.text = text;
    ...
  }
}
// 创建元素vnode
function createElement(tag, data = {}, ...children) {
  let key = data.key;
  return new Vnode(tag, data, key, children);
}
// 创建文本vnode
function createTextNode(text) {
  return new Vnode(undefined, undefined, undefined, undefined, text);
}
Vue.prototype._c = createElement;
Vue.prototype._v = createTextNode;

核心方法 _update();本文只介绍初次渲染过程

// 1 虚拟dom转换成真实dom
// 2 保存vnode
Vue.prototype._update = function (vnode) {
  const vm = this;
  // patch是渲染vnode为真实dom核心
  patch(vm.$el, vnode);
};
function patch(oldVnode, vnode) {
  // 初次渲染$el为options.el查询到的dom节点
  const isRealElement = oldVnode.nodeType;
  if (isRealElement) {
    // 这里是初次渲染的逻辑
    const oldElm = oldVnode;
    const parentElm = oldElm.parentNode;
    // 将虚拟dom转化成真实dom节点
    let el = createElm(vnode);
    parentElm.insertBefore(el, oldElm.nextSibling);
    // 删除老的el节点
    parentElm.removeChild(oldVnode);
    this.$el = el;
    return el;
  }
}
// 虚拟dom转成真实dom 就是调用原生方法生成dom树
  // 普通dom标签:
    // 创建dom
    // 更新属性 updateProperties(vnode)
    // 有子节点则递归创建,加入到父元素中
  // 组件标签例如tag为执行beforeMount,进入当前组件的子组件的初始化过程,在这个阶段当前已经执行beforeMount
    // 组件初始化后会挂载dom,也就是说在父把虚拟dom转化成真实dom的过程中渲染了子组件
    // 所以父子组件的生命周期也就是 父beforeCreate 父created 父beforeMount 子beforeCreate ... 子mounted 所有子都mounted 父mounted
function createElm(vnode) {
  let { tag, data, key, children, text } = vnode;
  if (typeof tag === "string") {
    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]);
    }
  }
}

思维导图

三:渲染$mount(el).png

如果觉得本文对你有帮助,记得点赞、收藏、评论,十分感谢!