资讯专栏INFORMATION COLUMN

(解析)vue源码解读

mingde / 747人阅读

摘要:前言为什么要做源码解读我们新到一个环境,第一件事情就是熟悉环境熟悉项目,这个很考验阅读源码的能力以及耐心。构造函数拉到最下面,导出的是一个构造函数。

前言

A: 为什么要做源码解读?
Q: 我们新到一个环境,第一件事情就是熟悉环境熟悉项目,这个很考验阅读源码的能力以及耐心。vue是个很好的库,知名度高,对js的学习具有向上性,所以搞清楚逻辑是有好处的。
A: 阅读源码的程度?
Q: 我们完全没必要从头到尾细细品味,只需要知道一些核心的实现就好了,毕竟vue也算是个产品,我们没必要搞清楚一个产品,我们只需要知道产品的核心就够了,多余的也是业务代码。(相对来说)

开始 Vue应用的实例化以及挂载 main.js
new Vue({                      // 初始化vue实例
  //components: { App }        // vue1.0的写法
  render: h => h(App)          // 最先执行,返回一个符合component的对象,vue2.0的写法
})
.$mount("#app")                // 挂载vue实例到 ‘#app’
render函数
render: h => h(App) 
就是
render:function(h){
    return h(App)
}
即
render: function (createElement) {
    return createElement(App)
}
找到Vue引用源文件(debug顺序)
import Vue from "vue"

找到

node-modules/vue

打开package.json 找到

"main": "dist/vue.runtime.common.js"

"main"是 npm模块曝光的主要文件.

打开vue.runtime.common.js
ctrl/command + a, ctrl/command + k, ctrl/command + 1
,快捷键把所有方法折叠

你会发现 update执行了2次,我明明只初始化了一次vue实例,为什么update2次了呢?原因在下方表明。

构造函数

拉到最下面,

module.exports = Vue;

导出的是一个Vue构造函数。
当前文件搜索 Vue 找到 构造函数

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); // 调用 初始化方法
}
初始化函数
var uid$3 = 0;
vm._uid = uid$3++; //每个vue实例 拥有唯一id,从0开始 ++

// 合并初始化vue实例的参数(入参options和默认参数)
vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );

initLifecycle();// 初始化实例生命周期相关的参数
使用Object.create(null)用来获取一个没有原型链的对象类型

initEvents(); // 初始化实例事件触发相关的参数
initRender(); // 初始化实例渲染相关的参数

//create的准备工作做好了,触发beforeCreate的生命周期
callHook(vm, "beforeCreate");

initState(); // 初始化实例状态

// 状态也初始化好了,触发create的生命周期,所以 create和 breforCreate的区别就在 create的时候  有状态。
callHook(vm, "created");

至此,vue实例的初始化完成,然后挂载到节点

挂载实例到节点
// 将mount("#app") => query("#app") 查找到dom对象,赋值给vue.$el
// 触发beforeMount的生命周期,所以beforeMount 和 create的区别就在  beforeMount的时候 有挂载节点。
callHook(vm, "beforeMount");

// 拿到当前将要挂载的Vnode(虚拟dom对象)
vm._render()  vm.$vnode

// (更新)渲染页面
vm._update(vm._render(), hydrating);
vm.__patch__(); 
createElm(); // 按照虚拟dom生成真实dom
createElm函数
// 这是一个递归方法,vue实例的初始化是创建一个根节点,然后再将render函数传入的组件挂载,这就是流程图执行2次update的原因。
// 如果是组件,则去做 组件的初始化
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}
// 已经删减,只留主要逻辑,判断虚拟dom 的 tag属性
if (isDef(tag)) {
  {
    // 遍历虚拟dom的子节点,并且创建,然后递归当前方法。
    createChildren(vnode, children, insertedVnodeQueue); 
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    insert(parentElm, vnode.elm, refElm);
  }
}
// 如果没有子节点的,直接创建dom,然后插入
 else if (isTrue(vnode.isComment)) {
  vnode.elm = nodeOps.createComment(vnode.text);
  insert(parentElm, vnode.elm, refElm);
} else {
  vnode.elm = nodeOps.createTextNode(vnode.text);
  insert(parentElm, vnode.elm, refElm);
}
// 触发Mount的生命周期
callHook(vm, "mounted");

至此就是 Vue根节点初始化挂载和渲染的流程.

Vue数据更新

首先 我们改造下 app.vue,像官网一样,我们新增一个双向绑定的文本框。
现在我们知道了,第一次的update只是挂载了根节点,那么我们新增了文本框的组件其实是在第二次init的时候初始化的。
我们可以着重看第二次的流程,搞清楚,数据的监听与更新。

initState函数

在每次vue实例初始化的时候 都会执行initState,这里面做了vue实例 数据的监听。

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {   // 这里判断vm.$options.data,从而执行initData()或者 直接 监听 vm._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);
  }
}

vm.$options.data 哪里来的呢?
是在 Vue._init方法中

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(     // 这个方法中  给data赋值,也就是我们render中 的  data(){return{//我们组件的数据}}
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
initData函数
function initData (vm) {
  var data = vm.$options.data;
  // 把从render函数返回的data函数对象赋值给data,然后data.call(this,this),也就是vm.data();拿到data返回值
  data = vm._data = typeof data === "function"      
    ? getData(data, vm)
    : data || {};
  // observe data
  observe(data, true /* asRootData */);
}
function getData (data, vm) {
  pushTarget();
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, "data()");
    return {}
  } finally {
    popTarget();
  }
}

function observe (value, asRootData) {
  var ob;
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);   // 返回一个 新建的 Observer
  }
  return ob
}
Observer对象(监听者)
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep(); // 这里的dep 稍后会说
  this.vmCount = 0;
  def(value, "__ob__", this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value); 
  } else {
    this.walk(value);
  }
};
// 如果是对象,则按照key来劫持
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
};

// 如果是数组,就遍历每个数组元素,再每个元素再判断是否为数组,对象
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};
数据劫持
function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  var dep = new Dep(); // 这里的dep 稍后会说
    
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  if (!getter && arguments.length === 2) {
    val = obj[key];
  }
  var setter = property && property.set;

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend(); // 这里的dep 稍后会说
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify(); // 这里的dep 稍后会说
    }
  });
}
依赖收集

我们在使用vue的时候,data方法会返回对象,包含了所有我们想要观察的数据属性,同样 vue也会帮我们监听这些属性的变化,但是,假如我们在data中设置了多个属性,但是在模板中只使用了1个,又会如何呢?我们在脚本中设置value2的值(this.value3= "hello world"),那么vue监听到变化 还会去通知模板重新渲染么?

new Vue({
    template: 
        `
value1: {{value1}}
`, data: { value1: "value1", value2: "value2", value3: "value3", ... } });

自然是不会的,vue很聪明的使用了依赖收集
Dep : 一个订阅者的容器,可以增加或删除订阅者,可以向订阅者发送消息;
Watcher : 订阅者类。它在初始化时可以接受getter, callback两个函数作为参数。getter用来计算Watcher对象的值。当Watcher被触发时,会重新通过getter计算当前Watcher的值,如果值改变,则会执行callback.

Dep(订阅者容器) Watcher对象(订阅者) 小结

未完待续。。。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98862.html

相关文章

  • 入口文件开始,分析Vue源码实现

    摘要:一方面是因为想要克服自己的惰性,另一方面也是想重新温故一遍。一共分成了个基础部分,后续还会继续记录。文章中如果有笔误或者不正确的解释,也欢迎批评指正,共同进步。最后地址部分源码 Why? 网上现有的Vue源码解析文章一搜一大批,但是为什么我还要去做这样的事情呢?因为觉得纸上得来终觉浅,绝知此事要躬行。 然后平时的项目也主要是Vue,在使用Vue的过程中,也对其一些约定产生了一些疑问,可...

    Karrdy 评论0 收藏0
  • 入口文件开始,分析Vue源码实现

    摘要:一方面是因为想要克服自己的惰性,另一方面也是想重新温故一遍。一共分成了个基础部分,后续还会继续记录。文章中如果有笔误或者不正确的解释,也欢迎批评指正,共同进步。最后地址部分源码 Why? 网上现有的Vue源码解析文章一搜一大批,但是为什么我还要去做这样的事情呢?因为觉得纸上得来终觉浅,绝知此事要躬行。 然后平时的项目也主要是Vue,在使用Vue的过程中,也对其一些约定产生了一些疑问,可...

    nidaye 评论0 收藏0
  • vue源码解读-目录结构

    摘要:目录结构构建相关的文件,一般情况下我们不需要动钩子别名配置 目录结构 ├── scripts ------------------------------- 构建相关的文件,一般情况下我们不需要动│ ├── git-hooks ------------------------- git钩子│ ├── alias.js -------------------------- 别名配...

    philadelphia 评论0 收藏0
  • Vue 2.0】核心源码解读 -- 不定期更新

    摘要:观察员由模板解析指令创建的观察员负责模板中的更新视图操作。观察员种类目前了解情况来看主要分三类视图指令的计算属性的用户自定义的 介绍 关于 Vue.js 的原理一直以来都是一个话题。经过几天的源码学习和资料介绍,我将一些个人理解的经验给写下来,希望能够与大家共勉。 附上GITHUB源码地址, 如果有任何不解 可以在 文章下面提出或者写下issue, 方便大家回答和学习, 有兴趣可以St...

    sunsmell 评论0 收藏0
  • 前方来报,八月最新资讯--关于vue2&3的最佳文章推荐

    摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...

    izhuhaodev 评论0 收藏0

发表评论

0条评论

mingde

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<