Skip to content

wepy 的运行原理探索 #3

Description

@loo41

前言

刚搭建了一个博客,咋都得写上几篇文章,不然不就白搭了。

关于 博客

一开始打算用 hexo + gitalk + github 搭建一个,但是人太懒,总想一次完事,以后更新就写一篇 .md 文章就可以了,不想弄其他流程了,然后就用了 vue + github-issuse-api + gitalk + github 搭建了一个一劳永逸的办法,除非 github 挂了。

关于 wepy

  • 官方介绍

    是小程序【最早(wepy你已经很成熟了)】 的框架之一,是一款让小程序支持 【组件化开发(现在小程序已经支持npm构建,好像没啥优势)】 的框架,通过预编译的手段让开发者可以选择自己 【喜欢的开发风格(类 vue 语法,数据劫持,自动 setData)】 去开发小程序。框架的细节优化,Promise,【Async Functions(原生不支持)】的引入都是为了能让开发小程序项目变得更加简单,高效

  • 优缺点
     - 优点
    - vue 语法,更好的开发体验
    - 更好的组件化
    - 更多的组件库
    - 支持更多 ECMAscript 语法
    - 缺点
    - 官方维护不太积极,遇到问题解决太慢
    - 引入了额外的代码,增加了代码体积
    
  • 代码库
     - 研究源码版本 2.0.0-alpha.8
    - wepy 核心代码是 cli 和 core
    - wepy-cli 是文件转换打包的处理
    - core 是对数据绑定生命周期注入的处理
    

core 源码探索

  • wepy 核心代码分为 wepy-cli 和 core,这篇文章主要对 core 进行探索
  • 源码版本 2.0.0-alpha.8
  • 源码库 wepy/packages/core
  • 为了看的更清晰一些我删除了大部分占时没有用到代码
  • 每个 js 部分,头部我会写清对于源码文件路径
  • 牢记一点,运行环境和this是在运行时候确认,不然你看源码时候你会很懵,明明 this 上没有这个函数或属性啊,明明全局搜不到这个方法啊,就像下面这个 App() 你不和小程序环境关联起来,你全局都搜不到有这么一个函数
  • 代码里面注释很关键

wepy 是怎么编译之后运行到小程序中去的?

// core/weapp/apis/index.jsimport{app,page,component}from'../native/index';exportfunctioninitGlobalAPI(wepy){wepy.app=app;// return App({})wepy.page=page;// return Page({})wepy.component=component;// return Component({})returnwepy;}

写过 wepy2.0 的就知道,通过 wepy.page(), wepy.component(), 注入页面和组件,以及在app.wpy中的 wepy.app() 方法 ,通过这三个方法,分别会返回 App() Page() Component(),写过原生到小伙伴是不是很熟悉这三个 函数,分别是实现组册小程序,注册页面和组件,这样就和原生关联起来了

wepy 怎样实现生命周期绑定关联的了?

拿 app 为例, 首先这个函数做了什么?app 函数里运行 patchAppLifecycle 函数 并传入小程序运行环境 appConfig 中的对象【相当于 APP({}) 函数中的对象】,然后将 所有生命周期绑定上去,每次原生环境运行,触发原生生命周期,每个生命周期都会触发一个回调函数,在执行 .wey 文件中存在的生命周期函数逻辑,这样,小程序的生命周期和你页面的生命周期进行了关联

// core/weapp/native/app.jsexportfunctionapp(option,rel){letappConfig={};patchAppLifecycle(appConfig,option,rel);// 注册和初始化生命周期returnApp(appConfig);// 这里最终运行于小程序中 appConfig 就是小程序js中的逻辑}// patchAppLifecycle 函数// core/weapp/init/lifecycleexportfunctionpatchAppLifecycle(appConfig,options,rel={}){appConfig.onLaunch=function(...args){letvm=newWepyApp();// 生命周期回调,触发页面生命周期函数returncallUserMethod(vm,vm.$options,'onLaunch',args);};// 相当于 App({onLaunch: function() {}})// 获取生命周期列表letlifecycle=getLifecycycle(WEAPP_APP_LIFECYCLE,rel,'app');// 循环绑定生命周期lifecycle.forEach(k=>{appConfig[k]=function(...args){returncallUserMethod(app,app.$options,k,args);};});};

数据绑定

如果你了解过 vue 的数据绑定那这个实现原理也和 vue 一样 (虚!!我怀疑是抄的,连函数名都一样),也是实现了一个观察者模式,只是,vue 做的更新处理是 patch 函数 diff 虚拟 dom 实现更新 web,而,wepy做调是调用 setData 更新数据,话不多说,上代码。

// core/weapp/init/lifecycleexportfunctionpatchLifecycle(output,options,rel,isComponent){// output 小程序的运行环境constinitLifecycle=function(...args){letvm=newinitClass();// 将小程序 this 绑定到 vm.$wx 上去vm.$wx=this;// 初始化数据,进行数据绑定,进行绑定对数据只是 data 中的数据initData(vm,output.data,isComponent);// 进行依赖收集,做 setDatainitRender(vm,Object.keys(vm._data).concat(Object.keys(vm._props)).concat(Object.keys(vm._computedWatchers||{})));returncallUserMethod(vm,vm.$options,'created',args);};// 绑定到小程序到生命周期中去,做点事// 相当于 Page({created: initLifecycle(){}})output.created=initLifecycle;};

来看看 initData 做了啥, 对 data 进行了处理,代理到 _data 属性上去,进行观察

exportfunctioninitData(vm,data){vm._data=_data;Object.keys(_data).forEach(key=>{proxy(vm,'_data',key)});observe({vm: vm,key: '',value: _data,parent: '',root: true});}

observe 函数做了啥

// core/weapp/observer/index.jsexportfunctionobserve({vm, key, value, parent, root}){// 判断是否为观察者 __ob__ 属性定义if(hasOwn(value,'__ob__')&&value.__ob__instanceofObserver){ob=value.__ob__}// 观察数据ob=newObserver({vm: vm,key: key,value: value, parent});returnob}classObserver{// 如果是数组,则做特殊处理,因为数组 push, pop 等不能劫持,所以需要特殊处理if(Array.isArray(value)){constaugment=hasProto
? protoAugment
: copyAugmentaugment(value,arrayMethods,arrayKeys)this.observeArray(key,value);}else{// 否则劫持数据this.walk(key,value);}}walk(key,obj){constkeys=Object.keys(obj)for(leti=0;i<keys.length;i++){defineReactive({vm: this.vm,obj: obj,key: keys[i],value: obj[keys[i]],parent: obj});}}defineReactive(){Object.defineProperty(obj,key,{enumerable: true,configurable: true,get: functionreactiveGetter(){constval=getter ? getter.call(obj) : value// 依赖收集if(Dep.target){dep.depend()if(childOb){childOb.dep.depend()if(Array.isArray(val)){dependArray(val)}}}returnval},set: functionreactiveSetter(newVal){constval=getter ? getter.call(obj) : value// 更新数据value=newVal// 通知数据更新dep.notify();}})}

如果是数组需要做特殊处理,数据,push,pop等方式改变等数据不能被劫持,如果不是,就做数据劫持 getter setter,数据劫持的初始化已经做完了,现在在来看如进行等依赖收集,回到 initRender() 上了,进行依赖收集,就得触发一次数据到 getter, initRender 就是干这个的,watch 观察每个页面实例,触发一次 data getter 进行依赖收集,每次 setter 到时候就会触发这个函数 setData

// core/weapp/init/render.jsexportfunctioninitRender(vm,keys){returnnewWatcher(vm,function(){// 会触发数据到 getter 进行依赖收集Object.keys(keys).forEach(key=>clone(vm[key]))// 调用 setData 进行数据更新,vm.$wx 在页面 created 钩子到时候进行绑定,写在了前面,是小程序中的 thisvm.$wx.setData(dirty,renderFlushCallbacks)}},function(){},null,true);};

编译结果

可以看到最后编译出来就是执行最开始导出的三个函数,对应到不同的小程序页中去

// app.js// vendor.js 为编译后的核心代码,相当于,导出的 wepyvar_core=_interopRequireDefault(require('vendor.js')(1));// 调用 app 函数,return App({}) 完成对小程序的注册_core["default"].app({},{a: 1})
// page.js// vendor.js 为编译后的核心代码,相当于,导出的 wepyvar_core=_interopRequireDefault(require('../vendor.js')(1));// 调用 page 函数,return Page({}) 完成对小程序页面的注册_core["default"].page({},{});

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions