Vue源码解析

此文项目代码:https://github.com/bei-yang/I-want-to-be-an-architect
码字不易,辛苦点个star,感谢!

引言


此篇文章主要涉及以下内容:

  1. vue工作机制
  2. vue响应式的原理
  3. 依赖收集与追踪
  4. 编译compile

为什么要懂原理


编程世界和武侠世界是比较像的,每一个入门的程序员,都幻想自己有朝一日,神功大成,青衣长剑,救民于水火,但其实大部分人一开始的学习方式就错了,导致一直无法进入到高手的行列,究其原因,就是过于看中招式、武器,而忽略了内功的修炼,所以任你慕容复有琅环玉洞的百家武学,还是被我乔峰一招制敌,这就是内功差距。

武学之道,切勿贪多嚼不烂,博而不精不如一招鲜吃遍天,编程亦是如此。

源码,就是内力修炼的捷径。

Vue工作机制


初始化

new Vue()之后,Vue会调用进行初始化,会初始化生命周期、事件、propsmethodsdatacomputedwatch等。其中最重要的是通过Object.defineProperty设置settergetter,用来实现响应式依赖收集

因为浏览器的瓶颈是在页面渲染方面,vue的核心思想是减少页面渲染的次数及数量。

初始化之后调用$mount挂载组件。

vue工作机制

简化版

编译

编译模块分为三个阶段,主要是解析和生成两个阶段,优化阶段是次要的。

  1. parse (解析)
  • 使用正则解析template中的vue的指令(v-xxx)变量等非正常HTML的内容,形成语法树AST
  1. optimize(优化)
  • 标记一些静态节点,用作后面的性能优化,在diff的时候直接略过。
  1. generate(生成)
  • 把第一部生成的AST转化为渲染函数render function

响应式

响应式是vue最核心的内容。
gettersetter看稍后的代码演示,初始化的时候通过defineProperty进行绑定,设置通知的机制,当编译生成的渲染函数被实际渲染的时候,会触发getter进行依赖收集,在数据变化的时候,触发setter进行更新。

虚拟dom

Virtual DOMreact首创,Vue2开始支持,就是用JavaScript对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js的对比很快,而真实的dom操作太慢。

// vdom
{
  tag:'div',
  props:{
    name:'虚拟dom的名字',
    style:{color:red},
    onClick:xxx
  },
  children:[
    {
      tag:'a',
      text:'click me'
    }
  ]
}
// js
<div name="虚拟dom的名字" style="color:red" @click="xxx">
  <a>
    click me
  </a>
</div>

更新视图

数据修改触发setter,然后监听器会通知进行修改,通过对比两个dom数,得到改变的地方,就是patch,然后只需要把这些差异修改即可。

接下来是实战部分:

Vue2响应式的原理:defineProperty


数据绑定的原理:vue利用es5defineProperty这个属性,将data里面的数据每个都定义了一个settergetter,这样我们就可以监听属性的变化,当属性变化的时候,我们就可以通知那些需要更新的地方进行更新。

// 以下仅实现了数据绑定部分,响应到组件部分见后面解析
class LVue{
  constructor(options){
    this.$options=options;
    //数据响应化
    this.$data=options.data;
    this.observe(this.$data);
  }
  observe(value){
    // 对传参进行判断
    if(!value||typeof value!=="object"){
      return;
    }
    // 遍历该对象
    Object.keys(value).forEach(key=>{
      this.defineReactive(value,key,value[key]);
    });
  }
  // 数据响应化
  defineReactive(obj,key,val){
    this.observe(val); // 递归解决数据嵌套
    Object.defineProperty(obj,key,{
      enumerable:true, // 属性可枚举
      configurable:true, // 属性可被修改或删除
      get(){
        return val;
      },
      set(newVal){
        if(newVal===val) return;
        val=newVal;
        console.log(`${key}属性更新了:${val}`);
      }
    })
  }
}

let o=new LVue({
  data:{
    test:"I am test"
  }
});
o.$data.test="changed test"

defineProperty使用方法

依赖收集与追踪


简述图
new Vue({
  template:
    `<div>
        <span>{{text1}}</span>
        <span>{{text2}}</span>
     </div>`,
  data:{
    text1:'name1'
  },
  created(){
    this.text1='changed text1'
  }
})

text1被修改,所以视图更新,但是text2视图没用到,所以不需要更新,就需要我们的依赖收集。

// 依赖收集类Dep,用来管理watcher
class Dep{
  constructor(){
    // 存储所有的依赖(watcher),一个watcher对应一个属性text1 or text2
    this.deps=[]
  }
  // 在deps中添加一个监听器(watcher)对象
  addDep(dep){
    this.deps.push(dep)
  }
  // 通知所有监听器(watcher)去更新视图
  notify(){
    this.deps.forEach((dep)=>{
      dep.update()
    })
  }
}
// Watcher:实现前面的update方法
class Watcher{
  constructor(){
    // 在new一个监听器对象时将该对象赋值给Dep.target,在get中会用到
    // 将当前watcher实例指定到Dep静态属性target
    Dep.target=this
  }
  // 更新视图的方法
  update(){
    console.log('视图更新啦...')
  }
}

我们增加了一个Dep类的对象,用来收集Watcher对象。读数据的时候,会触发reactiveGettter函数把当前的Watcher对象(存放在Dep.target中)收集到Dep类中去。
写数据的时候,则会触发reactiveSetter方法,通知Dep类调用notify来触发所有watcher对象的update方法更新对应视图。

// 和前面响应式原理一起整合的代码
class LVue{
  constructor(options){
    this.$options=options;
    //数据响应化
    this.$data=options.data;
    this.observe(this.$data);

    // 模拟一下watcher观察者对象,这时候Dep.target会指向这个watcher对象
    new Watcher();
    // 在这里模拟render的过程,为了触发test属性的get函数
    console.log('模拟render,触发test的getter',this.$data.test);
  }
  observe(value){
    // 对传参进行判断
    if(!value||typeof value!=="object"){
      return;
    }
    // 遍历该对象
    Object.keys(value).forEach(key=>{
      this.defineReactive(value,key,value[key]);
    });
  }
  // 数据响应化
  defineReactive(obj,key,val){
    this.observe(val); // 递归解决数据嵌套
    
    const dep=new Dep();

    Object.defineProperty(obj,key,{
      enumerable:true, // 属性可枚举
      configurable:true, // 属性可被修改或删除
      get(){

        // 将Dep.target,即当前的watcher对象存入Dep的deps中
        Dep.target&&dep.addDep(Dep.target);

        return val;
      },
      set(newVal){
        if(newVal===val) return;

        // 在set的时候触发dep的notify来通知所有的watcher对象更新视图
        dep.notify()

        // val=newVal;
        // console.log(`${key}属性更新了:${val}`);
      }
    })
  }
}

编译compile


核心逻辑:获取dom,遍历dom,获取{{}}、k-和@开头的,设置响应式。

简述图(图中的K-理解为V-即可)

目标功能

// 目标功能
<body>
  <div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
    <p>
      {{doubleAge}}
    </p>
    <input type="text" k-model="name">
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
  </div>
  <script src='./compile.js'></script>
  <script src='./k-vue.js'></script>
  
  <script>
    let k=new LVue({
      el:'#app',
      data:{
        name:'i am test',
        age:12,
        html:'<button>这是一个按钮</button>'
      },
      created(){
        console.log(‘开始啦’)
        setTimeout(()=>{
          this.name='我是蜗牛'
        },16)
      },
      methods:{
        changeName(){
          this.name='changed name',
          this.age=1,
          this.id='xxx'
          console.log(1,this)
        }
      }
    })
  </script>
</body>

compile.js

// 用法 new Compile(el, vm)

class Compile {
  constructor(el, vm) {
    // 要遍历的宿主节点
    this.$el = document.querySelector(el);

    this.$vm = vm;

    // 编译
    if (this.$el) {
      // 转换内部内容为片段Fragment
      this.$fragment = this.node2Fragment(this.$el);
      // 执行编译
      this.compile(this.$fragment);
      // 将编译完的html结果追加至$el
      this.$el.appendChild(this.$fragment);
    }
  }

  // 将宿主元素中代码片段拿出来遍历,这样做比较高效
  node2Fragment(el) {
    const frag = document.createDocumentFragment();
    // 将el中所有子元素搬家至frag中
    let child;
    while ((child === el.firstChild)) {
      frag.appendChild(child);
    }
    return frag;
  }
  // 编译过程
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 类型判断
      if (this.isElement(node)) {
        // 元素
        // console.log('编译元素'+node.nodeName);
        // 查找k-,@,:
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr => {
          const attrName = attr.name; //属性名
          const exp = attr.value; // 属性值
          if (this.isDirective(attrName)) {
            // k-text
            const dir = attrName.substring(2);
            // 执行指令
            this[dir] && this[dir](node, this.$vm, exp);
          }
          if (this.isEvent(attrName)) {
            const dir = attrName.substring(1); // @click
            this.eventHandler(node, this.$vm, exp, dir);
          }
        });
      } else if (this.isInterpolation(node)) {
        // 文本
        // console.log('编译文本'+node.textContent);
        this.compileText(node);
      }

      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    });
  }

  compileText(node) {
    // console.log(RegExp.$1);
    this.update(node, this.$vm, RegExp.$1, "text");
  }

  // 更新函数
  update(node, vm, exp, dir) {
    const updaterFn = this[dir + "Updater"];
    // 初始化
    updaterFn && updaterFn(node, vm[exp]);
    // 依赖收集
    new Watcher(vm, exp, function(value) {
      updaterFn && updaterFn(node, value);
    });
  }

  text(node, vm, exp) {
    this.update(node, vm, exp, "text");
  }

  //   双绑
  model(node, vm, exp) {
    // 指定input的value属性
    this.update(node, vm, exp, "model");

    // 视图对模型响应
    node.addEventListener("input", e => {
      vm[exp] = e.target.value;
    });
  }

  modelUpdater(node, value) {
    node.value = value;
  }

  html(node, vm, exp) {
    this.update(node, vm, exp, "html");
  }

  htmlUpdater(node, value) {
    node.innerHTML = value;
  }

  textUpdater(node, value) {
    node.textContent = value;
  }

  //   事件处理器
  eventHandler(node, vm, exp, dir) {
    //   @click="onClick"
    let fn = vm.$options.methods && vm.$options.methods[exp];
    if (dir && fn) {
      node.addEventListener(dir, fn.bind(vm));
    }
  }

  isDirective(attr) {
    return attr.indexOf("k-") == 0;
  }
  isEvent(attr) {
    return attr.indexOf("@") == 0;
  }
  isElement(node) {
    return node.nodeType === 1;
  }
  // 插值文本
  isInterpolation(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
}

入口文件

class LVue{
  constructor(options){
    this.$data=options.data
    this.$options=options
    this.observer(this.$data)
    // 新建一个watcher观察者对象,这时候Dep.target会指向这个watcher对象
    // new Watcher()
    // 在这里模拟render的过程,为了触发test属性的get函数
    console.log('模拟render,触发test的getter',this.$data)
    if(options.created){
      options.created.call(this)
    }
    this.$compile=new Compile(options.el,this)
  }
  obserber(value){
    if(!value||(typeof value!=='object')){
      return
    }
    Object.keys(value).forEach((key)=>{
      this.proxyData(key)
      this.defineReactive(value,key,value[key])
    })
  }
  defineReactive(obj,key,val){
    const dep=new Dep()
    Object.defineProperty(obj,key,{
      enumerable:true,
      configurable:true,
      get(){
        // 将Dep.target(即当前的watcher对象存入Dep的deps中
        Dep.target&&dep.addDep(Dep.target)
        return val
      },
      set(newVal){
        if(newVal===val) return
        val=newVal
        // 在set的时候触发dep的notify来通知所有的watcher对象更新视图
        dep.notify()
      }
    })
  }
  proxyData(key){
    Object.defineProperty(this,key,{
      configurable:true,  //   可配置
      enumerable:true,  //  可枚举
      get(){
        return this.$data[key]
      },
      set(newVal){
        this.$data[key]=newVal
      }
    })
  }
}

依赖收集Dep

class Dep{
  constructor(){
    // 存数所有的依赖
    this.deps=[]
  }
  // 在deps中添加一个监听器对象
  addDep(dep){
    this.deps.push(dep)
  }
  depend(){
    Dep.target.addDep(this)
  }
  // 通知所有监听器去更新视图
  notify(){
    this.deps.forEach((dep)=>{
      dep.update()
    })
  }
}

监听器

// 监听器
class watcher{
  constructor(vm,key,cb){
    // 在new一个监听器对象时将该对象赋值给Dep.target,在get中用到
    // 将Dep.target指向自己
    // 然后触发属性的getter添加监听
    // 最后将Dep.target置空
    this.cb=cb
    this.vm=vm
    this.key=key
    this.value=this.get()
  }
  get(){
    Dep.target=this
    let value=this.vm[this.key]
    return value
  }
  // 更新视图的方法
  update(){
    this.value=this.get()
    this.cb.call(this.vm,this.value)
  }
}

总结


  • vue编译过程是怎样的?
    首先编译是因为vue写的语句HTML不识别,可以进行依赖收集,模型和视图有依赖关系,后面模型发生变化可通知依赖的视图发生更新,然后模型推进视图的变化,这就是编译。
  • 双向绑定的原理是什么?
    vue利用es5defineProperty这个属性,将data里面的数据每个都定义了一个settergetter,这样我们就可以监听属性的变化,当属性变化的时候,我们就可以通知那些需要更新的地方进行更新。

你的赞是我前进的动力

求赞,求评论,求分享...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容