源码分析:new Vue() 数据如何渲染到页面,以超简单代码为例

237 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 数据驱动

Vue框架的一个核心理念就是数据驱动视图。何谓数据驱动视图?简单理解,就是在我们想改变浏览器视图的时候,仅仅通过修改数据就可以完成。这个过程,大大简化了传统的前端开发的代码量,从而开发过程中不用考虑DOM的修改,不需考虑复杂的DOM操作,只需要将逻辑重点关注在数据的改变即可。

那这是怎么实现的呢?其实主要可以分为两个场景来分析:

1.1 页面初次渲染的过程

比如如下代码:

<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

这些看似非常简单的代码是如何渲染到浏览器的页面上面的呢?

1.2 改变数据触发视图更新

<div id="app">
  {{ message }}
</div>
<button @click="handleClick"></button>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
      handleClick() {
          this.message = 'Hello huihui_new'
      }
  }
})

又比如说我们点击按钮改变了message的值,页面上立马更新了,这个过程又是如何发生的?

这两个过程如果清楚了,对vue的理解肯定会更深刻。

那么该如何分析这两个过程,那就开始读源码吧

2.new Vue() 的时候发生了什么

发生了什么?接下来我们以1.1中的代码来进行分析,new Vue(options)的时候到底发生了什么。

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

在src/core/instance的目录下,有下面一段代码:

import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

export default Vue;

可以看到,在这里定义了Vue构造函数,所以在外部我们必须使用new 的方法去实例化Vue,实例化Vue的过程中执行了_init(options)方法(options是我们实例化的时候传入的配置对象)。

init()方法是在initMixin(Vue)的过程中挂载到vue上面的,

export function initMixin(Vue: Class<Component>) {
  // 挂载到vue上
  Vue.prototype._init = function (options?: Object) {
    // vm是Vue
    const vm: Component = this;
    // a uid
    vm._uid = uid++;
    
    // 性能埋点相关,跳过
    let startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    // a flag to avoid this being observed
    // 这个属性标志避免被观察,这里不用关注
    vm._isVue = true;
    // merge options 合并配置 这里会初始化一些配置 后面会出文章分析
    if (options && options._isComponent) {
      // 组件的合并配置
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      // 非组件的合并配置
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm,
      );
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      initProxy(vm);
    } else {
      // Vue._renderProxy指向自身
      vm._renderProxy = 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");

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(`vue ${vm._name} init`, startTag, endTag);
    }
    
    // options对象里面有el 这里的el:'#app',
    if (vm.$options.el) {
      // 执行Vue.$mount('#app') 挂载
      vm.$mount(vm.$options.el);
    }
  };
}

这个过程中 主要做了几件事:

  1. 合并配置(可参考mergeOptions);
  2. 初始化生命周期、初始化事件中心、初始化渲染,初始化 data、props、computed、watcher 等;
  3. 挂载;

那data是如何渲染到页面呢,我们继续看源码,在上面第二步中执行了initState(vm),初始化了传入的数据配置,代码如下:

export function initState (vm: Component) {
  vm._watchers = []
  // 这里的opts 中包含 {el: '#app',data: { message: 'Hello Vue!' }}
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 这里有data,初始化data
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

这里面初始化了props、methods、data等,我们的例子中传入了数据data: { message: 'Hello Vue!' },则初始化数据,执行initData()方法;

function initData (vm: Component) {
  // 这里的data 为 { message: 'Hello Vue!' }
  let data = vm.$options.data
  // 组件中建议以data() { return {message: 'Hello Vue!'} }的方式来定义data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // 不是普通对象则报错
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  // 跟methods、props中的属性对比做校验,保证唯一性进行代理
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // 校验成功,且不是保留标签,进行代理
      proxy(vm, `_data`, key)
    }
  }
  // observe data 进行响应式操作
  observe(data, true /* asRootData */)
}

这里主要分为4步:

  1. 首先判断定义的是否是函数,是的话执行函数,获取到返回的data对象;
  2. 判断与定义的methods、props是否存在冲突;
  3. 代理数据属性:
// 代理后,组件内部可以通过this.key的方式来访问数据属性
proxy(vm, `_data`, key)

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

通过defineProperty这个API对vm._data.key 进行代理,这样就可以通过 vm.key的方式访问到di定义在data中的属性,方便用户使用

  1. 对数据进行监听 这儿的逻辑主要是对数据进行深度监听,从而实现响应式,这点我们之后再分析。深入响应式原理

到目前为止我们知道了new Vue() 是如何initData了,下一步继续分析data是如何挂载到页面上的!!! 点这里去往下一节我们继续分析