Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wepy 的运行原理探索 #3

Open
loo41 opened this issue Aug 3, 2019 · 0 comments
Open

wepy 的运行原理探索 #3

loo41 opened this issue Aug 3, 2019 · 0 comments

Comments

@loo41
Copy link
Owner

loo41 commented Aug 3, 2019

前言

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

关于 博客

一开始打算用 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.js
import { app, page, component } from '../native/index';

export function initGlobalAPI (wepy) {

 wepy.app = app; // return App({})
 wepy.page = page; // return Page({})
 wepy.component = component; // return Component({})

 return wepy;
}

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

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

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

// core/weapp/native/app.js
export function app (option, rel) {
 let appConfig = {};
 patchAppLifecycle(appConfig, option, rel); // 注册和初始化生命周期
 return App(appConfig); // 这里最终运行于小程序中 appConfig 就是小程序js中的逻辑
}

// patchAppLifecycle 函数
// core/weapp/init/lifecycle
export function patchAppLifecycle (appConfig, options, rel = {}) {
 appConfig.onLaunch = function (...args) {
   let vm = new WepyApp();
   // 生命周期回调,触发页面生命周期函数
   return callUserMethod(vm, vm.$options, 'onLaunch', args);
 };
 // 相当于 App({onLaunch: function() {}})

 // 获取生命周期列表
 let lifecycle = getLifecycycle(WEAPP_APP_LIFECYCLE, rel, 'app');
 
 // 循环绑定生命周期
 lifecycle.forEach(k => {
  appConfig[k] = function (...args) {
   return callUserMethod(app, app.$options, k, args);
  };
 });
};

数据绑定

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

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

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

export function initData (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.js
export function observe ({vm, key, value, parent, root}) {
  // 判断是否为观察者 __ob__ 属性定义
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  }
  // 观察数据
  ob = new Observer({vm: vm, key: key, value: value,  parent});
  return ob
}

class Observer {
    // 如果是数组,则做特殊处理,因为数组 push, pop 等不能劫持,所以需要特殊处理
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(key, value);
    } else {
      // 否则劫持数据
      this.walk(key, value);
    }
}

walk (key, obj) {
    const keys = Object.keys(obj)
    for (let i = 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: function reactiveGetter () {
      const val = getter ? getter.call(obj) : value
      // 依赖收集
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(val)) {
            dependArray(val)
          }
        }
      }
      return val
    },
    set: function reactiveSetter (newVal) {
      const val = 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.js
export function initRender (vm, keys) {
 return new Watcher(vm, function () {
     // 会触发数据到 getter 进行依赖收集
     Object.keys(keys).forEach(key => clone(vm[key]))
     // 调用 setData 进行数据更新,vm.$wx 在页面 created 钩子到时候进行绑定,写在了前面,是小程序中的 this
     vm.$wx.setData(dirty, renderFlushCallbacks)
   }
 }, function () {

 }, null, true);
};

编译结果

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

// app.js
// vendor.js 为编译后的核心代码,相当于,导出的 wepy
var _core = _interopRequireDefault(require('vendor.js')(1));

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

// 调用 page 函数,return Page({}) 完成对小程序页面的注册
_core["default"].page({}, {});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant