bt365体育投注.主頁欢迎您!!

    <acronym id="zvmrr"></acronym>
    <td id="zvmrr"></td>
  • <tr id="zvmrr"><label id="zvmrr"></label></tr>
  • <acronym id="zvmrr"></acronym>
  • justjavac

    justjavac 查看完整档案

    天津编辑  |  填写毕业院校自由职业  |  前端 编辑 discuss.flarum.org.cn 编辑
    编辑

    会写点 js 代码

    个人动态

    justjavac 回答了问题 · 1月20日

    typescript class定义私有方法

    如果你需要 es 中的真正的私有属性,而非 ts 的假私有属性,可以这样写:

    class Test {
        #a = () => console.log(1)
    
        public b() {
            this.#a();
        }
    }

    如果你需要支持低版本的 js,而只是希望这个属性不可见(其实还是可以访问的),可以将这个数值设置为不可枚举。

    function Test() {}
    
    Object.defineProperty(Test.prototype, "a", {
      enumerable: false,
      writable: true,
    });
    
    Test.prototype.a = function() {
      console.log(111)
    }

    关注 3 回答 2

    justjavac 回答了问题 · 2020-11-20

    解决express的res.redirect('back')是干什么用的啊?

    这个并不是常规意义上的“退回上一步”。这个操作是将访问重定向到了请求头(request headers)的 referer

    当我们从 https://a.com/xxx 跳转到 https://b.com/yyy 时,我们向 yyy 发送的请求头里面,referer 是 https://a.com/xxx。此时调用 res.redirect('back') 就会回退到 xxx 页面。

    但是当我们进入到 yyy 页面后,再刷新页面,此时的 referer 会变成 https://b.com/yyy,表现的结果就是,会停留在本页不动。

    而你的情况更特殊,100% 是停留在本页,根本不会跳转到上一页。

    以为你在 yyy 页面中使用超链接发起一个请求,<a href="/back">后退</a>,此时的 referer 永远是 https://b.com/yyy,所以你会停留在本页面。

    你还可以这么理解:你目前在 yyy 页面,点击链接后你跳转到了 back 页面,但是 back 页面的逻辑是回退到上一个页面,于是又跳回了 yyy 页面。

    关注 1 回答 1

    justjavac 回答了问题 · 2020-11-20

    解决问一个网络传输和序列化问题!

    序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程,对应的则是反序列化的过程,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

    content-type 表示数据以什么样的方式进行编码。比如需要传递一个对象 User,这个对象有 2 个 key 和 2 个 value。

    let user = {
      "name": "justjavac",
      "age": 18
    }

    在内存中这个对象不仅仅只有这 2 个属性,还有原型链以及其他一些对象的状态信息和内存布局信息。然而我们只需要传输或者存储这 2 个信息,也就是说我们要对 User 的这 2 个属性进行序列化。

    而编码就涉及到,我们如何将这个对象通过网络进行传输,为了确保接收方(服务器)能够正确的解码这个对象,我们不仅仅要发送序列化的字符串,还要发送 content-type,告知对方以何种方式进行解码。

    我们可以编码为:

    name:justjavac\n
    age:18

    或者

    name=justjavac&age=18

    或者

    name:justjavac;age=18

    服务器接收到数据后,再根据 content-type 的编码类型进行解码。

    浏览器和服务器只支持第二种,这种方式的优势是发送的数据量少,缺点是没有类型信息。服务器接收到数据后,根据约定转化为对应的类型(此类型不必和前端 js 一一对应)。

    另一种编码就是 json,发送数据为:

    {"name":"justjavac","age":18}

    这种方式包含了类型信息。

    此外 http 还支持另外的 2 中编码方式(共4种)。

    此外,如果你的业务涉及到加密或者更优秀的编码方式,也可以使用 raw 方式传递原始字符串数据,然后前后端约定编码/解码算法。

    比如传递

    u^3hpo)4hAU

    以上。

    关注 2 回答 2

    justjavac 回答了问题 · 2020-11-18

    解决从csv文件导入行情数据时,列名是乱码

    应该是编码问题,在 Windows 系统中 csv 是 gb2312 编码。试试把编码改为 utf8。

    关注 2 回答 2

    justjavac 赞了文章 · 2020-11-16

    基于 Serverless 的 Valine 可能并没有那么香

    Valine 是一款样式精美,部署简单的评论系统, 第一次接触便被它精美的样式,无服务端的特性给吸引了。它最大的特色是基于 LeanCloud 直接在前端进行数据库操作而无需服务端,极大的缩减了部署流程,仅需要在静态页引入 Valine SDK 即可。

    ?????? 初识 Valine

    以下是 Valine 官网提供的快速部署脚本,其中 appIdappKey 是你在 LeanCloud 上创建应用后对应的应用密钥。也正是基于这对密钥,Valine 在内部调用了 LeanCloud SDK 进行数据的获取,最终将数据渲染在 #vcomments 这个 DOM 上。这便是 Valine 的大概原理。

    <head>
      ..
      <script data-original='//unpkg.com/valine/dist/Valine.min.js'></script>
      ...
    </head>
    <body>
      ...
      <div id="vcomments"></div>
      <script>
        new Valine({
          el: '#vcomments',
          appId: 'Your appId',
          appKey: 'Your appKey'
        })
      </script>
    </body>

    有同学可能会有疑问了,appIdappKey 都直接写在前端了,那岂不是谁都可以修改数据了?这就需要牵扯到 LeanCloud 的数据安全问题了,官方专门写了篇文档《数据和安全》 来说明这个问题。简单的理解就是针对数据设置用户的读写权限,确保正确的人对数据有且仅有正确的权限来保证数据的安全。

    乍听一下,保证用户数据只读的话,感觉还是挺安全的。可事实真的如此么,让我们继续来看看。

    ???♂? Valine 的问题

    ?? 阅读统计篡改

    Valien 1.2.0 增加了文章阅读统计的功能,用户访问页面就会在后台 Counter 表中根据 url 记录访问次数。由于每次访问页面都需要更新数据,所以在权限上必须设置成可写,才能进行后续的字段更新。这样就造成了一个问题,实际上该条数据是可以被更新成任意值的。感兴趣的同学可以打开 https://valine.js.org/visitor... 官网页面后进入控制台输入以下代码试试。试完了记得把数改回去哈~

    const counter = new AV.Query('Counter');
    const resp = await counter.equalTo('url', '/visitor.html').find();
    resp[0].set('time', -100001).save();
    location.reload();

    可以看到该页面的访问统计被设置成了 -100000 了。这个问题唯一值得庆幸的是 time 字段的值是 Number 类型的,其它的值都无法插入。如果是字符串类型的话就是一个 XSS 漏洞了。

    该问题有一个解决办法,就是不使用次数累加的存储方式。更改为每次访问都存储一条只读的访问记录,读取的时候使用 count() 方法进行统计。这样所有数据都是只读的,就不存在篡改的问题了。这种解决方案唯一的问题就是数据量会比较大,对查询会造成一定压力。当然如果是在基于原数据不变的情况下,只能是增加一层服务端来做修改权限的隔离了。

    ?? XSS 安全

    从很早的版本开始就有用户报告了 Valine 的 XSS 问题,社区也在使用各种方法在修复这些问题。包括增加验证码,前端XSS过滤等方式。不过后来作者才明白,前端的一切验证都只能防君子,所以把验证码之类的限制去除了。

    现有的逻辑里,前端发布评论的时候会将 Markdown 转换成 HTML 然后走一下前端的一个 XSS 过滤方法最后提交到 LeanCloud 中。从 LeanCloud 中拿到数据之后因为是 HTML 直接插入进行显示即可。很明显,这个流程是存在问题的。只要直接提交的是 HTML 而且拿到 HTML 之后直接进行展示的话,XSS 从根本上是无法根除的。

    那有没有根本的解决办法?其实是有的。针对存储型的 XSS 攻击,我们可以使用转义编码进行解决。只要效仿早前 BBCode 的做法,提交到数据库的是 Markdown 内容。前端读取到内容对所有 HTML 进行编码后再进行 Markdown 转换后展示。

    function encodeForHTML(str){
      return ('' + str)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')    
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#x27;')
        .replace(/\//g, '&#x2F;');
    };

    由于 Serverless 攻击者是可以直达存储阶段,所以数据存储之前的一切防范是无效的,只能在读取展示过程处理。由于所有的 HTML 转义后无法解析,Markdown 相当于我们根据自定义的语法解析成 HTML,保证转换后的 HTML 没有被插入的机会。

    不过这个方法存在一个问题,那就是对老数据存在不兼容。因为这相当于修改了存储和展示的规则,而之前一直存储的都是 HTML 内容,修复后之前的数据将无法展示 HTML 样式。而为了能在存储的还是 HTML 情况下规避 XSS 安全问题,唯一的办法就是增加服务端中间层。存储阶段增加一道阀门,将转义阶段提前至存储阶段,保证新老数据的通用。

    ?? 隐私泄露

    说完了存储的问题,我们再来看看读取的问题。攻击者除了可以直达存储,也可以直达读取,当一个数据库的字段开放了读取权限后,相当于该字段的内容对攻击者是透明的。

    在评论数据中,有两个字段是用户比较敏感的数据,分别是 IP 和邮箱。灯大甚至专门写了一篇文章来批判该问题 《请马上停止使用Valine.js评论系统,除非它修复了用户隐私泄露问题》。甚至掘金社区在早期使用 LeanCloud 的时候也暴出过泄露用户手机号的安全问题。

    为了规避这个问题,Valine 作者增加了 recordIP 配置用来设置是否允许记录用户 IP。由于是 Serverless,目前能想到的也只是不存储的方式解决了。不过该配置项会存在一个问题,就是该配置项的配置权在网站,隐私的问题是评论者遇到的,也就是说评论者是无权管理自己的隐私的。

    除了这个矛盾点之外,还有就是邮箱的问题。邮箱本质上只需要返回 md5 用来获取 Gravatar 头像即可。但是由于无服务端的限制,只能返回原始内容由前端计算。而邮箱我们又需要获取到原始值,方便做评论回复邮件通知功能。所以我们也不能不存储,或者存储 md5 后的值。

    该问题的解决方案只能是增加一层服务端,通过服务端过滤敏感信息解决这个问题。

    ?? Waline!

    基于以上原因,我们发现只有增加一层服务端中间层才能很好的解决 Valine 的安全问题,所以 Waline 横空出世了!Waline 与 Valine 最大的不同就是增加了服务端中间层,解决 Valine 暴露出来的安全问题。同时基于服务端的特性,提供了邮件通知微信通知评论后台管理、LeanCloud, MySQL, MongoDB, SQLite, PostgreSQL 多存储服务支持等诸多特性。不仅如此,Waline 默认使用 Vercel 部署,实现完全免费部署!

    Waline 最初的目标仅仅是为 Valine 增加上服务端中间层。但是由于作者不知为何从 1.4.0 版本开始只推送编译后的文件到 Github 仓库中,源文件停止更新。导致我只能连带前端也实现一遍。当然前端的很多代码和逻辑为了和 Valine 的配置保持一致都有参考 Valine,甚至在名字上,我也是从 Valine 上衍生的,让大家能明白这个项目是 Valine 的衍生版。

    ?? 后记

    Serverless 的概念火了非常多年,但技术没有银弹,我们在看到它的优点的同时,也要正视它所带来的问题。而 Serverless 自己可能也意识到了这个问题,从早期的无服务端慢慢转向了无服务器,更偏向 BaaS 了。不过由于 Valine 没有开放源代码,所以上面说的一些问题和解决方法只能等待作者自己发现这件事了。

    查看原文

    赞 9 收藏 2 评论 0

    justjavac 赞了文章 · 2020-10-27

    React 架构的演变 - Hooks 的实现

    这是这个系列的最后一篇文章了,终于收尾了?? 。

    React Hooks 可以说完全颠覆了之前 Class Component 的写法,进一步增强了状态复用的能力,让 Function Component 也具有了内部状态,对于我个人来说,更加喜欢 Hooks 的写法。当然如果你是一个使用 Class Component 的老手,初期上手时会觉得很苦恼,毕竟之前沉淀的很多 HOC、Render Props 组件基本没法用。而且之前的 Function Component 是无副作用的无状态组件,现在又能通过 Hooks 引入状态,看起来真的很让人疑惑。Function Component 的另一个优势就是可以完全告别 this ,在 Class Component 里面 this 真的是一个让人讨厌的东西??。

    Hook 如何与组件关联

    在之前的文章中多次提到,Fiber 架构下的 updateQueueeffectList 都是链表的数据结构,然后挂载的 Fiber 节点上。而一个函数组件内所有的 Hooks 也是通过链表的形式存储的,最后挂载到 fiber.memoizedState 上。

    function App() {
      const [num, updateNum] = useState(0)
    
      return <div
        onClick={() => updateNum(num => num + 1)}
      >{ num }</div>
    }
    
    export default App

    我们先简单看下,调用 useState 时,构造链表的过程:

    var workInProgressHook = null
    var HooksDispatcherOnMount = {
      useState: function (initialState) {
        return mountState(initialState)
      }
    }
    
    function function mountState(initialState) {
      // 新的 Hook 节点
      var hook = mountWorkInProgressHook()
      // 缓存初始值
      hook.memoizedState = initialState
      // 构造更新队列,类似于 fiber.updateQueue
      var queue = hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedState: initialState
      }
      // 用于派发更新
      var dispatch = queue.dispatch = dispatchAction.bind(
        null, workInProgress, queue
      )
      // [num, updateNum] = useState(0)
      return [hook.memoizedState, dispatch]
    }
    
    function mountWorkInProgressHook() {
      var hook = {
        memoizedState: null,
        baseState: null,
        baseQueue: null,
        queue: null,
        next: null
      }
    
      if (workInProgressHook === null) {
        // 构造链表头节点
        workInProgress.memoizedState = workInProgressHook = hook
      } else {
        // 如果链表已经存在,在挂载到 next
        workInProgressHook = workInProgressHook.next = hook
      }
    
      return workInProgressHook
    }

    Hook

    如果此时有两个 Hook,第二个 Hook 就会挂载到第一个 Hook 的 next 属性上。

    function App() {
      const [num, updateNum] = useState(0)
      const [str, updateStr] = useState('value: ')
    
      return <div
        onClick={() => updateNum(num => num + 1)}
      >{ str } { num }</div>
    }
    
    export default App

    Hook

    Hook 的更新队列

    Hook 通过 .next 彼此相连,而每个 Hook 对象下,还有个 queue 字段,该字段和 Fiber 节点上的 updateQueue 一样,是一个更新队列在,上篇文章 《React 架构的演变-更新机制》中有讲到,React Fiber 架构中,更新队列通过链表结构进行存储。

    class App extends React.Component {
      state = { val: 0 }
      click () {
        for (let i = 0; i < 3; i++) {
          this.setState({ val: this.state.val + 1 })
        }
      }
      render() {
        return <div onClick={() => {
          this.click()
        }}>val: { this.state.val }</div>
      }
    }

    点击 div 之后,产生的 3 次 setState 通过链表的形式挂载到 fiber.updateQueue 上,待到 MessageChannel 收到通知后,真正执行更新操作时,取出更新队列,将计算结果更新到 fiber.memoizedState

    setState

    hook.queue 的逻辑和 fiber.updateQueue 的逻辑也是完全一致的。

    function App() {
      const [num, updateNum] = useState(0)
    
      return <div
        onClick={() => {
          // 连续更新 3 次
          updateNum(num => num + 1)
          updateNum(num => num + 1)
          updateNum(num => num + 1)
        }}
      >
        { num }
      </div>
    }
    
    export default App;
    var dispatch = queue.dispatch = dispatchAction.bind(
      null, workInProgress, queue
    )
    // [num, updateNum] = useState(0)
    return [hook.memoizedState, dispatch]

    调用 useState 的时候,返回的数组第二个参数为 dispatch,而 dispatchdispatchAction bind 后得到。

    function dispatchAction(fiber, queue, action) {
      var update = {
        next: null,
        action: action,
        // 省略调度相关的参数...
      };
    
      var pending = queue.pending
      if (pending === null) {
        update.next = update
      } else {
        update.next = pending.next
        pending.next = update
      }
      queue.pending = update
    
      // 执行更新
      scheduleUpdateOnFiber()
    }

    可以看到这里构造链表的方式与 fiber.updateQueue 如出一辙。之前我们通过 updateNumnum 连续更新了 3 次,最后形成的更新队列如下:

    更新队列

    函数组件的更新

    前面的文章分享过,Fiber 架构下的更新流程分为递(beginWork)、归(completeWork)两个步骤,在 beginWork 中,会依据组件类型进行 render 操作构造子组件。

    function beginWork(current, workInProgress) {
      switch (workInProgress.tag) {
        // 其他类型组件代码省略...
        case FunctionComponent: {
          // 这里的 type 就是函数组件的函数
          // 例如,前面的 App 组件,type 就是 function App() {}
          var Component = workInProgress.type
          var resolvedProps = workInProgress.pendingProps
          // 组件更新
          return updateFunctionComponent(
            current, workInProgress, Component, resolvedProps
          )
        }
      }
    }
    
    function updateFunctionComponent(
        current, workInProgress, Component, nextProps
    ) {
      // 构造子组件
      var nextChildren = renderWithHooks(
        current, workInProgress, Component, nextProps
      )
      reconcileChildren(current, workInProgress, nextChildren)
      return workInProgress.child
    }
    

    看名字就能看出来,renderWithHooks 方法就是构造带 Hooks 的子组件。

    function renderWithHooks(
        current, workInProgress, Component, props
    ) {
      if (current !== null && current.memoizedState !== null) {
        ReactCurrentDispatcher.current = HooksDispatcherOnUpdate
      } else {
        ReactCurrentDispatcher.current = HooksDispatcherOnMount
      }
      var children = Component(props)
      return children
    }

    从上面的代码可以看出,函数组件更新或者首次渲染时,本质就是将函数取出执行了一遍。不同的地方在于给 ReactCurrentDispatcher 进行了不同的赋值,而 ReactCurrentDispatcher 的值最终会影响 useState 调用不同的方法。

    根据之前文章讲过的双缓存机制,current 存在的时候表示是更新操作,不存在的时候表示首次渲染。

    function useState(initialState) {
      // 首次渲染时指向 HooksDispatcherOnMount
      // 更新操作时指向 HooksDispatcherOnUpdate
      var dispatcher = ReactCurrentDispatcher.current
      return dispatcher.useState(initialState)
    }

    HooksDispatcherOnMount.useState 的代码前面已经介绍过,这里不再着重介绍。

    // HooksDispatcherOnMount 的代码前面已经介绍过
    var HooksDispatcherOnMount = {
      useState: function (initialState) {
        return mountState(initialState)
      }
    }

    我们重点看看 HooksDispatcherOnMount.useState 的逻辑。

    var HooksDispatcherOnUpdateInDEV = {
      useState: function (initialState) {
        return updateState()
      }
    }
    
    function updateState() {
      // 取出当前 hook
      workInProgressHook = nextWorkInProgressHook
      nextWorkInProgressHook = workInProgressHook.next
    
      var hook = nextWorkInProgressHook
      var queue = hook.queue
      var pendingQueue = queue.pending
    
      // 处理更新
      var first = pendingQueue.next
      var state = hook.memoizedState
      var update = first
    
      do {
        var action = update.action
        state = typeof action === 'function' ? action(state) : action
    
        update = update.next;
      } while (update !== null && update !== first)
    
    
      hook.memoizedState = state
    
      var dispatch = queue.dispatch
      return [hook.memoizedState, dispatch]
    }

    如果有看之前的 setState 的代码,这里的逻辑其实是一样的。将更新对象的 action 取出,如果是函数就执行,如果不是函数就直接对 state 进行替换操作。

    总结

    React 系列的文章终于写完了,这一篇文章应该是最简单的一篇,如果想抛开 React 源码,单独看 Hooks 实现可以看这篇文章:《React Hooks 原理》。Fiber 架构为了能够实现循环的方式更新,将所有涉及到数据的地方结构都改成了链表,这样的优势就是可以随时中断,为异步模式让路,Fiber 树就像一颗圣诞树,上面挂满了各种彩灯(alternateEffectListupdateQueueHooks)。

    推荐大家可以将这个系列从头到尾看一遍,相信会特别有收获的。

    image

    查看原文

    赞 12 收藏 8 评论 2

    justjavac 赞了文章 · 2020-10-23

    jvm类加载器,类加载机制详解,看这一篇就够了

    前言

    今天我们来讲讲jvm类加载的过程,我们写了那么多类,却不知道类的加载过程,岂不是很尴尬

    jvm的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由jvm的具体实现指定的。[来自官方规范]

    jvm组成结构之一就是类装载器子系统,我们今天就来仔细讲讲这个组件。

    Java代码执行流程图

    大家通过这个流程图,了解一下我们写好的Java代码是如何执行的,其中要经历类加载器这个流程,我们就来仔细讲讲这里面的知识点。

    image.png

    类加载子系统

    image.png

    类加载系统架构图

    暂时看不懂这两张图没关系,跟着老哥往下看

    image.png

    类的生命周期

    类的生命周期包括:加载、链接、初始化、使用和卸载,其中加载链接初始化,属于类加载的过程,我们下面仔细讲解。使用是指我们new对象进行使用,卸载指对象被垃圾回收掉了。

    类加载的过程

    image.png

    • 第一步:Loading加载
    通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流

    将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构

    内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

    总结:加载二进制数据到内存 —> 映射成jvm能识别的结构 —> 在内存中生成class文件

    • 第二步:Linking链接

    链接是指将上面创建好的class类合并至Java虚拟机中,使之能够执行的过程,可分为验证准备解析三个阶段。

    ① 验证(Verify)

    确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。

    ② 准备(Prepare)

    为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了

    ③ 解析(Resolve)

    解析阶段的目的,是将常量池内的符号引用转换为直接引用的过程(将常量池内的符号引用解析成为实际引用)。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

    事实上,解析器操作往往会伴随着 JVM 在执行完初始化之后再执行。 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java 虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

    解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

    • 第三步:initialization初始化
    初始化就是执行类的构造器方法init()的过程。

    这个方法不需要定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并来的。

    若该类具有父类,jvm会保证父类的init先执行,然后在执行子类的init

    类加载器的分类

    image.png

    • 第一个:启动类/引导类:Bootstrap ClassLoader
    这个类加载器使用C/C++语言实现的,嵌套在JVM内部,java程序无法直接操作这个类。

    它用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的包,用于提供jvm运行所需的包。

    并不是继承自java.lang.ClassLoader,它没有父类加载器

    它加载扩展类加载器应用程序类加载器,并成为他们的父类加载器

    出于安全考虑,启动类只加载包名为:java、javax、sun开头的类

    • 第二个:扩展类加载器:Extension ClassLoader
    Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,我们可以用Java程序操作这个加载器

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

    从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext目录下加载类库。我们就可以将我们自己的包放在以上目录下,就会自动加载进来了。

    • 第三个:应用程序类加载器:Application Classloader
    Java语言编写,由sun.misc.Launcher$AppClassLoader实现。

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

    它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库

    它是程序中默认的类加载器,我们Java程序中的类,都是由它加载完成的。

    我们可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器

    • 第四个:自定义加载器
    一般情况下,以上3种加载器能满足我们日常的开发工作,不满足时,我们还可以自定义加载器

    比如用网络加载Java类,为了保证传输中的安全性,采用了加密操作,那么以上3种加载器就无法加载这个类,这时候就需要自定义加载器

    自定义加载器实现步骤

    继承java.lang.ClassLoader类,重写findClass()方法

    如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoaderExtClassLoader

    获取ClassLoader几种方式

    它是一个抽象类,其后所有的类加载器继承自 ClassLoader(不包括启动类加载器)

    //?方式一:获取当前类的?ClassLoader
    clazz.getClassLoader()
    //?方式二:获取当前线程上下文的?ClassLoader
    Thread.currentThread().getContextClassLoader()
    //?方式三:获取系统的?ClassLoader
    ClassLoader.getSystemClassLoader()
    //?方式四:获取调用者的?ClassLoader
    DriverManager.getCallerClassLoader()

    类加载机制—双亲委派机制

    jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。

    在加载类的时候,是采用的双亲委派机制,即把请求交给父类处理的一种任务委派模式。

    image.png

    • 工作原理

    (1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

    (2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

    (3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

    • 第三方包加载方式:反向委派机制

    在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载。而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器(双亲委派模型的破坏者)就是很好的选择。

    从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。

    image.png

    • 沙箱安全机制

    自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 JDK 自带的文件(rt.jar 包中的 javalangString.class),报错信息说没有 main 方法就是因为加载的 rt.jar 包中的 String 类。这样可以保证对 Java 核心源代码的保护,这就是沙箱安全机制。

    IT 老哥

    我是一个通过大学四年自学,进入大厂做高级java开

    发的程序员。关注我,每天分享技术干货,助你进大

    厂。我可以,你也可以的。

    如果你觉得文章写的还不错,请帮老哥点个赞!
    查看原文

    赞 11 收藏 9 评论 0

    justjavac 回答了问题 · 2020-10-03

    有什么用vuePress做的,比较好的知识库吗?

    维基百科就是开源的 mediawiki

    关注 2 回答 3

    justjavac 发布了文章 · 2020-09-15

    鸿蒙系统中的 JS 开发框架

    今天鸿蒙终于发布了,开发者们也终于“沸腾”了。

    源码托管在国内知名开源平台码云上,https://gitee.com/openharmony

    我也第一时间下载了源码,研究了一个晚上,顺带写了一个 hello world 程序,还顺手给鸿蒙文档提了 2 个 PR。

    当然我最感兴趣的就是鸿蒙的 JS 框架 ace_lite_jsfwk,从名字中可以看出来这是一个非常轻量级的框架,官方介绍说是“轻量级 JS 核心开发框架”。

    当我看完源码后发现它确实轻。其核心代码只有 5 个 js 文件,大概也就 300-400 行代码吧。(没有单元测试)

    • runtime-coresrccoreindex.js
    • runtime-coresrcobserverobserver.js
    • runtime-coresrcobserversubject.js
    • runtime-coresrcobserverutils.js
    • runtime-coresrcprofilerindex.js

    从名字可以看出来,这些代码实现了一个观察者模式。也就是说,它实现了一个非常轻量级的 MVVM 模式。通过使用和 vue2 相似的属性劫持技术实现了响应式系统。这个应该是目前培训班的“三大自己实现”之一了吧。(自己实现 Promise,自己实现 vue,自己实现 react)

    utils 里面定义了一个 Observer 栈,存放了观察者。subject 定义了被观察者。当我们观察某个对象时,也就是劫持这个对象属性的操作,还包括一些数组函数,比如 push、pop 等。这个文件应该是代码最多的,160 行。observer 的代码就更简单了,五六十行。

    而当我们开发的时候,通过 Toolkit 将开发者编写的 HML、CSS 和 JS 文件编译打包成 JS Bundle,然后再将 JS Bundle 解析运行成C++ native UI 的 View 组件进行渲染。

    “通过支持三方开发者使用声明式的 API 进行应用开发,以数据驱动视图变化,避免了大量的视图操作,大大降低了应用开发难度,提升开发者开发体验”。基本上就是一个小程序式的开发体验。

    在 srccorebaseframework_min_js.h 文件中,这段编译好的 js 被编译到了 runtime 里面。编译完的 js 文件不到 3K,确实够轻量。

    js runtime 没有使用 V8,也没有使用 jscore。而是选择了 JerryScript。JerryScript 是用于物联网的超轻量 JavaScript 引擎。它能够在内存少于 64 KB 的设备上执行 ECMAScript 5.1 源代码。这也是为什么在文档中说鸿蒙 JS 框架支持 ECMAScript 5.1 的原因。

    从整体看这个 js 框架大概使用了 96% 的 C/C++ 代码,1.8% 的 JS 代码。在 htm 文件中写的组件会被编译为原生组件。而 app_style_manager.cpp 和同级的七八个文件则用来解析 css,最终生成原生布局。

    虽然在 SDK 中有几个 weex 包,也发现了 react 的影子。但是在 C/C++ 代码中并没有看到 yoga 相关的内容(全局搜索没发现)。而 SDK 中的那些包仅仅是做 loader 用的,大概是为了在 webpack 打包时解析 htm 组件用的。将 htm 的 template 编译为 js 代码。

    整体而言,比我预想的要好一些。

    查看原文

    赞 16 收藏 7 评论 3

    justjavac 发布了文章 · 2020-09-15

    逐行分析鸿蒙系统的 JavaScript 框架

    我在前文中曾经介绍过鸿蒙的 Javascript 框架,这几天终于把 JS 仓库编译通过了,期间踩了不少坑,也给鸿蒙贡献了几个 PR。今天我们就来逐行分析鸿蒙系统中的 JS 框架。

    文中的所有代码都基于鸿蒙的当前最新版(版本为 677ed06,提交日期为 2020-09-10)。

    鸿蒙系统使用 JavaScript 开发 GUI 是一种类似于微信小程序、轻应用的模式。而这个 MVVM 模式中,V 其实是由 C++ 来承担的。JavaScript 代码只是其中的 ViewModel 层。

    鸿蒙 JS 框架是零依赖的,只在开发打包过程中使用到了一些 npm 包。打包完之的代码是没有依赖任何 npm 包的。我们先看一下使用鸿蒙 JS 框架写出来的 JS 代码到底长什么样。

    export default {
      data() {
        return { count: 1 };
      },
      increase() {
        ++this.count;
      },
      decrease() {
        --this.count;
      },
    }

    如果我不告诉你这是鸿蒙,你甚至会以为它是 vue 或小程序。如果单独把 JS 拿出来使用(脱离鸿蒙系统),代码是这样:

    const vm = new ViewModel({
      data() {
        return { count: 1 };
      },
      increase() {
        ++this.count;
      },
      decrease() {
        --this.count;
      },
    });
    
    console.log(vm.count); // 1
    
    vm.increase();
    console.log(vm.count); // 2
    
    vm.decrease();
    console.log(vm.count); // 1

    仓库中的所有 JS 代码实现了一个响应式系统,充当了 MVVM 中的 ViewModel。

    下面我们逐行分析。

    src 目录中一共有 4 个目录,总计 8 个文件。其中 1 个是单元测试。还有 1 个性能分析。再除去 2 个 index.js 文件,有用的文件一共是 4 个。也是本文分析的重点。

    src
    ├── __test__
    │   └── index.test.js
    ├── core
    │   └── index.js
    ├── index.js
    ├── observer
    │   ├── index.js
    │   ├── observer.js
    │   ├── subject.js
    │   └── utils.js
    └── profiler
        └── index.js

    首先是入口文件,src/index.js,只有 2 行代码:

    import { ViewModel } from './core';
    export default ViewModel;

    其实就是重新导出。

    另一个类似的文件是 src/observer/index.js,也是 2 行代码:

    export { Observer } from './observer';
    export { Subject } from './subject';

    observer 和 subject 实现了一个观察者模式。subject 是主题,也就是被观察者。observer 是观察者。当 subject 有任何变化时需要主动通知被观察者。这就是响应式。

    这 2 个文件都使用到了 src/observer/utils.js,所以我们先分析一下 utils 文件。分 3 部分。

    第一部分

    export const ObserverStack = {
      stack: [],
      push(observer) {
        this.stack.push(observer);
      },
      pop() {
        return this.stack.pop();
      },
      top() {
        return this.stack[this.stack.length - 1];
      }
    };

    首先是定义了一个用来存放观察者的栈,遵循后进先出的原则,内部使用 stack 数组来存储。

    • 入栈操作 push,和数组的 push 函数一样,在栈顶放入一个观察者 observer。
    • 出栈操作 pop,和数组的 pop 函数一样,在将栈顶的观察者删除,并返回这个被删除的观察者。
    • 取栈顶元素 top,和 pop 操作不同,top 是把栈顶元素取出来,但是并不删除。

    第二部分

    export const SYMBOL_OBSERVABLE = '__ob__';
    export const canObserve = target => typeof target === 'object';

    定义了一个字符串常量 SYMBOL_OBSERVABLE。为了后面用着方便。

    定义了一个函数 canObserve,目标是否可以被观察。只有对象才能被观察,所以使用 typeof 来判断目标的类型。等等,好像有什么不对。如果 targetnull 的话,函数也会返回 true。如果 null 不可观察,那么这就是一个 bug。(写这篇文章的时候我已经提了一个 PR,并询问了这种行为是否是期望的行为)。

    第三部分

    export const defineProp = (target, key, value) => {
      Object.defineProperty(target, key, { enumerable: false, value });
    };

    这个没有什么好解释的,就是 Object.defineProperty 代码太长了,定义一个函数来避免代码重复。

    下面再来分析观察者 src/observer/observer.js,分 4 部分。

    第一部分

    export function Observer(context, getter, callback, meta) {
      this._ctx = context;
      this._getter = getter;
      this._fn = callback;
      this._meta = meta;
      this._lastValue = this._get();
    }

    构造函数。接受 4 个参数。

    context 当前观察者所处的上下午,类型是 ViewModel。当第三个参数 callback 调用时,函数的 this 就是这个 context

    getter 类型是一个函数,用来获取某个属性的值。

    callback 类型是一个函数,当某个值变化后执行的回调函数。

    meta 元数据。观察者(Observer)并不关注 meta 元数据。

    在构造函数的最后一行,this._lastValue = this._get()。下面来分析 _get 函数。

    第二部分

    Observer.prototype._get = function() {
      try {
        ObserverStack.push(this);
        return this._getter.call(this._ctx);
      } finally {
        ObserverStack.pop();
      }
    };

    ObserverStack 就是上面分析过的用来存储所有观察者的栈。将当前观察者入栈,并通过 _getter 取得当前值。结合第一部分的构造函数,这个值存储在了 _lastValue 属性中。

    执行完这个过程后,这个观察者就已经初始化完成了。

    第三部分

    Observer.prototype.update = function() {
      const lastValue = this._lastValue;
      const nextValue = this._get();
      const context = this._ctx;
      const meta = this._meta;
    
      if (nextValue !== lastValue || canObserve(nextValue)) {
        this._fn.call(context, nextValue, lastValue, meta);
        this._lastValue = nextValue;
      }
    };

    这部分实现了数据更新时的脏检查(Dirty checking)机制。比较更新后的值和当前值,如果不同,那么就执行回调函数。如果这个回调函数是渲染 UI,那么则可以实现按需渲染。如果值相同,那么再检查设置的新值是否可以被观察,再决定到底要不要执行回调函数。

    第四部分

    Observer.prototype.subscribe = function(subject, key) {
      const detach = subject.attach(key, this);
      if (typeof detach !== 'function') {
        return;
      }
      if (!this._detaches) {
        this._detaches = [];
      }
      this._detaches.push(detach);
    };
    
    Observer.prototype.unsubscribe = function() {
      const detaches = this._detaches;
      if (!detaches) {
        return;
      }
      while (detaches.length) {
        detaches.pop()();
      }
    };

    订阅与取消订阅。

    我们前面经常说观察者和被观察者。对于观察者模式其实还有另一种说法,叫订阅/发布模式。而这部分代码则实现了对主题(subject)的订阅。

    先调用主题的 attach 方法进行订阅。如果订阅成功,subject.attach 方法会返回一个函数,当调用这个函数就会取消订阅。为了将来能够取消订阅,这个返回值必需保存起来。

    subject 的实现很多人应该已经猜到了。观察者订阅了 subject,那么 subject 需要做的就是,当数据变化时即使通知观察者。subject 如何知道数据发生了变化呢,机制和 vue2 一样,使用 Object.defineProperty 做属性劫持。

    下面再来分析观察者 src/observer/subject.js,分 7 部分。

    第一部分

    export function Subject(target) {
      const subject = this;
      subject._hijacking = true;
      defineProp(target, SYMBOL_OBSERVABLE, subject);
    
      if (Array.isArray(target)) {
        hijackArray(target);
      }
    
      Object.keys(target).forEach(key => hijack(target, key, target[key]));
    }

    构造函数。基本没什么难点。设置 _hijacking 属性为 true,用来标示这个对象已经被劫持了。Object.keys 通过遍历来劫持每个属性。如果是数组,则调用 hijackArray

    第二部分

    两个静态方法。

    Subject.of = function(target) {
      if (!target || !canObserve(target)) {
        return target;
      }
      if (target[SYMBOL_OBSERVABLE]) {
        return target[SYMBOL_OBSERVABLE];
      }
      return new Subject(target);
    };
    
    Subject.is = function(target) {
      return target && target._hijacking;
    };

    Subject 的构造函数并不直接被外部调用,而是封装到了 Subject.of 静态方法中。

    如果目标不能被观察,那么直接返回目标。

    如果 target[SYMBOL_OBSERVABLE] 不是 undefined,说明目标已经被初始化过了。

    否则,调用构造函数初始化 Subject。

    Subject.is 则用来判断目标是否被劫持过了。

    第三部分

    Subject.prototype.attach = function(key, observer) {
      if (typeof key === 'undefined' || !observer) {
        return;
      }
      if (!this._obsMap) {
        this._obsMap = {};
      }
      if (!this._obsMap[key]) {
        this._obsMap[key] = [];
      }
      const observers = this._obsMap[key];
      if (observers.indexOf(observer) < 0) {
        observers.push(observer);
        return function() {
          observers.splice(observers.indexOf(observer), 1);
        };
      }
    };

    这个方法很眼熟,对,就是上文的 Observer.prototype.subscribe 中调用的。作用是某个观察者用来订阅主题。而这个方法则是“主题是怎么订阅的”。

    观察者维护这一个主题的哈希表 _obsMap。哈希表的 key 是需要订阅的 key。比如某个观察者订阅了 name 属性的变化,而另一个观察者订阅了 age 属性的变化。而且属性的变化还可以被多个观察者同时订阅,因此哈希表存储的值是一个数组,数据的每个元素都是一个观察者。

    第四部分

    Subject.prototype.notify = function(key) {
      if (
        typeof key === 'undefined' ||
        !this._obsMap ||
        !this._obsMap[key]
      ) {
        return;
      }
      this._obsMap[key].forEach(observer => observer.update());
    };

    当属性发生变化是,通知订阅了此属性的观察者们。遍历每个观察者,并调用观察者的 update 方法。我们上文中也提到了,脏检查就是在这个方法内完成的。

    第五部分

    Subject.prototype.setParent = function(parent, key) {
      this._parent = parent;
      this._key = key;
    };
    
    Subject.prototype.notifyParent = function() {
      this._parent && this._parent.notify(this._key);
    };

    这部分是用来处理属性嵌套(nested object)的问题的。就是类似这种对象:{ user: { name: 'JJC' } }

    第六部分

    function hijack(target, key, cache) {
      const subject = target[SYMBOL_OBSERVABLE];
    
      Object.defineProperty(target, key, {
        enumerable: true,
        get() {
          const observer = ObserverStack.top();
          if (observer) {
            observer.subscribe(subject, key);
          }
    
          const subSubject = Subject.of(cache);
          if (Subject.is(subSubject)) {
            subSubject.setParent(subject, key);
          }
    
          return cache;
        },
        set(value) {
          cache = value;
          subject.notify(key);
        }
      });
    }

    这一部分展示了如何使用 Object.defineProperty 进行属性劫持。当设置属性时,会调用 set(value),设置新的值,然后调用 subject 的 notify 方法。这里并不进行任何检查,只要设置了属性就会调用,即使属性的新值和旧值一样。notify 会通知所有的观察者。

    第七部分

    劫持数组方法。

    const ObservedMethods = {
      PUSH: 'push',
      POP: 'pop',
      UNSHIFT: 'unshift',
      SHIFT: 'shift',
      SPLICE: 'splice',
      REVERSE: 'reverse'
    };
    
    const OBSERVED_METHODS = Object.keys(ObservedMethods).map(
        key => ObservedMethods[key]
    );

    ObservedMethods 定义了需要劫持的数组函数。前面大写的用来做 key,后面小写的是需要劫持的方法。

    function hijackArray(target) {
      OBSERVED_METHODS.forEach(key => {
        const originalMethod = target[key];
    
        defineProp(target, key, function() {
          const args = Array.prototype.slice.call(arguments);
          originalMethod.apply(this, args);
    
          let inserted;
          if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) {
            inserted = args;
          } else if (ObservedMethods.SPLICE) {
            inserted = args.slice(2);
          }
    
          if (inserted && inserted.length) {
            inserted.forEach(Subject.of);
          }
    
          const subject = target[SYMBOL_OBSERVABLE];
          if (subject) {
            subject.notifyParent();
          }
        });
      });
    }

    数组的劫持和对象不同,不能使用 Object.defineProperty

    我们需要劫持 6 个数组方法。分别是头部添加、头部删除、尾部添加、尾部删除、替换/删除某几项、数组反转。

    通过重写数组方法实现了数组的劫持。但是这里有一个需要注意的地方,数据的每一个元素都是被观察过的,但是当在数组中添加了新元素时,这些元素还没有被观察。因此代码中还需要判断当前的方法如果是 pushunshiftsplice,那么需要将新的元素放入观察者队列中。

    另外两个文件分别是单元测试和性能分析,这里就不再分析了。

    查看原文

    赞 28 收藏 12 评论 9

    认证与成就

    • SegmentFault 讲师
    • 获得 5496 次点赞
    • 获得 131 枚徽章 获得 5 枚金徽章, 获得 39 枚银徽章, 获得 87 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    注册于 2012-03-07
    个人主页被 59.7k 人浏览

    bt365体育投注