手写50行代码实现vue中this是如何访问data和methods,并调试vue源码详细解剖原理
前言
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.yaml和pnpm-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属性
get和set字段的默认值为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属性之后,不允许再出现value和writable中的任何一个属性,否则就会报错。
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
