热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从源码切入Vue双向绑定原理,并实现一个demo

本文涉及源码版本为2.6.9准备工作down一份Vue源码,从package.json入手,找我们需要的代码1、package.json中的scripts,build:nodesc

本文涉及源码版本为 2.6.9

准备工作

down一份Vue源码,从package.json入手,找我们需要的代码
1、package.json中的scripts,"build": "node scripts/build.js"
2、scripts/build.js line26 build(builds),其中builds的定义为11行的let builds = require('./config').getAllBuilds(),这个大概就是打包的代码内容,另一个build是在下面定义的函数,他的代码是这样的:

function build (builds) {
let built = 0
const total = builds.length
const next = () => {
buildEntry(builds[built]).then(() => {
built++
if (built next()
}
}).catch(logError)
}
next()
}

这段代码有说法,其中的buildEntry是使用rollup进行打包的函数,定义一个next函数,把多个吃内存的打包操作串行,达到减小瞬间内存消耗的效果,这算是常用的一个优化方式了。
3、顺着scripts/config.js里的getAllBuilds()的逻辑摸到line28的const aliases = require('./alias'),然后打开scripts/alias.js,看到里面的vue: resolve('src/platforms/web/entry-runtime-with-compiler')终于有点豁然开朗,然后再根据一层层的import找到src/core/instance/index.js里的function Vue(){},准备工作到此结束。

new Vue()发生了什么

就一行,this._init(options),这是在函数initMixin()中定义在Vue.prototype上的方法

export function initMixin (Vue: Class) {
Vue.prototype._init = function (options?: Object) {
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')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

需要留意的分别是initState(vm)vm.$mount(vm.$option.el)

  1. initState(vm)

    export function initState (vm: Component) {
    const opts = vm.$options
    if (opts.props) initProps(vm, opts.props)
    if (opts.methods) initMethods(vm, opts.methods)
    if (opts.data) {
    initData(vm)
    }
    }

    字面意思,初始化props,methods,data,由于目的是看数据双向绑定,就直接进initData()

    1.1 proxy

    在initData()中,遍历data中的keys判断是否与props和methods重名,然后对他们设置了一层代理

    const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
    }
    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)
    }

    这就是为什么我们可以直接通过this.name获取到this.data.name的值。
    关于Object.defineProperty()可以设置的属性描述符,其中

  • configurable控制是否可以配置,以及是否可以delete删除, 配置就是指是否可以通过Object.defineProperty修改这个属性的描述,没错如果你通过defineProperty把某个属性的configurable改为false,再想改回来是不可能的。《从源码切入Vue双向绑定原理,并实现一个demo》
  • enumerable控制是否可枚举,赋值为false之后,Object.keys()就看不见他了。
  • 还有value、writable、get、set,都比较好理解就不再赘述。

    1.2、new Observe()

    遍历完keys,就是以data作为参数调用observe了,而observe内部得主要内容就是ob = new Observer(value),再看Observer这个类。(有一种抽丝剥茧得感觉)

    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
    this.dep = new Dep()
    this.vmCount = 0
    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 (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i defineReactive(obj, keys[i])
    }
    }
    observeArray (items: Array) {
    for (let i = 0, l = items.length; i observe(items[i])
    }
    }
    }

    函数def的作用就是在对象上定义属性。然后判断传进的data是对象还是数组。

    1.2.1、Array.isArray(value)

    如果value是数组的话,先通过hasProto这个自定义函数来判断当前环境中是否存在__proto__,如果有的话就可以直接用,没有的话,手动
    实现一下,功能是一样的,那就只看protoAugment(value, arrayMethods)干了啥就好

    function protoAugment (target, src: Object) {
    target.__proto__ = src
    }

    其中target自然就是我们observe的数组,而src也就是arrayMethods的定义如下

    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
    ]
    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
    })
    })

    看着代码里的methodsToPatch里的几项,眼熟吗
    《从源码切入Vue双向绑定原理,并实现一个demo》
    再看到倒数第四行的ob.dep.notify(),配上官方注释notify change
    也就是说arrayMethods是一个继承数组原型的对象,并对其中特定的几种方法做了处理,然后在new Observe(value)的时候,如果value是数组,就让value继承这个arrayMethods,然后这个数组调用特定的方法时,会调用当前Observe类上的dep属性的notify方法,进行后续操作。
    定义完这些,再进行递归对数组中的每一项继续调用observe

    1.2.2、walk & defineReactive

    然后对于对象而言,直接调用walk,然后遍历对象中的非继承属性,对每一项调用defineReactive

    export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
    ) {
    const dep = new Dep()
    let childOb = !shallow && observe(val)
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
    dep.depend()
    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()
    }
    })
    }

    defineReactive的主要代码就是各种判断递归和Object.defineProperty()了,这也是双向绑定的关键一部分,从数据到DOM。
    其中对get的定义包含了if(Dep.target){ dep.depend() },对set的定义包含了dep.notify(),接下来看Dep的方法。

    1.3 Dep

    Dep的定义是这样的

    export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array;
    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 subs[i].update()
    }
    }
    }

    来看在get中调用的dep.depend(),Dep.target不为空的情况下,以this为参数,调用Dep.target.addDep,target是Dep类的静态属性,类型为Watcher,方法addDep定义如下

    addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
    dep.addSub(this)
    }
    }
    }

    可以看到addDep有去重dep的作用,然后通过调用dep.addSub(this),把当前的Dep.target push到subs中。
    也就是说,data里面有个observer,然后observer里面有个dep,dep里面有个watcher数组,收集依赖一条龙。

    至于在set中调用的dep.notify(),是遍历watcher数组,调用每一项的update方法,而update方法,核心代码是调用watcher的run方法,run方法的核心是this.cb.call(this.vm, value, oldValue)。问题又来了,这个cb是new Watcher时的传参,但是从initState一步一步看下来,先new一个Observe,然后定义其中每个属性的getsetget时收集依赖,set时通知变更。但是并没有看到哪里真的触发了我们所设置的get,而且之前说到的Dep.target是个啥呢。

  1. vm.$mount(vm.$option.el)

    前文有提到new Vue时也调用了这个方法,$mount是前面找Vue入口文件的过程中,在其中一个里定义在Vue原型上的方法

    Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
    ): Component {
    el = el && inBrowser ? query(el) : undefined
    return mountComponent(this, el, hydrating)
    }

    然后再找mountComponent,果然在这个函数的调用中,找到了

    mountComponent() {
    // 其他逻辑
    new Watcher(vm, updateComponent, noop, {
    before () {
    if (vm._isMounted && !vm._isDestroyed) {
    callHook(vm, 'beforeUpdate')
    }
    }
    }, true /* isRenderWatcher */)
    }

    再去看Watcher的构造函数,有调用自己的get方法,定义如下

    get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    value = this.getter.call(vm, vm)
    } catch (e) {
    if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
    throw e
    }
    } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
    traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    }
    return value
    }

    pushTarget(this)来设置Dep的静态属性target,然后调用this.getter.call(vm, vm)来做虚拟DOM相关的操作,并且触发对data对象上的属性设置的getter,最后popTarget()Dep.target置为null。
    Dep.target的作用就是只有在初始化时才会收集依赖,要不然每次取个值收集依赖再判重,卡都卡死了。

最后

跟着源码梳理了一遍逻辑,对Vue的了解也更深入了一些,再去看Vue官网中对响应式原理的描述,也更清晰了。

《从源码切入Vue双向绑定原理,并实现一个demo》

本文也只是大概讲了一下右边红框中的实现逻辑,关于左边的虚拟DOM,暂时真的没看懂。基于上面逻辑自己尝试着写了一个简版的Vue->传送门,尤大不是说一开始Vue也只是个自己写着玩的项目,多尝试总是没有错。
文中没有说清楚的地方欢迎指正,如果你也对Vue实现原理感兴趣,不妨也去down一份源码亲自探索吧


推荐阅读
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文讨论了如何使用Web.Config进行自定义配置节的配置转换。作者提到,他将msbuild设置为详细模式,但转换却忽略了带有替换转换的自定义部分的存在。 ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
  • VUE中引用路径的配置
    在vue项目开发中经常引用JS、CSS、IMG文件。当项目较大时文件层级很多,导致路径很长,我们可以通过在bulidwebpack.base.conf.js设置简便的引用路径一、 ... [详细]
  • 媒介本文的前身是源自github上的项目awesome-github-vue,但由于该项目上次更新时候为2017年6月12日,许多内容早已逾期或是许多近期优异组件未被收录,所以小肆 ... [详细]
  • 本文介绍了使用AJAX的POST请求实现数据修改功能的方法。通过ajax-post技术,可以实现在输入某个id后,通过ajax技术调用post.jsp修改具有该id记录的姓名的值。文章还提到了AJAX的概念和作用,以及使用async参数和open()方法的注意事项。同时强调了不推荐使用async=false的情况,并解释了JavaScript等待服务器响应的机制。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 先看看ElementUI里关于el-table的template数据结构:<template><el-table:datatableData><e ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
  • 本文介绍了OkHttp3的基本使用和特性,包括支持HTTP/2、连接池、GZIP压缩、缓存等功能。同时还提到了OkHttp3的适用平台和源码阅读计划。文章还介绍了OkHttp3的请求/响应API的设计和使用方式,包括阻塞式的同步请求和带回调的异步请求。 ... [详细]
author-avatar
南塘所有的经筒
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有