【 web高级 01vue 】 vue直播课04 Vue源码剖析01

作业


知识点


目标

  • 环境搭建
  • 理顺学习流程
  • vue初始化过程
  • 深入数据响应式

获取vue

github地址

文件结构

在这里插入图片描述

源码目录-src

在这里插入图片描述

调试环境搭建

npm i
npm run dev

输出文件

在这里插入图片描述

术语解释:

  • runtime:仅包含运行时,不包含编译器 — 不能使用template这个配置项去写字符串的模板
  • common:cjs规范打包 — cjs commonjs,用于webpack1版本
  • esm:ES模块,用于webpack2+
  • umd:universal module definition,兼容cjs和amd,用于浏览器 – vue.js

入口

"dev": "rollup -w -c scripts/config.js  --sourcemap  --environment TARGET:web-full-dev"

dev脚本中 -c scripts/config.js 指明配置文件所在
参数 TARGET:web-full-dev 指明输出文件配置项,line123

在这里插入图片描述

 // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'), //入口
    dest: resolve('dist/vue.js'), //目标文件
    format: 'umd', //输出规范
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

在这里插入图片描述
在这里插入图片描述

一、初始化流程

src\platforms\web\entry-runtime-with-compiler.js

入口文件,覆盖$mount,执行模板解析和编译工作

测试代码

01-init.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue源码剖析</title>
  <script src="../../dist/vue.min.js"></script>
</head>

<body>
  <div id="demo">
    <h1>初始化流程</h1>
    <p>{{foo}}</p>
  </div>
  <script>
  
    //创建实例
    const app = new Vue({
      el: "#demo",
      template: '<div>template<div>',
      render(h) {
        return h('div', 'render')
      },
      data: {
        foo: 'foo'
      }
    });

  </script>
</body>

</html>

el: "#demo",
template: '<div>template<div>',
render(h) {
 return h('div', 'render')
},

el,template,render谁的优先级最高,谁会起作用

在这里插入图片描述

render > remplate > el
在这里插入图片描述

el

const app = new Vue({
	el: "#demo",
	data: {
		foo: 'foo'
	}
});

或者 

new Vue({
	
}).$mount("#demo")

在这里插入图片描述

template

const app = new Vue({
	template: '<div>template<div>',
	data: {
		foo: 'foo'
	}
});

在这里插入图片描述
是有问题的!
使用template,必须明文调用 $mount('#demo')

const app = new Vue({
	template: '<div>template<div>',
	data: {
		foo: 'foo'
	}
}).$mount('#demo');

在这里插入图片描述

源码
//+ 保存原来的$mount
const mount = Vue.prototype.$mount
//+ 覆盖默认$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)


  //+ 解析option
  const options = this.$options


  //+ 1.先判断render
  if (!options.render) {
    let template = options.template
    //+ 2. template 模板解析 
    if (template) {
      ......
    //+ 3. el
    } else if (el) {
      template = getOuterHTML(el)
    }
    
    //+ 如果存在模板,执行编译
    if (template) {
     
      //+ 得到渲染函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render 
      options.staticRenderFns = staticRenderFns

      
    }
  }
  //+ 执行挂载 
  return mount.call(this, el, hydrating)
}

得到render函数

const app = new Vue({
	el: "#demo",
	data: {
		foo: 'foo'
	}
});
console.log(app.$options.render); // options.render = render 

在这里插入图片描述

src\platforms\web\runtime\index.js

定义$mount

在这里插入图片描述

源码


//1+ 指定补丁方法:传入的虚拟dom转换为真实dom
//+ 1.初始化,2.更新
Vue.prototype.__patch__ = inBrowser ? patch : noop


//+ 实现$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  //+ 初始化,将首次渲染结果替换el
  return mountComponent(this, el, hydrating)
}

src\core\index.js

定义全局API

在这里插入图片描述

源码

//+  定义全局API
initGlobalAPI(Vue)

src\core\instance\index.js

定义构造函数

定义实例方法

在这里插入图片描述

源码

//+ 构造函数
function Vue (options) {
  ......
  //+ 初始化
  this._init(options)
}

initMixin(Vue)  //通过该方法给Vue添加_init方法  实现iinit函数
2.定义实例方法
stateMixin(Vue) //状态相关api $set,$delete,$watch
eventsMixin(Vue) //事件相关api $emit,$on,$off,$once
lifecycleMixin(Vue) //生命周期api  _update(),$forceupdate(),$destory()
renderMixin(Vue)  //渲染api _render(),$nextTick

src\core\instance\init.js

初始化方法_init定义的地方

在这里插入图片描述

源码

Vue.prototype._init = function (options?: Object) {
   ......

   //+ 合并选项
   if (options && options._isComponent) {
     
     initInternalComponent(vm, options)
   } else {
     vm.$options = mergeOptions(
       resolveConstructorOptions(vm.constructor),
       options || {},
       vm
     )
   }
   
   ......
   
   //+ 重点

   initLifecycle(vm) //声明 $parent,$root,$children,$refs
   initEvents(vm) //对父组件传入的事件添加监听
   initRender(vm) //声明$slots,$scopedSlots,$createElement  插槽,虚拟dom的生成
   callHook(vm, 'beforeCreate') //调用beforeCreate钩子
   initInjections(vm)  //注入数据
   initState(vm)   //重要:数据的初始化,响应式
   initProvide(vm) // 提供数据
   callHook(vm, 'created') //

   ......
}

mountComponent src\core\instance\lifecycle.js

执行挂载,获取vdom并转换为dom

执行创建的时候是:上–>下
执行挂载的时候是:下–>上

render() src\core\instance\render.js

渲染组件,获取dom

update() src\core\instance\lifecycle.js

执行更新,将传入vdom转换为dom,初始化执行的是dom创建操作

整体流程

new Vue()-----------------调用init
this._init(options)--------初始化各种属性
$mount--------------------调用mountComponent
mountComponent()-----声明updateComponent、创建watcher
_render()------------------获取虚拟dom
_update()-----------------把虚拟dom转为真实dom

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

const app = new Vue({
	el: "#demo",
	data: {
		foo: 'foo'
	}
});

写el不需要$mount
if (vm.$options.el) {
	vm.$mount(vm.$options.el)
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

vue1+,页面中有一个绑定就有一个watcher–上节课讲的
vue2+中,一个组件只有一个watcher

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、数据响应化

Vue一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲,是利用了JS语言特性Object.defineProperty(),通过定义对象属性setter方法拦截对象属性变更,从而将数值的变化转换为UI的变化。

具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始化

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

入口:src\core\instance\state.js

initState 初始化数据,包括props、methods、data、computed、watch

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  //+ 属性初始化
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  //+ 数据响应式
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true )
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initData 核心代码是将data数据响应化

function initData (vm: Component) {
  let data = vm.$options.data
  //+ 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
  //+ 代理这些数据到实例上
  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 */)
}

src\core\observer\index.js

observe() 返回一个Observer实例

源码
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }

  //+ 观察者,已经存在直接返回,不否则创建新实例
  let ob: Observer | void
  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)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer 对象根据数据类型执行对应的响应化操作

源码
//+ 每一个响应式的对象,都会有一个ob
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    //+ 为什么在Observer里面声明一个dep
    //+ object里面新增或者删除属性
    //+ array中有变更方法
    //+ 只要这些方法被调用,
    //+ 都要通过dep去通知
    this.dep = new Dep()
    this.vmCount = 0
    //+ 设置下划线__ob__属性,应用当前Observer实例
    def(value, '__ob__', this)
    //+ 判断类型
    if (Array.isArray(value)) {
      //+ 替换数组对象原型
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {  
        //+ 没有原型,强硬覆盖
        copyAugment(value, arrayMethods, arrayKeys)
      }
      //+ 如果数组里面的元素是对象,还需要做响应化处理
      this.observeArray(value)
    } else {
      //+ 对象直接处理
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  //+ 循环所有的key
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  //+ 遍历所有数组的key,对每个项执行响应化
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive 定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新

源码
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //+ dep和key一一对应
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

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


  //+ 核心代码   属性拦截,只要是对象类型,均会返回childOb
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //+ 获取key对应的值
      const value = getter ? getter.call(obj) : val
      //+ 如果存在依赖
      if (Dep.target) {
        //+ 收集依赖
        dep.depend()
        //+ 如果存在子ob,子ob也收集这个依赖
        if (childOb) {
          childOb.dep.depend()
          //+ 如果是数组
          if (Array.isArray(value)) {
            dependArray(value) //+ 收集数组中的每一个选项
          }
        }
      }
      //+ 只要在界面中用到,每层都会收集
      //+ 在界面中没用,他也不会收集
      return value
    },
    set: function reactiveSetter (newVal) {
      const 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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //+ 如果新值是对象,也要做响应化
      childOb = !shallow && observe(newVal)
      //+ 通知更新
      dep.notify()
    }
  })
}

src\core\observer\dep.js

Dep负责管理一组Watcher,包括watcher实例的增删及通知更新

源码
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

src\core\observer\watcher.js

Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。

每个组件也会有对应Watcher,数值变化会触发其update函数导致重新渲染

源码
export default class Watcher {
  ......

  constructor () {
   ......
  }

  get () {
   ......
  }

 
  addDep (dep: Dep) {
   ......
  }

 
  update () {
   ......
  }

  ......

}

测试代码

相关API:$watch

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue源码剖析</title>
  <script src="../../dist/vue.js"></script>
  <style>
    body {
      background: #abcdef;
    }

  </style>
</head>

<body>
  <div id="demo">
    <h1>数据响应化</h1>
    <p>{{obj.foo}}</p>


  </div>
  <script>
    //创建实例
    const app = new Vue({
      el: "#demo",
      data: {
        obj: {  
            //+ obj是一个对象,有一个ob实例,通过obj上面挂载的dep去通知,只要界面中涉及obj,把和obj相关的组件实例和watcher关联起来
            //+ 因为当前组件实例和watcher是一对一对应的,在ob里面将watcher提交更新

            //+ dep将订阅的watcher保存下来,便于后面通知更新

            //+ 会触发setter方法,告诉watcher有依赖发生了变化,wathcer收到依赖变化的消息,重新渲染虚拟dom,实现页面数据的更新

            //+ get
            //+ obj和当前组件watcher建立关系
            //+ obj.foo 也和当前组件watcher建立关系
          foo: 'foo', //删除obj存在的foo,或者动态set一个bar

        }
      },
      mounted(){
          setTimeout(()=>{
            this.obj.foo = "fooooo"
          },1000)
      }
    });

  </script>
</body>

</html>

1.有几个Observer的实例?2个  
一个对象会有一个,data最外层{},obj:{} 
2.有几个Dep?       2个
有几个key,有几个dep
3.有几个Watcher?   1个
1个组件只有1个Watcher

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

创建了两个Observer的实例
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
一个组件一个Watcher

obj = {foo:'foo'}
obj.bar = 'aaa'; //no ok
Vue.set(obj,'bar','aaa');//ok

items = [1,3];
Vue.set(items,0,'abc');//ok
items.length = 0; //no ok
items[0]=='aaa';//no ok

Vue 2.0响应式缺点
1.递归遍历,性能会受影响
2.api不统一

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

数组响应化

数组数据变化的侦测跟对象不同,我们操作数组通常使用push、pop、splice等方法,此时没有办法得知数据变化。所以vue中采取的策略是拦截这些方法并通知dep。

src\core\observer\array.js

源码

//+ 获取数组原型
const arrayProto = Array.prototype
//+ 备份
export const arrayMethods = Object.create(arrayProto)

//+ 这7个方法才会修改数组
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
//+ 覆盖7个方法
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    //+ 执行原定的任务
    const result = original.apply(this, args)
    //+ 通知
    const ob = this.__ob__
    //+ 如果操作是插入操作,还需要额外的响应化处理
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted) 
    // notify change
    //+ 通知更新
    ob.dep.notify()
    return result
  })
})

Observer中覆盖数组原型

源码
//+ 判断类型
if (Array.isArray(value)) {
  //+ 替换数组对象原型
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {  
    //+ 没有原型,强硬覆盖
    copyAugment(value, arrayMethods, arrayKeys)
  }
  //+ 如果数组里面的元素是对象,还需要做响应化处理
  this.observeArray(value)
}

测试代码

相关API:Vue.set()/delete()

在这里插入代码片

作业

1.理出整体流程思维导图
2.自己尝试编写测试案例调试
3.自己研究Vue.set/delete/$watch等API
4.尝试看看vue异步更新队列是如何实现的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值