vue初渲染流程

212 阅读3分钟

本篇讲述了vue实现初始化渲染过程的源码流程,因此不可避免的涉及到大量代码(事实上已经尽量剔除非关键代码),如果您对vue响应式原理还不是太了解,请先阅读vue响应式原理一文,本篇文章将按照初始化阶段->收集需要更新的组件阶段-->dom-diff更新组件dom阶段(只涉及初渲染对比)进行展开,同时跳过模版编译阶段,只关注最核心流程;模版编译的主要作用是构建模版引擎以及多端代码构建,由于vue中采用的是正则提取的方式解析模版,而不是采用更通用的有限状态机模式。因此建议您避免在模版编译阶段耗费过多时间。

1. 初始化阶段

new Vue传入option选项,之后会调用init方法,在这个方法里首先会把vue混入的选项与option选项进行合并,然后进行一系列的初始化操作,如初始化生命周期,初始化render函数 初始化数据,以及调用钩子函数等,其中最重要的是通过Object.defineProperty实现代理模式。

import {initMixin} from './init';

function Vue(options) {
    this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  vm._uid = uid++
  // merge options
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }

  // expose real self
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

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

初始化data,以及把data的属性直接挂载到vm上方便读取


function proxy(vm,source,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key];
        },
        set(newValue){
            vm[source][key] = newValue;
        }
    });
}
function initData(vm){
    let data = vm.$options.data;
    data = vm._data = typeof data === 'function' ? data.call(vm) : data;
    for(let key in data){ // 将_data上的属性全部代理给vm实例
        proxy(vm,'_data',key)
    }
    observe(data);
}

2. 收集需要更新的组件阶段

通过代理模式与观察者模式 在对象属性读取以及修改时收集依赖项以及通知依赖项

class Observer { // 观测值
    constructor(value){
        this.walk(value);
    }
    walk(data){ // 让对象上的所有属性依次进行观测
        let keys = Object.keys(data);
        for(let i = 0; i < keys.length; i++){
            let key = keys[i];
            let value = data[key];
            defineReactive(data,key,value);
        }
    }
}

class Dep{
    constructor(){
        this.id = id++;
        this.subs = [];
    }
    depend(){
        if(Dep.target){
            Dep.target.addDep(this);// 让watcher,去存放dep
        }
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
}

let dep = new Dep();

Object.defineProperty(data, key, {
    get() {
        if(Dep.target){ // 如果取值时有watcher
            dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
        }
        return value
    },
    set(newValue) {
        if (newValue == value) return;
        observe(newValue);
        value = newValue;
        dep.notify(); // 通知渲染watcher去更新
    }
});

export function observe(data) {
    if(typeof data !== 'object' || data == null){
        return;
    }
    return new Observer(data);
}

实现观察者watcher

let id = 0;
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if (typeof exprOrFn == 'function') {
            this.getter = exprOrFn;
        }
        this.cb = cb;
        this.options = options;
        this.id = id++;
        this.get();
    }
    get() {
        this.getter();
    }
}

export default Watcher;

3. 生成render函数阶段


初始化完成之后会检测当前实例是否为根组件,如果为根组件则调用vm.$mount(vm.$options.el)进行模版编译操作,最终的目标是生成虚拟dom,提供给dom-diff进行比对,更新dom.

vm.$mount是一个高阶函数,它首先会检测用户是否传入了render函数,如果用户没有传入,它会用来调用compileToFunctions使用数据和模版编译render函数,render函数的作用是生产虚拟dom。

react和vue都使用到了dom-diff ,之所以使用dom-diff是因为无法做到收集dom元素级的依赖,react中没有进行依赖收集,vue中收集依赖到组件级,这是因为收集dom级的依赖,会导致内存和计算性能大量浪费,因此他们都需要使用dom-diff来对比组件中变化了的dom元素,从而实现最小量更新

事实上虚拟dom并不比真实dom快,只是在解决前端手动操作dom的繁琐时 找到的性能相对较高的实现方法。参见 www.zhihu.com/question/31…

import {mountComponent} from './lifecycle'
Vue.prototype.$mount = function (el) {
    const vm = this;
    const options = vm.$options;
    el = document.querySelector(el);

    // 如果没有render方法
    if (!options.render) {
        let template = options.template;
        // 如果没有模板但是有el
        if (!template && el) {
            template = el.outerHTML;
        }
        const render= compileToFunctions(template);
        options.render = render;
    }
    mountComponent(vm,el);
}
function gen(node) {
    if (node.type == 1) {
        return generate(node);
    } else {
        let text = node.text
        if(!defaultTagRE.test(text)){
            return `_v(${JSON.stringify(text)})`
        }
        let lastIndex = defaultTagRE.lastIndex = 0
        let tokens = [];
        let match,index;

        while (match = defaultTagRE.exec(text)) {
            index = match.index;
            if(index > lastIndex){
                tokens.push(JSON.stringify(text.slice(lastIndex,index)));
            }
            tokens.push(`_s(${match[1].trim()})`)
            lastIndex = index + match[0].length;
        }
        if(lastIndex < text.length){
            tokens.push(JSON.stringify(text.slice(lastIndex)))
        }
        return `_v(${tokens.join('+')})`;
    }
}
function getChildren(el) { // 生成儿子节点
    const children = el.children;
    if (children) {
        return `${children.map(c=>gen(c)).join(',')}`
    } else {
        return false;
    }
}
function genProps(attrs){ // 生成属性
    let str = '';
    for(let i = 0; i<attrs.length; i++){
        let attr = attrs[i];
        if(attr.name === 'style'){
            let obj = {}
            attr.value.split(';').forEach(item=>{
                let [key,value] = item.split(':');
                obj[key] = value;
            })
            attr.value = obj;
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;
    }
    return `{${str.slice(0,-1)}}`;
}
function generate(el) {
    let children = getChildren(el);
    let code = `_c('${el.tag}',${
        el.attrs.length?`${genProps(el.attrs)}`:'undefined'
    }${
        children? `,${children}`:''
    })`;
    return code;
}

生成render函数

export function compileToFunctions(template) {
    parseHTML(template);
    let code = generate(root);
    let render = `with(this){return ${code}}`;
    let renderFn = new Function(render);
    return renderFn
}

生成虚拟dom

import {createTextNode,createElement} from './vdom/create-element'
export function renderMixin(Vue){
    Vue.prototype._v = function (text) { // 创建文本
        return createTextNode(text);
    }
    Vue.prototype._c = function () { // 创建元素
        return createElement(...arguments);
    }
    Vue.prototype._s = function (val) {
        return val == null? '' : (typeof val === 'object'?JSON.stringify(val):val);
    }
    Vue.prototype._render = function () {
        const vm = this;
        const {render} = vm.$options;
        let vnode = render.call(vm);
        return vnode;
    }
}

至此,我们实现了数据属性与组件更新逻辑的绑定,有了生成虚拟dom的render方法,我们来看下当如何把虚拟dom挂载到页面上

4. dom-diff更新组件dom阶段

mountCompoent函数也是一个高阶函数,通过patch方法 进行dom-diff比对,更新组件的dom节点

Vue.prototype._update = function(vnode) {
  const el = this.$el
  this.$el = patch(el,vnode)
}


function patch(oldVnode,vnode) { // oldVnode->el,vnode->render函数返回值
  const isRealElement = oldVnode.nodeType
  if(isRealElement) {
    const oldEle = oldVnode // el
    const parentEle  = oldEle.parentNode
    const el = createEle(vnode)
    parentEle.insertBefore(el,oldVnode)
    parentEle.removeChild(oldVnode)
    return el
  }else {
    // diff算法

  }
}

// 真正的渲染函数
function createEle(vnode) {
  // 如果是数字类型转化为字符串
  // 字符串类型直接就是文本节点
  if (vnode.tag == null) {
    return document.createTextNode(vnode.text);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => dom.appendChild(createEle(child)));
  return dom;
}



<!-- ```

回看`mountCompoent`方法

---

```js
function mountComponent(vm,el) {
  el = document.querySelector(el)
  vm.$el = el
  const updateComponent = ()=>{
    vm._update(vm._render())
  }
  new Watcher(vm,updateComponent,()=>{},true)
}