9

手写Vue源码(三) - 组件初渲染

 3 years ago
source link: https://zhuanlan.zhihu.com/p/342356081
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

手写Vue源码(三) - 组件初渲染

Enjoy what you are doing!

源码地址: 传送门

Vue进行文本编译之后,会得到代码字符串生成的render函数。本文会基于render函数介绍以下内容:

  • 执行render函数生成虚拟节点
  • 通过vm._update方法,将虚拟节点渲染为真实DOM

vm.$mount方法中,文本编译完成后,要进行组件的挂载,代码如下:

Vue.prototype.$mount = function (el) {
  // text compile code ....
  mountComponent(vm);
};

// src/lifecycle.js
export function mountComponent (vm) {
  vm._update(vm._render());
}

下面详细介绍vm._render()vm._update()中到底做了什么

生成虚拟节点

原生DOM节点拥有大量的属性和方法,操作DOM比较耗费性能。在Vue中通过一个对象来描述DOM中的节点,这个对象就是虚拟节点,Vue组件树构建的整个虚拟节点树就是虚拟DOM

这是一段html

<div id="app">
  <span>hello world {{name}}</span>
</div>
<script>
  new Vue({
    el: '#app',
    data () {
      return {
        name: 'zs'
      }
    }
  })
</script>

其对应的虚拟节点如下:

const vNode = {
  tag: 'div',
  props: { id: 'app' },
  key: undefined,
  children: [
    {
      tag: 'span',
      props: {},
      key: undefined,
      children: undefined,
      text: 'helloworldzs'
    }
  ],
  text: undefined
}

Vue.prototype._render函数中,通过执行文本编译后生成的render方法,会得到虚拟节点:

// src/vdom/index.js
Vue.prototype._render = function () {
  const vm = this;
  // 执行选项中的render方法,指定this为Vue实例
  const { render } = vm.$options;
  return render.call(vm);
};

render函数中用到了_c,_v,_s这些方法,需要在Vue.prototype上添加这些方法,在render函数内就可以通过实例调用它们:

// 创建虚拟节点
function vNode (tag, props, key, children, text) {
  return {
    tag,
    props,
    key,
    children,
    text
  };
}

// 创建虚拟元素节点
function createVElement (tag, props = {}, ...children) {
  const { key } = props;
  delete props.key;
  return vNode(tag, props, key, children);
}

// 创建虚拟文本节点
function createTextVNode (text) {
  return vNode(undefined, undefined, undefined, undefined, text);
}

// 将实例中data里的值转换为字符串
function stringify (value) {
  if (value == null) {
    return '';
  } else if (typeof value === 'object') {
    return JSON.stringify(value);
  } else {
    return value;
  }
}

export function renderMixin (Vue) {
  Vue.prototype._c = createVElement;
  Vue.prototype._v = createTextVNode;
  Vue.prototype._s = stringify;
  // some code ...  
}

_render函数最终会递归的调用这些函数来得到虚拟节点,并将其返回:

const vNode = vm.createVElement('div', { id: 'app' },
  vm.createVElement('span', undefined,
    vm.createTextVNode('hello') + vm.createTextVNode('world') + vm.stringify(vm.name)
  )
)

在生成虚拟节点的过程中,会从组件实例vm中取值,从而触发对应属性的get/set方法。

将虚拟节点处理为真实节点

在通过Vue.prototype._render函数生成虚拟节点后,在Vue.prototype._update方法中会利用虚拟节点,替换当前页面上渲染的元素app

其代码如下:

// src/lifecycle.js
export function lifecycleMixin (Vue) {
  Vue.prototype._update = function (vNode) {
    const vm = this;
    patch(vm.$el, vNode);
  };
}

patch方法中,会通过虚拟节点创建真实节点,并将真实节点插入页面中:

// src/vdom/patch.js
export function patch (oldVNode, vNode) {
  // 将虚拟节点创建为真实节点,并插入到dom中
  const el = createElement(vNode);
  // 获取到老节点的父节点
  const parentNode = oldVNode.parentNode;
  // 将新节点插入到老节点之后
  parentNode.insertBefore(el, oldVNode.nextSibling);
  // 删除老节点
  parentNode.removeChild(oldVNode);
}

createElement中是用虚拟节点生成真实节点的逻辑:

  • 通过document.createElement来创建元素节点
  • 元素节点通过updateProperties方法来设置它的属性
  • 通过document.createTextNode来创建文本节点
function createElement (vNode) {
  if (typeof vNode.tag === 'string') {
    vNode.el = document.createElement(vNode.tag);
    updateProperties(vNode);
    for (let i = 0; i < vNode.children.length; i++) {
      const child = vNode.children[i];
      vNode.el.appendChild(createElement(child));
    }
  } else {
    vNode.el = document.createTextNode(vNode.text);
  }
  return vNode.el;
}

createElement会生成的真实DOM元素el并返回,内部会对子虚拟节点再次调用createElement来继续生成真实元素,然后将生成的真实元素通过appendChild方法插入到父节点中。

执行createElement最后得到的el是将所有子节点都插入到内部的元素,但其实el此时还是脱离真实DOM存在的,最后将它插入到真实DOM中便完成了整个真实节点的渲染。

下面是其执行逻辑示意图:

Vue的组件挂载vm.$mount(el)过程如下:

  1. template编译为render函数
  2. 使用render函数生成虚拟节点,函数中需要的变量和方法会去vm的自身和原型链中查找
  3. 将虚拟节点创建为真实节点,并递归的插入到页面中
  4. 使用真实节点替换之前老的节点

到目前为止,我们已经实现了Vue组件初渲染的整个过程,下面用一张图来总结一下:

v2-f5988b7ed43537cd7485ef5ff51c966e_720w.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK