首页前端开发VUE手写50行代码实现vue中this是如何访问data和methods,并调试vue源码详细解剖原理

手写50行代码实现vue中this是如何访问data和methods,并调试vue源码详细解剖原理

时间2023-07-05 12:20:02发布访客分类VUE浏览982
导读:前言1、通过本文可以了解到调试vue2.7.2+源码的两种方式,其他项目同样适用 2、了解vue2.7.2+初始化的流程 3、了解构造函数 new 操作符 4、了解 Object.defineProperty 5、了解 call、apply...


前言


1、通过本文可以了解到调试vue2.7.2+源码的两种方式,其他项目同样适用
2、了解vue2.7.2+初始化的流程
3、了解构造函数 new 操作符
4、了解 Object.defineProperty
5、了解 call、apply、bind 来改变 this 的指向
6、this.data 和 this.methods 的访问原理解剖
7、手写 mini 版 this.data 和 this.methods


1、准备源码和测试代码


1.1、拉取代码


git clone git@github.com:vuejs/vue.git


1.2、安装依赖


查看根目录可以轻松的发现pnpm-workspace.yamlpnpm-lock.yaml,那么就说明尤大大把vue2.7+也升级到pnpm。


如果你想去看历史版本,比如2.6版本可以,可以点击链接 github.com/vuejs/vue/t… ,本文就主要来看一下2.7+版本的。


pnpm i


1.3、准备测试代码


对于很多工作中正在使用vue2,甚至是vue3的大神们来说,下面这段代码再简单熟悉不过了。


script src="../../dist/vue.js">
    /script>
    
div id="demo">
    
  div>
{
{
name}
}
    /div>
    
  button @click="testThis">
    测试this/button>
    
/div>
    
script>

    const vm = new Vue({

        data: {

            name: 'aehyok',
        }
,
        methods: {

            sayName(){
    
                this.testThis();

            }
,
            testThis() {
    
              this.name = 'update-aehyok';
    
              console.log(this.name);

            }

        }
,
    }
    ).$mount('#demo');
    
/script>
    


其中有this.name可以直接访问data中的属性,然后通过this.testThis可以直接访问methods中的方法。




简化一个小例子,主要简单来看看new操作符


script>

function Vue() {

  this.name ="aehyok"
}
    
let vue = new Vue();
    
console.log(vue);
   // Vue {
name: 'aehyok'}
    
/script>
    


通过执行打印可以发现,new 通过构造函数创建出来的实例可以访问到构造函数中的属性。


关于new操作符更详细的可以查看下面两位大神的文章,感觉这里知识点还是蛮多,等有空再进行总结实践一下。


一篇是若川大佬的:juejin.cn/post/684490…

另外一篇则是掘金七级大牛的精彩文章:juejin.cn/post/684490…


1.4、调试方式


  • http-server


一种是通过pnpm build指令进行直接编译,然后将vue打包生成到dist目录,执行完pnpm build后的目录文件



这样可以直接调试dist/vue.js文件,但调试不到源代码文件。 此时我们可以使用http-server


// 全局安装
npm i -g http-server
// 安装完成后命令行运行
http-server -p 8089


调试效果如下



打开浏览器源代码标签,然后ctrl + p快捷键,输入state,便能找到对应的源代码文件。 比如在53行打上断点,刷新页面后,就会运行到断点位置。


  • sourcemap


先修改package.json中的dev指令


//未修改前
"dev": "rollup -w -c scripts/config.js --environment TARGET:full-dev",
// 修改后,主要添加 -m
"dev": "rollup -w -m -c scripts/config.js --environment TARGET:full-dev",


-m就是要生成sourceMap文件,生成sourceMap文件才能在源代码中打断点调试



运行起来之后的调试方式,跟http-server一样。


2、解析源码


2.1、入口文件


src/core/index.ts,通过调试发现,这应该是vue的主入口文件。


import Vue from './instance/index'
import {
 initGlobalAPI }
     from './global-api/index'
//其他无关的代码暂时移除了
//......
initGlobalAPI(Vue)
export default Vue


通过字面意思就可以发现,初始化全局API,可以发现Vue是通过模块import引入的。我来看看是否有代码自动执行了?/src/core/instance/index打开文件后发现果然有初始化的代码,这里我只挑主线的代码进行分析


2.2、Vue初始化


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'
import type {
 GlobalAPI }
 from 'types/global-api'
function Vue(options) {
    
  if (__DEV__ &
    &
 !(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 as unknown as GlobalAPI


初始化了Vue函数,然后下面的函数都将Vue作为参数进行传递,那我们就来看下面的第一个函数initMixin函数,剩下的几个函数可以自行去详细查看阅读。


export function initMixin(Vue: typeof Component) {
    
  Vue.prototype._init = function (options?: Recordstring, any>
) {
    
    const vm: Component = this
    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 (__DEV__ &
    &
     config.performance &
    &
 mark) {

      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${
vm._name}
 init`, startTag, endTag)
    }

    if (vm.$options.el) {

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

  }

}
    


Vue通过prototype为自身添加_init函数,这样当然只是初始化_init,并没有执行,等后面执行到这里我们再进行详细的解析。


2.3、初始化全局API


Vue简单初始化完毕,我们继续回到initGlobalAPI函数。


export function initGlobalAPI(Vue: GlobalAPI) {
    
  // 手动移除了很多无关紧要的代码,
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
  // 2.6 explicit observable API
  Vue.observable = T>
    (obj: T): T =>
 {

    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
  extend(Vue.options.components, builtInComponents)
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}
    


还是从字面意思我们就可以明白,在Vue函数对象上初始化全局函数,例如(set、delete、nextTick、options、Use、Mixin等等吧)。


2.4、调试 new Vue


接下来代码会运行到new Vue的位置,我提前打好了断点。



function Vue(options) {
    
  if (__DEV__ &
    &
 !(this instanceof Vue)) {

    warn('Vue is a constructor and should be called with the `new` keyword')
  }

  this._init(options)
}
    


要开始执行_init函数,这个_init在上面我已经将其初始化,直接执行即可。其实这里开始初始化的是当前页面组件的所需的方法,这里我着重来看一下initState,其他的初始化是类似的,只是方法实现不太一样。


src/core/instance/state.ts


export function initState(vm: Component) {

  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  // Composition API
  initSetup(vm)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {

    initData(vm)
  }
 else {

    const ob = observe((vm._data = {
}
    ))
    ob &
    &
 ob.vmCount++
  }
    
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch &
    &
 opts.watch !== nativeWatch) {

    initWatch(vm, opts.watch)
  }

}
    


  • initProps初始化Props


  • initSetup初始化组合式api的Setup生命周期钩子函数


  • initmethods初始化methods方法


  • initData初始化Data数据


  • initComputed 初始化computed


  • initwatch 初始化watch


从函数名字就可以非常清楚初始化的是什么,如果你使用过vue的话


2.5、initMethods 初始化方法


function initMethods(vm: Component, methods: Object) {

  const props = vm.$options.props
  for (const key in methods) {

    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }

}

// 空函数
function noop(a?: any, b?: any, c?: any) {
}
    


原来initMethods就是这么简单通过一个循环,将methods循环添加到vm对象上,就是这么的霸道。当然这里重点有一个bind函数


function polyfillBind(fn: Function, ctx: Object): Function {

  function boundFn(a: any) {
    
    const l = arguments.length
    return l
      ? l >
 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

function nativeBind(fn: Function, ctx: Object): Function {

  return fn.bind(ctx)
}
    
// @ts-expect-error bind cannot be `undefined`
export const bind = Function.prototype.bind ? nativeBind : polyfillBind


首先判断当前宿主下Function.prototype.bind 是否存在bind方法,有bind方法就直接调用bind,如果没有就采用call或者apply来改变this指向。


  • 就是使用 call apply bind,三种方式都可以实现相同的效果


    • fun.apply(thisArgs, [arg1, arg2]) 参数通过数组的方式传递
    • fun.call(thisArgs, arg1, arg2) 参数通过多个参数传递
    • fun.bind(thisArgs, arg1, arg2)() bind 相当于创建一个新的函数,我们还需要手动调用


2.6、initData初始化data


function initData(vm: Component) {

  
  // 中间省略或移除很多暂时不用的代码
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {
}

  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {

    //中间省略移除很多代码
    const key = keys[i]
    proxy(vm, `_data`, key)
  }
    
  // observe data
  const ob = observe(data)
  ob &
    &
 ob.vmCount++
}

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)
}
    


先是对data属性通过isFunction进行判断是否为一个函数,如果为一个函数通过getData将函数转换为数据对象。 所以我们在定义data的时候其实有两种方式(这里以前还真的不知道)


// 第一种方式
    data: {

        name: 'aehyok',
    }
    ,
    
    // 第二种方式
    data: () =>
 {

      return {

        name: 'aehyok'
      }

    }
    ,


然后通过Object.keys获取data中所有的keys,循环并通过proxy中的Object.defineProperty来实现对vm._data中所有数据属性的监听,其实就相当于在this._data做了一层代理。


通过proxy代理后,可以vm[key] 直接来访问,同时外部便可以达到this[key],举例:这里的key也便是我们上面在data中定义的name, this.name便最终取值成功。


this.name = vm.name = vm._data.name


observe(data)应该就是vue2的重点知识监听器,实现vue2双向绑定的代码应该都在这里面,这里简单看了一下没看明白,等有时间再来详细学习一下。


对于这里用到的Object.definePropery之前也只是知道、看过、但从来没真正的学习一下,这里刚好遇到了,就来手动demo尝试一遍。


3、Object.definePropery的剖析


3.1、最最简单的demo


script>

  const obj = {
}

  Object.defineProperty(obj, 'name', {

  }
    )
  console.log(obj.name)  // undefined
/script>
    


可以发现打印出来的为undefined。这其中所使用的其实是descriptor中的value属性。 该属性默认值为undefined,同时该属性的值可以设置为任何有效的JavaScript值(基础类型、对象、函数等等)。


3.2、 descriptor的value属性


script>

  const obj = {
}

  Object.defineProperty(obj, 'name', {

    value: 'aehyok'
  }
    )
  console.log(obj.name) // aehyok
/script>
    


执行之后最终打印 aehyok。


3.3、 descriptor的writable属性


writable默认值为false,设置为true后,下面属性的name值才能被修改


script>

  const obj = {
}

  Object.defineProperty(obj, 'name', {

    value: 'aehyok',  
    writable: true, 
  }
    )
  obj.name = 'leo'
  console.log(obj.name)  // leo
/script>
    


可以发现打印出来的为leo。如果不设置writable或者设置为false,则打印出来的为aehyok。


3.4、descriptor的configuable属性


configuable默认值为false,设置为true后,下面的name属性,可以从obj上删除


script>

  const obj = {
}

  Object.defineProperty(obj, 'name', {

    value: 'aehyok',  
    configurable: true,
  }
)
  console.log(obj) // {
name: 'aehyok'}

  delete obj.name
  console.log(obj)  // {
}
    
/script>


第一个console打印出来aehyok,第二个console可以发现打印出来的为{ } 空对象,obj中的name键通过delete被删除了。 如果不设置configuable或设置为false,则第二个console打印出来的{ name: 'aehyok'}


3.5、descriptor的enumerable属性


enumerable默认值为false,设置为true后,该属性才会出现在对象的枚举属性中。


script>

  const obj = {
}

  Object.defineProperty(obj, 'name', {

    value: 'aehyok',  
    enumerable: true,
  }
)
  for(let key in obj) {

    console.log(key,obj[key])
  }
    
/script>
    


可以发现打印出来的为name aehyok。 如果不设置enumerable或者设置为false,则什么都不会打印出来,因为刚好obj中没有一个可以枚举的属性。


3.6、descriptor的get和set属性


getset字段的默认值为undefined。


script>

  const obj = {
}

  let tempValue = 'temp name'
  Object.defineProperty(obj, 'name', {

    get() {

      return tempValue
    }
,
    set(value) {

      tempValue = value
    }

  }
    )
console.log(obj.name)
obj.name = 'Leo'
console.log(obj.name)
/script>
    


当我们通过obj.name访问name属性的时候,会调用get函数。 当我们通过obj.name = 'Leo'进行赋值的时候,会调用set函数。


3.7、小结


Object.defineProperty(obj, prop, descriptor)


  • obj :要定义的属性。


  • prop:要定义或修改的属性的名称或 [Symbol]。


  • descriptor:要定义或修改的属性描述符


    • value属性,默认值为undefined


    • get属性,默认值为undefined


    • set属性,默认值为undefined


    • writable属性,默认值为false


    • configuable属性,默认值为false


    • enumerable属性,默认值为false



通过截图可以发现,当同时存在value和get属性的时候,会发生如图所示的错误。


所以可以总结为:在使用了get或者set属性之后,不允许再出现valuewritable中的任何一个属性,否则就会报错。


4、 手写50行代码实现this访问data和methods


script>

  let descriptor = {

    enumerable: true,
    configuable: true,
  }

  function proxy(obj, sourceKey, key) {

    descriptor.get = function getter() {

      return this[sourceKey][key]
    }
    ;

    descriptor.set = function setter(val) {

      this[sourceKey][key] = val
    }

    Object.defineProperty(obj,key, descriptor)
  }

  function Vue(options) {
    
    let vm = this;

    vm.$options = options
    vm._init(vm)
  }

  Vue.prototype._init = function (vm) {

    let opts = vm.$options
    if(opts.data) {
    
      initData(vm);

    }

    if(opts.methods) {
    
      initMethods(vm, opts.methods);

    }

  }

  function initData(vm) {
    
    const data = vm._data  = vm.$options.data;
    
    const keys = Object.keys(data);
    
    let i = keys.length;

    while (i--) {
    
      var key = keys[i];

      proxy(vm, '_data', key)
    }

  }

  function initMethods(vm, methods) {

    for(let key in methods) {

      vm[key] = typeof methods[key] !== 'function' ? {
}
 : methods[key].bind(vm)
    }

  }
    
/script>
    


进行实例调用


const t_vue = new Vue({

    data: {

      name: 'aehyok',
    }
,
    methods: {

      testThis() {

        this.changeName()
        console.log(this.name)
      }
,
      changeName() {
    
        this.name = 'testThis';

      }

    }

  }
    )
  console.log(t_vue.name)
  console.log(t_vue)
  t_vue.testThis()



通过运行后的截图可以发现,data中的name属性,以及methods中的testThis方法和changeName方法都已经被加载到了实例上了,再来简单的说明一下:


  • 通过Vue.prototype._init初始化


  • initMethods中直接通过bind进行生成新的函数,并直接通过vm[key]赋值,达到this能够访问的目的


  • initData中则是通过Object.definePropery实现绑定到vm[key],从而达到this访问的目的


5、总结


  • 1、熟悉了解 new 、bind 、call、apply简单用法


  • 2、熟悉了解vue2中Object.defineProperty响应式原理


  • 3、熟悉了解vue2中初始化代码的逻辑处理


  • 4、手写实现mini版初始化来支持this访问


通过调试源码发现,只要仔细一点稍微花点时间,原来也能看懂尤大写的代码,没有想象中的那么难,而且感觉逻辑非常清晰,阅读起来很优雅。所以大家如果有想看源码,或者参加若川源码共读活动的,一定要大胆一些,不要怂,事情真的没有那么难。


有点目的性的阅读源码似乎更高效,这样针对性很强,不会大一统所有的源码都会过一下,时间一下子就过去了,每次带着一个小问题去看源码或许也是若川大佬的精髓所指。


通过阅读源码,就是把看不懂的函数方法关键字等,不断的查漏补缺。或者在这里的用法或者写法不一样,等等各种超乎你想象的用法、场景...,收获真的是非常大,尤其是看完后再写一篇小文总结出来,真的就比读一遍别人写的收获要多好几倍的感觉。


所以如果你还在犹豫自己看不懂,自己行不行等等借口,作为一个前端还不到两年经验的人告诉你,加加油相信自己,你完全可以的。最后一定要行动起来。

声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!


若转载请注明出处: 手写50行代码实现vue中this是如何访问data和methods,并调试vue源码详细解剖原理
本文地址: https://pptw.com/jishu/290240.html
Vue学习笔记(十) Vue Router asp.net core过滤器记录响应对象

游客 回复需填写必要信息